1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Conversation.Participation
13 alias Pleroma.Formatter
15 alias Pleroma.Notification
17 alias Pleroma.Pagination
19 alias Pleroma.ScheduledActivity
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
44 alias Pleroma.Web.ControllerHelper
49 plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
50 plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
52 @local_mastodon_name "Mastodon-Local"
54 action_fallback(:errors)
56 def create_app(conn, params) do
57 scopes = Scopes.fetch_scopes(params, ["read"])
61 |> Map.drop(["scope", "scopes"])
62 |> Map.put("scopes", scopes)
64 with cs <- App.register_changeset(%App{}, app_attrs),
65 false <- cs.changes[:client_name] == @local_mastodon_name,
66 {:ok, app} <- Repo.insert(cs) do
69 |> render("show.json", %{app: app})
78 value_function \\ fn x -> {:ok, x} end
80 if Map.has_key?(params, params_field) do
81 case value_function.(params[params_field]) do
82 {:ok, new_value} -> Map.put(map, map_field, new_value)
90 def update_credentials(%{assigns: %{user: user}} = conn, params) do
95 |> add_if_present(params, "display_name", :name)
96 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
97 |> add_if_present(params, "avatar", :avatar, fn value ->
98 with %Plug.Upload{} <- value,
99 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
106 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
109 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 :skip_thread_containment
122 |> Enum.reduce(%{}, fn key, acc ->
123 add_if_present(acc, params, to_string(key), key, fn value ->
124 {:ok, ControllerHelper.truthy_param?(value)}
127 |> add_if_present(params, "default_scope", :default_scope)
128 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
129 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
131 |> add_if_present(params, "header", :banner, fn value ->
132 with %Plug.Upload{} <- value,
133 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
139 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
140 with %Plug.Upload{} <- value,
141 {:ok, object} <- ActivityPub.upload(value, type: :background) do
147 |> Map.put(:emoji, user_info_emojis)
149 info_cng = User.Info.profile_update(user.info, info_params)
151 with changeset <- User.update_changeset(user, user_params),
152 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
153 {:ok, user} <- User.update_and_set_cache(changeset) do
154 if original_user != user do
155 CommonAPI.update(user)
160 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
166 |> json(%{error: "Invalid request"})
170 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
171 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
174 AccountView.render("account.json", %{
177 with_pleroma_settings: true,
178 with_chat_token: chat_token
184 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
185 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
188 |> render("short.json", %{app: app})
192 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
193 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
194 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
195 account = AccountView.render("account.json", %{user: user, for: for_user})
201 |> json(%{error: "Can't find user"})
205 @mastodon_api_level "2.7.2"
207 def masto_instance(conn, _params) do
208 instance = Config.get(:instance)
212 title: Keyword.get(instance, :name),
213 description: Keyword.get(instance, :description),
214 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
215 email: Keyword.get(instance, :email),
217 streaming_api: Pleroma.Web.Endpoint.websocket_url()
219 stats: Stats.get_stats(),
220 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
222 registrations: Pleroma.Config.get([:instance, :registrations_open]),
223 # Extra (not present in Mastodon):
224 max_toot_chars: Keyword.get(instance, :limit),
225 poll_limits: Keyword.get(instance, :poll_limits)
231 def peers(conn, _params) do
232 json(conn, Stats.get_peers())
235 defp mastodonized_emoji do
236 Pleroma.Emoji.get_all()
237 |> Enum.map(fn {shortcode, relative_url, tags} ->
238 url = to_string(URI.merge(Web.base_url(), relative_url))
241 "shortcode" => shortcode,
243 "visible_in_picker" => true,
250 def custom_emojis(conn, _params) do
251 mastodon_emoji = mastodonized_emoji()
252 json(conn, mastodon_emoji)
255 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
258 |> Map.drop(["since_id", "max_id", "min_id"])
261 last = List.last(activities)
268 |> Map.get("limit", "20")
269 |> String.to_integer()
272 if length(activities) <= limit do
278 |> Enum.at(limit * -1)
282 {next_url, prev_url} =
286 Pleroma.Web.Endpoint,
289 Map.merge(params, %{max_id: max_id})
292 Pleroma.Web.Endpoint,
295 Map.merge(params, %{min_id: min_id})
301 Pleroma.Web.Endpoint,
303 Map.merge(params, %{max_id: max_id})
306 Pleroma.Web.Endpoint,
308 Map.merge(params, %{min_id: min_id})
314 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
320 def home_timeline(%{assigns: %{user: user}} = conn, params) do
323 |> Map.put("type", ["Create", "Announce"])
324 |> Map.put("blocking_user", user)
325 |> Map.put("muting_user", user)
326 |> Map.put("user", user)
329 [user.ap_id | user.following]
330 |> ActivityPub.fetch_activities(params)
334 |> add_link_headers(:home_timeline, activities)
335 |> put_view(StatusView)
336 |> render("index.json", %{activities: activities, for: user, as: :activity})
339 def public_timeline(%{assigns: %{user: user}} = conn, params) do
340 local_only = params["local"] in [true, "True", "true", "1"]
344 |> Map.put("type", ["Create", "Announce"])
345 |> Map.put("local_only", local_only)
346 |> Map.put("blocking_user", user)
347 |> Map.put("muting_user", user)
348 |> ActivityPub.fetch_public_activities()
352 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
353 |> put_view(StatusView)
354 |> render("index.json", %{activities: activities, for: user, as: :activity})
357 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
358 with %User{} = user <- User.get_cached_by_id(params["id"]) do
359 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
362 |> add_link_headers(:user_statuses, activities, params["id"])
363 |> put_view(StatusView)
364 |> render("index.json", %{
365 activities: activities,
372 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
375 |> Map.put("type", "Create")
376 |> Map.put("blocking_user", user)
377 |> Map.put("user", user)
378 |> Map.put(:visibility, "direct")
382 |> ActivityPub.fetch_activities_query(params)
383 |> Pagination.fetch_paginated(params)
386 |> add_link_headers(:dm_timeline, activities)
387 |> put_view(StatusView)
388 |> render("index.json", %{activities: activities, for: user, as: :activity})
391 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
392 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
393 true <- Visibility.visible_for_user?(activity, user) do
395 |> put_view(StatusView)
396 |> try_render("status.json", %{activity: activity, for: user})
400 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
401 with %Activity{} = activity <- Activity.get_by_id(id),
403 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
404 "blocking_user" => user,
408 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
410 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
411 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
417 activities: grouped_activities[true] || [],
421 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
426 activities: grouped_activities[false] || [],
430 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
437 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
438 with %Object{} = object <- Object.get_by_id(id),
439 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
440 true <- Visibility.visible_for_user?(activity, user) do
442 |> put_view(StatusView)
443 |> try_render("poll.json", %{object: object, for: user})
448 |> json(%{error: "Record not found"})
453 |> json(%{error: "Record not found"})
457 defp get_cached_vote_or_vote(user, object, choices) do
458 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
461 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
462 case CommonAPI.vote(user, object, choices) do
463 {:error, _message} = res -> {:ignore, res}
464 res -> {:commit, res}
471 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
472 with %Object{} = object <- Object.get_by_id(id),
473 true <- object.data["type"] == "Question",
474 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
475 true <- Visibility.visible_for_user?(activity, user),
476 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
478 |> put_view(StatusView)
479 |> try_render("poll.json", %{object: object, for: user})
484 |> json(%{error: "Record not found"})
489 |> json(%{error: "Record not found"})
494 |> json(%{error: message})
498 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
499 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
501 |> add_link_headers(:scheduled_statuses, scheduled_activities)
502 |> put_view(ScheduledActivityView)
503 |> render("index.json", %{scheduled_activities: scheduled_activities})
507 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
508 with %ScheduledActivity{} = scheduled_activity <-
509 ScheduledActivity.get(user, scheduled_activity_id) do
511 |> put_view(ScheduledActivityView)
512 |> render("show.json", %{scheduled_activity: scheduled_activity})
514 _ -> {:error, :not_found}
518 def update_scheduled_status(
519 %{assigns: %{user: user}} = conn,
520 %{"id" => scheduled_activity_id} = params
522 with %ScheduledActivity{} = scheduled_activity <-
523 ScheduledActivity.get(user, scheduled_activity_id),
524 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
526 |> put_view(ScheduledActivityView)
527 |> render("show.json", %{scheduled_activity: scheduled_activity})
529 nil -> {:error, :not_found}
534 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
535 with %ScheduledActivity{} = scheduled_activity <-
536 ScheduledActivity.get(user, scheduled_activity_id),
537 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
539 |> put_view(ScheduledActivityView)
540 |> render("show.json", %{scheduled_activity: scheduled_activity})
542 nil -> {:error, :not_found}
547 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
550 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
552 scheduled_at = params["scheduled_at"]
554 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
555 with {:ok, scheduled_activity} <-
556 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
558 |> put_view(ScheduledActivityView)
559 |> render("show.json", %{scheduled_activity: scheduled_activity})
562 params = Map.drop(params, ["scheduled_at"])
564 case CommonAPI.post(user, params) do
568 |> json(%{error: message})
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
578 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
579 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
585 |> json(%{error: "Can't delete this post"})
589 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
590 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
591 %Activity{} = announce <- Activity.normalize(announce.data) do
593 |> put_view(StatusView)
594 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
598 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
599 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
600 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
602 |> put_view(StatusView)
603 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
607 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
608 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
609 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
611 |> put_view(StatusView)
612 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
616 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
617 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
618 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
620 |> put_view(StatusView)
621 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
625 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
626 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
628 |> put_view(StatusView)
629 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
633 |> put_resp_content_type("application/json")
634 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
638 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
639 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
641 |> put_view(StatusView)
642 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
646 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
647 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
648 %User{} = user <- User.get_cached_by_nickname(user.nickname),
649 true <- Visibility.visible_for_user?(activity, user),
650 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
658 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
659 %User{} = user <- User.get_cached_by_nickname(user.nickname),
660 true <- Visibility.visible_for_user?(activity, user),
661 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
663 |> put_view(StatusView)
664 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
668 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
669 activity = Activity.get_by_id(id)
671 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
673 |> put_view(StatusView)
674 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
678 |> put_resp_content_type("application/json")
679 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
683 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
684 activity = Activity.get_by_id(id)
686 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
688 |> put_view(StatusView)
689 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
693 def notifications(%{assigns: %{user: user}} = conn, params) do
694 notifications = MastodonAPI.get_notifications(user, params)
697 |> add_link_headers(:notifications, notifications)
698 |> put_view(NotificationView)
699 |> render("index.json", %{notifications: notifications, for: user})
702 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
703 with {:ok, notification} <- Notification.get(user, id) do
705 |> put_view(NotificationView)
706 |> render("show.json", %{notification: notification, for: user})
710 |> put_resp_content_type("application/json")
711 |> send_resp(403, Jason.encode!(%{"error" => reason}))
715 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
716 Notification.clear(user)
720 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
721 with {:ok, _notif} <- Notification.dismiss(user, id) do
726 |> put_resp_content_type("application/json")
727 |> send_resp(403, Jason.encode!(%{"error" => reason}))
731 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
732 Notification.destroy_multiple(user, ids)
736 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
738 q = from(u in User, where: u.id in ^id)
739 targets = Repo.all(q)
742 |> put_view(AccountView)
743 |> render("relationships.json", %{user: user, targets: targets})
746 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
747 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
749 def update_media(%{assigns: %{user: user}} = conn, data) do
750 with %Object{} = object <- Repo.get(Object, data["id"]),
751 true <- Object.authorize_mutation(object, user),
752 true <- is_binary(data["description"]),
753 description <- data["description"] do
754 new_data = %{object.data | "name" => description}
758 |> Object.change(%{data: new_data})
761 attachment_data = Map.put(new_data, "id", object.id)
764 |> put_view(StatusView)
765 |> render("attachment.json", %{attachment: attachment_data})
769 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
770 with {:ok, object} <-
773 actor: User.ap_id(user),
774 description: Map.get(data, "description")
776 attachment_data = Map.put(object.data, "id", object.id)
779 |> put_view(StatusView)
780 |> render("attachment.json", %{attachment: attachment_data})
784 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
785 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
786 %{} = attachment_data <- Map.put(object.data, "id", object.id),
787 %{type: type} = rendered <-
788 StatusView.render("attachment.json", %{attachment: attachment_data}) do
789 # Reject if not an image
790 if type == "image" do
792 # Save to the user's info
793 info_changeset = User.Info.mascot_update(user.info, rendered)
797 |> Ecto.Changeset.change()
798 |> Ecto.Changeset.put_embed(:info, info_changeset)
800 {:ok, _user} = User.update_and_set_cache(user_changeset)
806 |> put_resp_content_type("application/json")
807 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
812 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
813 mascot = User.get_mascot(user)
819 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
820 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
821 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
822 q = from(u in User, where: u.ap_id in ^likes)
826 |> put_view(AccountView)
827 |> render("accounts.json", %{for: user, users: users, as: :user})
833 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
834 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
835 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
836 q = from(u in User, where: u.ap_id in ^announces)
840 |> put_view(AccountView)
841 |> render("accounts.json", %{for: user, users: users, as: :user})
847 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
848 local_only = params["local"] in [true, "True", "true", "1"]
851 [params["tag"], params["any"]]
855 |> Enum.map(&String.downcase(&1))
860 |> Enum.map(&String.downcase(&1))
865 |> Enum.map(&String.downcase(&1))
869 |> Map.put("type", "Create")
870 |> Map.put("local_only", local_only)
871 |> Map.put("blocking_user", user)
872 |> Map.put("muting_user", user)
873 |> Map.put("tag", tags)
874 |> Map.put("tag_all", tag_all)
875 |> Map.put("tag_reject", tag_reject)
876 |> ActivityPub.fetch_public_activities()
880 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
881 |> put_view(StatusView)
882 |> render("index.json", %{activities: activities, for: user, as: :activity})
885 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
886 with %User{} = user <- User.get_cached_by_id(id),
887 followers <- MastodonAPI.get_followers(user, params) do
890 for_user && user.id == for_user.id -> followers
891 user.info.hide_followers -> []
896 |> add_link_headers(:followers, followers, user)
897 |> put_view(AccountView)
898 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
902 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
903 with %User{} = user <- User.get_cached_by_id(id),
904 followers <- MastodonAPI.get_friends(user, params) do
907 for_user && user.id == for_user.id -> followers
908 user.info.hide_follows -> []
913 |> add_link_headers(:following, followers, user)
914 |> put_view(AccountView)
915 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
919 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
920 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
922 |> put_view(AccountView)
923 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
927 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
928 with %User{} = follower <- User.get_cached_by_id(id),
929 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
931 |> put_view(AccountView)
932 |> render("relationship.json", %{user: followed, target: follower})
936 |> put_resp_content_type("application/json")
937 |> send_resp(403, Jason.encode!(%{"error" => message}))
941 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
942 with %User{} = follower <- User.get_cached_by_id(id),
943 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
945 |> put_view(AccountView)
946 |> render("relationship.json", %{user: followed, target: follower})
950 |> put_resp_content_type("application/json")
951 |> send_resp(403, Jason.encode!(%{"error" => message}))
955 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
956 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
957 {_, true} <- {:followed, follower.id != followed.id},
958 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
960 |> put_view(AccountView)
961 |> render("relationship.json", %{user: follower, target: followed})
968 |> put_resp_content_type("application/json")
969 |> send_resp(403, Jason.encode!(%{"error" => message}))
973 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
974 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
975 {_, true} <- {:followed, follower.id != followed.id},
976 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
978 |> put_view(AccountView)
979 |> render("account.json", %{user: followed, for: follower})
986 |> put_resp_content_type("application/json")
987 |> send_resp(403, Jason.encode!(%{"error" => message}))
991 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
992 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
993 {_, true} <- {:followed, follower.id != followed.id},
994 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
996 |> put_view(AccountView)
997 |> render("relationship.json", %{user: follower, target: followed})
1000 {:error, :not_found}
1007 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1008 with %User{} = muted <- User.get_cached_by_id(id),
1009 {:ok, muter} <- User.mute(muter, muted) do
1011 |> put_view(AccountView)
1012 |> render("relationship.json", %{user: muter, target: muted})
1014 {:error, message} ->
1016 |> put_resp_content_type("application/json")
1017 |> send_resp(403, Jason.encode!(%{"error" => message}))
1021 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1022 with %User{} = muted <- User.get_cached_by_id(id),
1023 {:ok, muter} <- User.unmute(muter, muted) do
1025 |> put_view(AccountView)
1026 |> render("relationship.json", %{user: muter, target: muted})
1028 {:error, message} ->
1030 |> put_resp_content_type("application/json")
1031 |> send_resp(403, Jason.encode!(%{"error" => message}))
1035 def mutes(%{assigns: %{user: user}} = conn, _) do
1036 with muted_accounts <- User.muted_users(user) do
1037 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1042 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1043 with %User{} = blocked <- User.get_cached_by_id(id),
1044 {:ok, blocker} <- User.block(blocker, blocked),
1045 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1047 |> put_view(AccountView)
1048 |> render("relationship.json", %{user: blocker, target: blocked})
1050 {:error, message} ->
1052 |> put_resp_content_type("application/json")
1053 |> send_resp(403, Jason.encode!(%{"error" => message}))
1057 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1058 with %User{} = blocked <- User.get_cached_by_id(id),
1059 {:ok, blocker} <- User.unblock(blocker, blocked),
1060 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1062 |> put_view(AccountView)
1063 |> render("relationship.json", %{user: blocker, target: blocked})
1065 {:error, message} ->
1067 |> put_resp_content_type("application/json")
1068 |> send_resp(403, Jason.encode!(%{"error" => message}))
1072 def blocks(%{assigns: %{user: user}} = conn, _) do
1073 with blocked_accounts <- User.blocked_users(user) do
1074 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1079 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1080 json(conn, info.domain_blocks || [])
1083 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1084 User.block_domain(blocker, domain)
1088 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1089 User.unblock_domain(blocker, domain)
1093 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1094 with %User{} = subscription_target <- User.get_cached_by_id(id),
1095 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1097 |> put_view(AccountView)
1098 |> render("relationship.json", %{user: user, target: subscription_target})
1100 {:error, message} ->
1102 |> put_resp_content_type("application/json")
1103 |> send_resp(403, Jason.encode!(%{"error" => message}))
1107 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1108 with %User{} = subscription_target <- User.get_cached_by_id(id),
1109 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1111 |> put_view(AccountView)
1112 |> render("relationship.json", %{user: user, target: subscription_target})
1114 {:error, message} ->
1116 |> put_resp_content_type("application/json")
1117 |> send_resp(403, Jason.encode!(%{"error" => message}))
1121 def favourites(%{assigns: %{user: user}} = conn, params) do
1124 |> Map.put("type", "Create")
1125 |> Map.put("favorited_by", user.ap_id)
1126 |> Map.put("blocking_user", user)
1129 ActivityPub.fetch_activities([], params)
1133 |> add_link_headers(:favourites, activities)
1134 |> put_view(StatusView)
1135 |> render("index.json", %{activities: activities, for: user, as: :activity})
1138 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1139 with %User{} = user <- User.get_by_id(id),
1140 false <- user.info.hide_favorites do
1143 |> Map.put("type", "Create")
1144 |> Map.put("favorited_by", user.ap_id)
1145 |> Map.put("blocking_user", for_user)
1149 ["https://www.w3.org/ns/activitystreams#Public"] ++
1150 [for_user.ap_id | for_user.following]
1152 ["https://www.w3.org/ns/activitystreams#Public"]
1157 |> ActivityPub.fetch_activities(params)
1161 |> add_link_headers(:favourites, activities)
1162 |> put_view(StatusView)
1163 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1166 {:error, :not_found}
1171 |> json(%{error: "Can't get favorites"})
1175 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1176 user = User.get_cached_by_id(user.id)
1179 Bookmark.for_user_query(user.id)
1180 |> Pagination.fetch_paginated(params)
1184 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1187 |> add_link_headers(:bookmarks, bookmarks)
1188 |> put_view(StatusView)
1189 |> render("index.json", %{activities: activities, for: user, as: :activity})
1192 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1193 lists = Pleroma.List.for_user(user, opts)
1194 res = ListView.render("lists.json", lists: lists)
1198 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1199 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1200 res = ListView.render("list.json", list: list)
1206 |> json(%{error: "Record not found"})
1210 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1211 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1212 res = ListView.render("lists.json", lists: lists)
1216 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1217 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1218 {:ok, _list} <- Pleroma.List.delete(list) do
1226 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1227 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1228 res = ListView.render("list.json", list: list)
1233 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1235 |> Enum.each(fn account_id ->
1236 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1237 %User{} = followed <- User.get_cached_by_id(account_id) do
1238 Pleroma.List.follow(list, followed)
1245 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1247 |> Enum.each(fn account_id ->
1248 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1249 %User{} = followed <- User.get_cached_by_id(account_id) do
1250 Pleroma.List.unfollow(list, followed)
1257 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1258 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1259 {:ok, users} = Pleroma.List.get_following(list) do
1261 |> put_view(AccountView)
1262 |> render("accounts.json", %{for: user, users: users, as: :user})
1266 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1267 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1268 {:ok, list} <- Pleroma.List.rename(list, title) do
1269 res = ListView.render("list.json", list: list)
1277 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1278 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1281 |> Map.put("type", "Create")
1282 |> Map.put("blocking_user", user)
1283 |> Map.put("muting_user", user)
1285 # we must filter the following list for the user to avoid leaking statuses the user
1286 # does not actually have permission to see (for more info, peruse security issue #270).
1289 |> Enum.filter(fn x -> x in user.following end)
1290 |> ActivityPub.fetch_activities_bounded(following, params)
1294 |> put_view(StatusView)
1295 |> render("index.json", %{activities: activities, for: user, as: :activity})
1300 |> json(%{error: "Error."})
1304 def index(%{assigns: %{user: user}} = conn, _params) do
1305 token = get_session(conn, :oauth_token)
1308 mastodon_emoji = mastodonized_emoji()
1310 limit = Config.get([:instance, :limit])
1313 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1318 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1319 access_token: token,
1321 domain: Pleroma.Web.Endpoint.host(),
1324 unfollow_modal: false,
1327 auto_play_gif: false,
1328 display_sensitive_media: false,
1329 reduce_motion: false,
1330 max_toot_chars: limit,
1331 mascot: User.get_mascot(user)["url"]
1333 poll_limits: Config.get([:instance, :poll_limits]),
1335 delete_others_notice: present?(user.info.is_moderator),
1336 admin: present?(user.info.is_admin)
1340 default_privacy: user.info.default_scope,
1341 default_sensitive: false,
1342 allow_content_types: Config.get([:instance, :allowed_post_formats])
1344 media_attachments: %{
1345 accept_content_types: [
1361 user.info.settings ||
1391 push_subscription: nil,
1393 custom_emojis: mastodon_emoji,
1399 |> put_layout(false)
1400 |> put_view(MastodonView)
1401 |> render("index.html", %{initial_state: initial_state})
1404 |> put_session(:return_to, conn.request_path)
1405 |> redirect(to: "/web/login")
1409 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1410 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1412 with changeset <- Ecto.Changeset.change(user),
1413 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1414 {:ok, _user} <- User.update_and_set_cache(changeset) do
1419 |> put_resp_content_type("application/json")
1420 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1424 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1425 redirect(conn, to: local_mastodon_root_path(conn))
1428 @doc "Local Mastodon FE login init action"
1429 def login(conn, %{"code" => auth_token}) do
1430 with {:ok, app} <- get_or_make_app(),
1431 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1432 {:ok, token} <- Token.exchange_token(app, auth) do
1434 |> put_session(:oauth_token, token.token)
1435 |> redirect(to: local_mastodon_root_path(conn))
1439 @doc "Local Mastodon FE callback action"
1440 def login(conn, _) do
1441 with {:ok, app} <- get_or_make_app() do
1446 response_type: "code",
1447 client_id: app.client_id,
1449 scope: Enum.join(app.scopes, " ")
1452 redirect(conn, to: path)
1456 defp local_mastodon_root_path(conn) do
1457 case get_session(conn, :return_to) do
1459 mastodon_api_path(conn, :index, ["getting-started"])
1462 delete_session(conn, :return_to)
1467 defp get_or_make_app do
1468 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1469 scopes = ["read", "write", "follow", "push"]
1471 with %App{} = app <- Repo.get_by(App, find_attrs) do
1473 if app.scopes == scopes do
1477 |> Ecto.Changeset.change(%{scopes: scopes})
1485 App.register_changeset(
1487 Map.put(find_attrs, :scopes, scopes)
1494 def logout(conn, _) do
1497 |> redirect(to: "/")
1500 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1501 Logger.debug("Unimplemented, returning unmodified relationship")
1503 with %User{} = target <- User.get_cached_by_id(id) do
1505 |> put_view(AccountView)
1506 |> render("relationship.json", %{user: user, target: target})
1510 def empty_array(conn, _) do
1511 Logger.debug("Unimplemented, returning an empty array")
1515 def empty_object(conn, _) do
1516 Logger.debug("Unimplemented, returning an empty object")
1520 def get_filters(%{assigns: %{user: user}} = conn, _) do
1521 filters = Filter.get_filters(user)
1522 res = FilterView.render("filters.json", filters: filters)
1527 %{assigns: %{user: user}} = conn,
1528 %{"phrase" => phrase, "context" => context} = params
1534 hide: Map.get(params, "irreversible", false),
1535 whole_word: Map.get(params, "boolean", true)
1539 {:ok, response} = Filter.create(query)
1540 res = FilterView.render("filter.json", filter: response)
1544 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1545 filter = Filter.get(filter_id, user)
1546 res = FilterView.render("filter.json", filter: filter)
1551 %{assigns: %{user: user}} = conn,
1552 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1556 filter_id: filter_id,
1559 hide: Map.get(params, "irreversible", nil),
1560 whole_word: Map.get(params, "boolean", true)
1564 {:ok, response} = Filter.update(query)
1565 res = FilterView.render("filter.json", filter: response)
1569 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1572 filter_id: filter_id
1575 {:ok, _} = Filter.delete(query)
1581 def errors(conn, {:error, %Changeset{} = changeset}) do
1584 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1585 |> Enum.map_join(", ", fn {_k, v} -> v end)
1589 |> json(%{error: error_message})
1592 def errors(conn, {:error, :not_found}) do
1595 |> json(%{error: "Record not found"})
1598 def errors(conn, _) do
1601 |> json("Something went wrong")
1604 def suggestions(%{assigns: %{user: user}} = conn, _) do
1605 suggestions = Config.get(:suggestions)
1607 if Keyword.get(suggestions, :enabled, false) do
1608 api = Keyword.get(suggestions, :third_party_engine, "")
1609 timeout = Keyword.get(suggestions, :timeout, 5000)
1610 limit = Keyword.get(suggestions, :limit, 23)
1612 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1614 user = user.nickname
1618 |> String.replace("{{host}}", host)
1619 |> String.replace("{{user}}", user)
1621 with {:ok, %{status: 200, body: body}} <-
1626 recv_timeout: timeout,
1630 {:ok, data} <- Jason.decode(body) do
1633 |> Enum.slice(0, limit)
1638 case User.get_or_fetch(x["acct"]) do
1639 {:ok, %User{id: id}} -> id
1645 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1648 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1654 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1661 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1662 with %Activity{} = activity <- Activity.get_by_id(status_id),
1663 true <- Visibility.visible_for_user?(activity, user) do
1667 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1677 def reports(%{assigns: %{user: user}} = conn, params) do
1678 case CommonAPI.report(user, params) do
1681 |> put_view(ReportView)
1682 |> try_render("report.json", %{activity: activity})
1686 |> put_status(:bad_request)
1687 |> json(%{error: err})
1691 def account_register(
1692 %{assigns: %{app: app}} = conn,
1693 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1701 "captcha_answer_data",
1705 |> Map.put("nickname", nickname)
1706 |> Map.put("fullname", params["fullname"] || nickname)
1707 |> Map.put("bio", params["bio"] || "")
1708 |> Map.put("confirm", params["password"])
1710 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1711 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1713 token_type: "Bearer",
1714 access_token: token.token,
1716 created_at: Token.Utils.format_created_at(token)
1722 |> json(Jason.encode!(errors))
1726 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1729 |> json(%{error: "Missing parameters"})
1732 def account_register(conn, _) do
1735 |> json(%{error: "Invalid credentials"})
1738 def conversations(%{assigns: %{user: user}} = conn, params) do
1739 participations = Participation.for_user_with_last_activity_id(user, params)
1742 Enum.map(participations, fn participation ->
1743 ConversationView.render("participation.json", %{participation: participation, user: user})
1747 |> add_link_headers(:conversations, participations)
1748 |> json(conversations)
1751 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1752 with %Participation{} = participation <-
1753 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1754 {:ok, participation} <- Participation.mark_as_read(participation) do
1755 participation_view =
1756 ConversationView.render("participation.json", %{participation: participation, user: user})
1759 |> json(participation_view)
1763 def try_render(conn, target, params)
1764 when is_binary(target) do
1765 res = render(conn, target, params)
1770 |> json(%{error: "Can't display this activity"})
1776 def try_render(conn, _, _) do
1779 |> json(%{error: "Can't display this activity"})
1782 defp present?(nil), do: false
1783 defp present?(false), do: false
1784 defp present?(_), do: true