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 update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
171 change = Changeset.change(user, %{avatar: nil})
172 {:ok, user} = User.update_and_set_cache(change)
173 CommonAPI.update(user)
175 json(conn, %{url: nil})
178 def update_avatar(%{assigns: %{user: user}} = conn, params) do
179 {:ok, object} = ActivityPub.upload(params, type: :avatar)
180 change = Changeset.change(user, %{avatar: object.data})
181 {:ok, user} = User.update_and_set_cache(change)
182 CommonAPI.update(user)
183 %{"url" => [%{"href" => href} | _]} = object.data
185 json(conn, %{url: href})
188 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
189 with new_info <- %{"banner" => %{}},
190 info_cng <- User.Info.profile_update(user.info, new_info),
191 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
192 {:ok, user} <- User.update_and_set_cache(changeset) do
193 CommonAPI.update(user)
195 json(conn, %{url: nil})
199 def update_banner(%{assigns: %{user: user}} = conn, params) do
200 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
201 new_info <- %{"banner" => object.data},
202 info_cng <- User.Info.profile_update(user.info, new_info),
203 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
204 {:ok, user} <- User.update_and_set_cache(changeset) do
205 CommonAPI.update(user)
206 %{"url" => [%{"href" => href} | _]} = object.data
208 json(conn, %{url: href})
212 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
213 with new_info <- %{"background" => %{}},
214 info_cng <- User.Info.profile_update(user.info, new_info),
215 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
216 {:ok, _user} <- User.update_and_set_cache(changeset) do
217 json(conn, %{url: nil})
221 def update_background(%{assigns: %{user: user}} = conn, params) do
222 with {:ok, object} <- ActivityPub.upload(params, type: :background),
223 new_info <- %{"background" => object.data},
224 info_cng <- User.Info.profile_update(user.info, new_info),
225 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
226 {:ok, _user} <- User.update_and_set_cache(changeset) do
227 %{"url" => [%{"href" => href} | _]} = object.data
229 json(conn, %{url: href})
233 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
234 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
237 AccountView.render("account.json", %{
240 with_pleroma_settings: true,
241 with_chat_token: chat_token
247 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
248 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
251 |> render("short.json", %{app: app})
255 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
256 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
257 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
258 account = AccountView.render("account.json", %{user: user, for: for_user})
264 |> json(%{error: "Can't find user"})
268 @mastodon_api_level "2.7.2"
270 def masto_instance(conn, _params) do
271 instance = Config.get(:instance)
275 title: Keyword.get(instance, :name),
276 description: Keyword.get(instance, :description),
277 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
278 email: Keyword.get(instance, :email),
280 streaming_api: Pleroma.Web.Endpoint.websocket_url()
282 stats: Stats.get_stats(),
283 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
285 registrations: Pleroma.Config.get([:instance, :registrations_open]),
286 # Extra (not present in Mastodon):
287 max_toot_chars: Keyword.get(instance, :limit),
288 poll_limits: Keyword.get(instance, :poll_limits)
294 def peers(conn, _params) do
295 json(conn, Stats.get_peers())
298 defp mastodonized_emoji do
299 Pleroma.Emoji.get_all()
300 |> Enum.map(fn {shortcode, relative_url, tags} ->
301 url = to_string(URI.merge(Web.base_url(), relative_url))
304 "shortcode" => shortcode,
306 "visible_in_picker" => true,
313 def custom_emojis(conn, _params) do
314 mastodon_emoji = mastodonized_emoji()
315 json(conn, mastodon_emoji)
318 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
321 |> Map.drop(["since_id", "max_id", "min_id"])
324 last = List.last(activities)
331 |> Map.get("limit", "20")
332 |> String.to_integer()
335 if length(activities) <= limit do
341 |> Enum.at(limit * -1)
345 {next_url, prev_url} =
349 Pleroma.Web.Endpoint,
352 Map.merge(params, %{max_id: max_id})
355 Pleroma.Web.Endpoint,
358 Map.merge(params, %{min_id: min_id})
364 Pleroma.Web.Endpoint,
366 Map.merge(params, %{max_id: max_id})
369 Pleroma.Web.Endpoint,
371 Map.merge(params, %{min_id: min_id})
377 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
383 def home_timeline(%{assigns: %{user: user}} = conn, params) do
386 |> Map.put("type", ["Create", "Announce"])
387 |> Map.put("blocking_user", user)
388 |> Map.put("muting_user", user)
389 |> Map.put("user", user)
392 [user.ap_id | user.following]
393 |> ActivityPub.fetch_activities(params)
397 |> add_link_headers(:home_timeline, activities)
398 |> put_view(StatusView)
399 |> render("index.json", %{activities: activities, for: user, as: :activity})
402 def public_timeline(%{assigns: %{user: user}} = conn, params) do
403 local_only = params["local"] in [true, "True", "true", "1"]
407 |> Map.put("type", ["Create", "Announce"])
408 |> Map.put("local_only", local_only)
409 |> Map.put("blocking_user", user)
410 |> Map.put("muting_user", user)
411 |> ActivityPub.fetch_public_activities()
415 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
416 |> put_view(StatusView)
417 |> render("index.json", %{activities: activities, for: user, as: :activity})
420 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
421 with %User{} = user <- User.get_cached_by_id(params["id"]) do
424 |> Map.put("tag", params["tagged"])
426 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
429 |> add_link_headers(:user_statuses, activities, params["id"])
430 |> put_view(StatusView)
431 |> render("index.json", %{
432 activities: activities,
439 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
442 |> Map.put("type", "Create")
443 |> Map.put("blocking_user", user)
444 |> Map.put("user", user)
445 |> Map.put(:visibility, "direct")
449 |> ActivityPub.fetch_activities_query(params)
450 |> Pagination.fetch_paginated(params)
453 |> add_link_headers(:dm_timeline, activities)
454 |> put_view(StatusView)
455 |> render("index.json", %{activities: activities, for: user, as: :activity})
458 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
459 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
460 true <- Visibility.visible_for_user?(activity, user) do
462 |> put_view(StatusView)
463 |> try_render("status.json", %{activity: activity, for: user})
467 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
468 with %Activity{} = activity <- Activity.get_by_id(id),
470 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
471 "blocking_user" => user,
475 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
477 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
478 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
484 activities: grouped_activities[true] || [],
488 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
493 activities: grouped_activities[false] || [],
497 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
504 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
505 with %Object{} = object <- Object.get_by_id(id),
506 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
507 true <- Visibility.visible_for_user?(activity, user) do
509 |> put_view(StatusView)
510 |> try_render("poll.json", %{object: object, for: user})
515 |> json(%{error: "Record not found"})
520 |> json(%{error: "Record not found"})
524 defp get_cached_vote_or_vote(user, object, choices) do
525 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
528 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
529 case CommonAPI.vote(user, object, choices) do
530 {:error, _message} = res -> {:ignore, res}
531 res -> {:commit, res}
538 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
539 with %Object{} = object <- Object.get_by_id(id),
540 true <- object.data["type"] == "Question",
541 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
542 true <- Visibility.visible_for_user?(activity, user),
543 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
545 |> put_view(StatusView)
546 |> try_render("poll.json", %{object: object, for: user})
551 |> json(%{error: "Record not found"})
556 |> json(%{error: "Record not found"})
561 |> json(%{error: message})
565 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
566 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
568 |> add_link_headers(:scheduled_statuses, scheduled_activities)
569 |> put_view(ScheduledActivityView)
570 |> render("index.json", %{scheduled_activities: scheduled_activities})
574 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
575 with %ScheduledActivity{} = scheduled_activity <-
576 ScheduledActivity.get(user, scheduled_activity_id) do
578 |> put_view(ScheduledActivityView)
579 |> render("show.json", %{scheduled_activity: scheduled_activity})
581 _ -> {:error, :not_found}
585 def update_scheduled_status(
586 %{assigns: %{user: user}} = conn,
587 %{"id" => scheduled_activity_id} = params
589 with %ScheduledActivity{} = scheduled_activity <-
590 ScheduledActivity.get(user, scheduled_activity_id),
591 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
593 |> put_view(ScheduledActivityView)
594 |> render("show.json", %{scheduled_activity: scheduled_activity})
596 nil -> {:error, :not_found}
601 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
602 with %ScheduledActivity{} = scheduled_activity <-
603 ScheduledActivity.get(user, scheduled_activity_id),
604 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
606 |> put_view(ScheduledActivityView)
607 |> render("show.json", %{scheduled_activity: scheduled_activity})
609 nil -> {:error, :not_found}
614 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
617 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
619 scheduled_at = params["scheduled_at"]
621 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
622 with {:ok, scheduled_activity} <-
623 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
625 |> put_view(ScheduledActivityView)
626 |> render("show.json", %{scheduled_activity: scheduled_activity})
629 params = Map.drop(params, ["scheduled_at"])
631 case CommonAPI.post(user, params) do
634 |> put_status(:unprocessable_entity)
635 |> json(%{error: message})
639 |> put_view(StatusView)
640 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
645 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
646 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
652 |> json(%{error: "Can't delete this post"})
656 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
657 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
658 %Activity{} = announce <- Activity.normalize(announce.data) do
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
665 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
666 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
667 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
675 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
676 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
678 |> put_view(StatusView)
679 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
683 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
684 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
685 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
687 |> put_view(StatusView)
688 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
692 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
693 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
695 |> put_view(StatusView)
696 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
700 |> put_resp_content_type("application/json")
701 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
705 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
706 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
708 |> put_view(StatusView)
709 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
713 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
714 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
715 %User{} = user <- User.get_cached_by_nickname(user.nickname),
716 true <- Visibility.visible_for_user?(activity, user),
717 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
719 |> put_view(StatusView)
720 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
724 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
725 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
726 %User{} = user <- User.get_cached_by_nickname(user.nickname),
727 true <- Visibility.visible_for_user?(activity, user),
728 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
730 |> put_view(StatusView)
731 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
735 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
736 activity = Activity.get_by_id(id)
738 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
740 |> put_view(StatusView)
741 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
745 |> put_resp_content_type("application/json")
746 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
750 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
751 activity = Activity.get_by_id(id)
753 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
755 |> put_view(StatusView)
756 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
760 def notifications(%{assigns: %{user: user}} = conn, params) do
761 notifications = MastodonAPI.get_notifications(user, params)
764 |> add_link_headers(:notifications, notifications)
765 |> put_view(NotificationView)
766 |> render("index.json", %{notifications: notifications, for: user})
769 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
770 with {:ok, notification} <- Notification.get(user, id) do
772 |> put_view(NotificationView)
773 |> render("show.json", %{notification: notification, for: user})
777 |> put_resp_content_type("application/json")
778 |> send_resp(403, Jason.encode!(%{"error" => reason}))
782 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
783 Notification.clear(user)
787 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
788 with {:ok, _notif} <- Notification.dismiss(user, id) do
793 |> put_resp_content_type("application/json")
794 |> send_resp(403, Jason.encode!(%{"error" => reason}))
798 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
799 Notification.destroy_multiple(user, ids)
803 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
805 q = from(u in User, where: u.id in ^id)
806 targets = Repo.all(q)
809 |> put_view(AccountView)
810 |> render("relationships.json", %{user: user, targets: targets})
813 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
814 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
816 def update_media(%{assigns: %{user: user}} = conn, data) do
817 with %Object{} = object <- Repo.get(Object, data["id"]),
818 true <- Object.authorize_mutation(object, user),
819 true <- is_binary(data["description"]),
820 description <- data["description"] do
821 new_data = %{object.data | "name" => description}
825 |> Object.change(%{data: new_data})
828 attachment_data = Map.put(new_data, "id", object.id)
831 |> put_view(StatusView)
832 |> render("attachment.json", %{attachment: attachment_data})
836 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
837 with {:ok, object} <-
840 actor: User.ap_id(user),
841 description: Map.get(data, "description")
843 attachment_data = Map.put(object.data, "id", object.id)
846 |> put_view(StatusView)
847 |> render("attachment.json", %{attachment: attachment_data})
851 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
852 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
853 %{} = attachment_data <- Map.put(object.data, "id", object.id),
854 %{type: type} = rendered <-
855 StatusView.render("attachment.json", %{attachment: attachment_data}) do
856 # Reject if not an image
857 if type == "image" do
859 # Save to the user's info
860 info_changeset = User.Info.mascot_update(user.info, rendered)
864 |> Ecto.Changeset.change()
865 |> Ecto.Changeset.put_embed(:info, info_changeset)
867 {:ok, _user} = User.update_and_set_cache(user_changeset)
873 |> put_resp_content_type("application/json")
874 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
879 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
880 mascot = User.get_mascot(user)
886 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
887 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
888 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
889 q = from(u in User, where: u.ap_id in ^likes)
893 |> put_view(AccountView)
894 |> render("accounts.json", %{for: user, users: users, as: :user})
900 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
901 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
902 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
903 q = from(u in User, where: u.ap_id in ^announces)
907 |> put_view(AccountView)
908 |> render("accounts.json", %{for: user, users: users, as: :user})
914 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
915 local_only = params["local"] in [true, "True", "true", "1"]
918 [params["tag"], params["any"]]
922 |> Enum.map(&String.downcase(&1))
927 |> Enum.map(&String.downcase(&1))
932 |> Enum.map(&String.downcase(&1))
936 |> Map.put("type", "Create")
937 |> Map.put("local_only", local_only)
938 |> Map.put("blocking_user", user)
939 |> Map.put("muting_user", user)
940 |> Map.put("tag", tags)
941 |> Map.put("tag_all", tag_all)
942 |> Map.put("tag_reject", tag_reject)
943 |> ActivityPub.fetch_public_activities()
947 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
948 |> put_view(StatusView)
949 |> render("index.json", %{activities: activities, for: user, as: :activity})
952 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
953 with %User{} = user <- User.get_cached_by_id(id),
954 followers <- MastodonAPI.get_followers(user, params) do
957 for_user && user.id == for_user.id -> followers
958 user.info.hide_followers -> []
963 |> add_link_headers(:followers, followers, user)
964 |> put_view(AccountView)
965 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
969 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
970 with %User{} = user <- User.get_cached_by_id(id),
971 followers <- MastodonAPI.get_friends(user, params) do
974 for_user && user.id == for_user.id -> followers
975 user.info.hide_follows -> []
980 |> add_link_headers(:following, followers, user)
981 |> put_view(AccountView)
982 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
986 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
987 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
989 |> put_view(AccountView)
990 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
994 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
995 with %User{} = follower <- User.get_cached_by_id(id),
996 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
998 |> put_view(AccountView)
999 |> render("relationship.json", %{user: followed, target: follower})
1001 {:error, message} ->
1003 |> put_resp_content_type("application/json")
1004 |> send_resp(403, Jason.encode!(%{"error" => message}))
1008 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1009 with %User{} = follower <- User.get_cached_by_id(id),
1010 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1012 |> put_view(AccountView)
1013 |> render("relationship.json", %{user: followed, target: follower})
1015 {:error, message} ->
1017 |> put_resp_content_type("application/json")
1018 |> send_resp(403, Jason.encode!(%{"error" => message}))
1022 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1023 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1024 {_, true} <- {:followed, follower.id != followed.id},
1025 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1027 |> put_view(AccountView)
1028 |> render("relationship.json", %{user: follower, target: followed})
1031 {:error, :not_found}
1033 {:error, message} ->
1035 |> put_resp_content_type("application/json")
1036 |> send_resp(403, Jason.encode!(%{"error" => message}))
1040 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1041 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1042 {_, true} <- {:followed, follower.id != followed.id},
1043 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1045 |> put_view(AccountView)
1046 |> render("account.json", %{user: followed, for: follower})
1049 {:error, :not_found}
1051 {:error, message} ->
1053 |> put_resp_content_type("application/json")
1054 |> send_resp(403, Jason.encode!(%{"error" => message}))
1058 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1059 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1060 {_, true} <- {:followed, follower.id != followed.id},
1061 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1063 |> put_view(AccountView)
1064 |> render("relationship.json", %{user: follower, target: followed})
1067 {:error, :not_found}
1074 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1075 with %User{} = muted <- User.get_cached_by_id(id),
1076 {:ok, muter} <- User.mute(muter, muted) do
1078 |> put_view(AccountView)
1079 |> render("relationship.json", %{user: muter, target: muted})
1081 {:error, message} ->
1083 |> put_resp_content_type("application/json")
1084 |> send_resp(403, Jason.encode!(%{"error" => message}))
1088 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1089 with %User{} = muted <- User.get_cached_by_id(id),
1090 {:ok, muter} <- User.unmute(muter, muted) do
1092 |> put_view(AccountView)
1093 |> render("relationship.json", %{user: muter, target: muted})
1095 {:error, message} ->
1097 |> put_resp_content_type("application/json")
1098 |> send_resp(403, Jason.encode!(%{"error" => message}))
1102 def mutes(%{assigns: %{user: user}} = conn, _) do
1103 with muted_accounts <- User.muted_users(user) do
1104 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1109 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1110 with %User{} = blocked <- User.get_cached_by_id(id),
1111 {:ok, blocker} <- User.block(blocker, blocked),
1112 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1114 |> put_view(AccountView)
1115 |> render("relationship.json", %{user: blocker, target: blocked})
1117 {:error, message} ->
1119 |> put_resp_content_type("application/json")
1120 |> send_resp(403, Jason.encode!(%{"error" => message}))
1124 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1125 with %User{} = blocked <- User.get_cached_by_id(id),
1126 {:ok, blocker} <- User.unblock(blocker, blocked),
1127 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1129 |> put_view(AccountView)
1130 |> render("relationship.json", %{user: blocker, target: blocked})
1132 {:error, message} ->
1134 |> put_resp_content_type("application/json")
1135 |> send_resp(403, Jason.encode!(%{"error" => message}))
1139 def blocks(%{assigns: %{user: user}} = conn, _) do
1140 with blocked_accounts <- User.blocked_users(user) do
1141 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1146 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1147 json(conn, info.domain_blocks || [])
1150 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1151 User.block_domain(blocker, domain)
1155 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1156 User.unblock_domain(blocker, domain)
1160 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1161 with %User{} = subscription_target <- User.get_cached_by_id(id),
1162 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1164 |> put_view(AccountView)
1165 |> render("relationship.json", %{user: user, target: subscription_target})
1167 {:error, message} ->
1169 |> put_resp_content_type("application/json")
1170 |> send_resp(403, Jason.encode!(%{"error" => message}))
1174 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1175 with %User{} = subscription_target <- User.get_cached_by_id(id),
1176 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1178 |> put_view(AccountView)
1179 |> render("relationship.json", %{user: user, target: subscription_target})
1181 {:error, message} ->
1183 |> put_resp_content_type("application/json")
1184 |> send_resp(403, Jason.encode!(%{"error" => message}))
1188 def favourites(%{assigns: %{user: user}} = conn, params) do
1191 |> Map.put("type", "Create")
1192 |> Map.put("favorited_by", user.ap_id)
1193 |> Map.put("blocking_user", user)
1196 ActivityPub.fetch_activities([], params)
1200 |> add_link_headers(:favourites, activities)
1201 |> put_view(StatusView)
1202 |> render("index.json", %{activities: activities, for: user, as: :activity})
1205 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1206 with %User{} = user <- User.get_by_id(id),
1207 false <- user.info.hide_favorites do
1210 |> Map.put("type", "Create")
1211 |> Map.put("favorited_by", user.ap_id)
1212 |> Map.put("blocking_user", for_user)
1216 ["https://www.w3.org/ns/activitystreams#Public"] ++
1217 [for_user.ap_id | for_user.following]
1219 ["https://www.w3.org/ns/activitystreams#Public"]
1224 |> ActivityPub.fetch_activities(params)
1228 |> add_link_headers(:favourites, activities)
1229 |> put_view(StatusView)
1230 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1233 {:error, :not_found}
1238 |> json(%{error: "Can't get favorites"})
1242 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1243 user = User.get_cached_by_id(user.id)
1246 Bookmark.for_user_query(user.id)
1247 |> Pagination.fetch_paginated(params)
1251 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1254 |> add_link_headers(:bookmarks, bookmarks)
1255 |> put_view(StatusView)
1256 |> render("index.json", %{activities: activities, for: user, as: :activity})
1259 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1260 lists = Pleroma.List.for_user(user, opts)
1261 res = ListView.render("lists.json", lists: lists)
1265 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1266 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1267 res = ListView.render("list.json", list: list)
1273 |> json(%{error: "Record not found"})
1277 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1278 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1279 res = ListView.render("lists.json", lists: lists)
1283 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1284 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1285 {:ok, _list} <- Pleroma.List.delete(list) do
1293 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1294 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1295 res = ListView.render("list.json", list: list)
1300 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1302 |> Enum.each(fn account_id ->
1303 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1304 %User{} = followed <- User.get_cached_by_id(account_id) do
1305 Pleroma.List.follow(list, followed)
1312 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1314 |> Enum.each(fn account_id ->
1315 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1316 %User{} = followed <- User.get_cached_by_id(account_id) do
1317 Pleroma.List.unfollow(list, followed)
1324 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1325 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1326 {:ok, users} = Pleroma.List.get_following(list) do
1328 |> put_view(AccountView)
1329 |> render("accounts.json", %{for: user, users: users, as: :user})
1333 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1334 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1335 {:ok, list} <- Pleroma.List.rename(list, title) do
1336 res = ListView.render("list.json", list: list)
1344 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1345 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1348 |> Map.put("type", "Create")
1349 |> Map.put("blocking_user", user)
1350 |> Map.put("muting_user", user)
1352 # we must filter the following list for the user to avoid leaking statuses the user
1353 # does not actually have permission to see (for more info, peruse security issue #270).
1356 |> Enum.filter(fn x -> x in user.following end)
1357 |> ActivityPub.fetch_activities_bounded(following, params)
1361 |> put_view(StatusView)
1362 |> render("index.json", %{activities: activities, for: user, as: :activity})
1367 |> json(%{error: "Error."})
1371 def index(%{assigns: %{user: user}} = conn, _params) do
1372 token = get_session(conn, :oauth_token)
1375 mastodon_emoji = mastodonized_emoji()
1377 limit = Config.get([:instance, :limit])
1380 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1385 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1386 access_token: token,
1388 domain: Pleroma.Web.Endpoint.host(),
1391 unfollow_modal: false,
1394 auto_play_gif: false,
1395 display_sensitive_media: false,
1396 reduce_motion: false,
1397 max_toot_chars: limit,
1398 mascot: User.get_mascot(user)["url"]
1400 poll_limits: Config.get([:instance, :poll_limits]),
1402 delete_others_notice: present?(user.info.is_moderator),
1403 admin: present?(user.info.is_admin)
1407 default_privacy: user.info.default_scope,
1408 default_sensitive: false,
1409 allow_content_types: Config.get([:instance, :allowed_post_formats])
1411 media_attachments: %{
1412 accept_content_types: [
1428 user.info.settings ||
1458 push_subscription: nil,
1460 custom_emojis: mastodon_emoji,
1466 |> put_layout(false)
1467 |> put_view(MastodonView)
1468 |> render("index.html", %{initial_state: initial_state})
1471 |> put_session(:return_to, conn.request_path)
1472 |> redirect(to: "/web/login")
1476 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1477 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1479 with changeset <- Ecto.Changeset.change(user),
1480 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1481 {:ok, _user} <- User.update_and_set_cache(changeset) do
1486 |> put_resp_content_type("application/json")
1487 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1491 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1492 redirect(conn, to: local_mastodon_root_path(conn))
1495 @doc "Local Mastodon FE login init action"
1496 def login(conn, %{"code" => auth_token}) do
1497 with {:ok, app} <- get_or_make_app(),
1498 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1499 {:ok, token} <- Token.exchange_token(app, auth) do
1501 |> put_session(:oauth_token, token.token)
1502 |> redirect(to: local_mastodon_root_path(conn))
1506 @doc "Local Mastodon FE callback action"
1507 def login(conn, _) do
1508 with {:ok, app} <- get_or_make_app() do
1513 response_type: "code",
1514 client_id: app.client_id,
1516 scope: Enum.join(app.scopes, " ")
1519 redirect(conn, to: path)
1523 defp local_mastodon_root_path(conn) do
1524 case get_session(conn, :return_to) do
1526 mastodon_api_path(conn, :index, ["getting-started"])
1529 delete_session(conn, :return_to)
1534 defp get_or_make_app do
1535 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1536 scopes = ["read", "write", "follow", "push"]
1538 with %App{} = app <- Repo.get_by(App, find_attrs) do
1540 if app.scopes == scopes do
1544 |> Ecto.Changeset.change(%{scopes: scopes})
1552 App.register_changeset(
1554 Map.put(find_attrs, :scopes, scopes)
1561 def logout(conn, _) do
1564 |> redirect(to: "/")
1567 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1568 Logger.debug("Unimplemented, returning unmodified relationship")
1570 with %User{} = target <- User.get_cached_by_id(id) do
1572 |> put_view(AccountView)
1573 |> render("relationship.json", %{user: user, target: target})
1577 def empty_array(conn, _) do
1578 Logger.debug("Unimplemented, returning an empty array")
1582 def empty_object(conn, _) do
1583 Logger.debug("Unimplemented, returning an empty object")
1587 def get_filters(%{assigns: %{user: user}} = conn, _) do
1588 filters = Filter.get_filters(user)
1589 res = FilterView.render("filters.json", filters: filters)
1594 %{assigns: %{user: user}} = conn,
1595 %{"phrase" => phrase, "context" => context} = params
1601 hide: Map.get(params, "irreversible", false),
1602 whole_word: Map.get(params, "boolean", true)
1606 {:ok, response} = Filter.create(query)
1607 res = FilterView.render("filter.json", filter: response)
1611 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1612 filter = Filter.get(filter_id, user)
1613 res = FilterView.render("filter.json", filter: filter)
1618 %{assigns: %{user: user}} = conn,
1619 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1623 filter_id: filter_id,
1626 hide: Map.get(params, "irreversible", nil),
1627 whole_word: Map.get(params, "boolean", true)
1631 {:ok, response} = Filter.update(query)
1632 res = FilterView.render("filter.json", filter: response)
1636 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1639 filter_id: filter_id
1642 {:ok, _} = Filter.delete(query)
1648 def errors(conn, {:error, %Changeset{} = changeset}) do
1651 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1652 |> Enum.map_join(", ", fn {_k, v} -> v end)
1656 |> json(%{error: error_message})
1659 def errors(conn, {:error, :not_found}) do
1662 |> json(%{error: "Record not found"})
1665 def errors(conn, _) do
1668 |> json("Something went wrong")
1671 def suggestions(%{assigns: %{user: user}} = conn, _) do
1672 suggestions = Config.get(:suggestions)
1674 if Keyword.get(suggestions, :enabled, false) do
1675 api = Keyword.get(suggestions, :third_party_engine, "")
1676 timeout = Keyword.get(suggestions, :timeout, 5000)
1677 limit = Keyword.get(suggestions, :limit, 23)
1679 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1681 user = user.nickname
1685 |> String.replace("{{host}}", host)
1686 |> String.replace("{{user}}", user)
1688 with {:ok, %{status: 200, body: body}} <-
1693 recv_timeout: timeout,
1697 {:ok, data} <- Jason.decode(body) do
1700 |> Enum.slice(0, limit)
1705 case User.get_or_fetch(x["acct"]) do
1706 {:ok, %User{id: id}} -> id
1712 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1715 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1721 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1728 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1729 with %Activity{} = activity <- Activity.get_by_id(status_id),
1730 true <- Visibility.visible_for_user?(activity, user) do
1734 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1744 def reports(%{assigns: %{user: user}} = conn, params) do
1745 case CommonAPI.report(user, params) do
1748 |> put_view(ReportView)
1749 |> try_render("report.json", %{activity: activity})
1753 |> put_status(:bad_request)
1754 |> json(%{error: err})
1758 def account_register(
1759 %{assigns: %{app: app}} = conn,
1760 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1768 "captcha_answer_data",
1772 |> Map.put("nickname", nickname)
1773 |> Map.put("fullname", params["fullname"] || nickname)
1774 |> Map.put("bio", params["bio"] || "")
1775 |> Map.put("confirm", params["password"])
1777 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1778 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1780 token_type: "Bearer",
1781 access_token: token.token,
1783 created_at: Token.Utils.format_created_at(token)
1789 |> json(Jason.encode!(errors))
1793 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1796 |> json(%{error: "Missing parameters"})
1799 def account_register(conn, _) do
1802 |> json(%{error: "Invalid credentials"})
1805 def conversations(%{assigns: %{user: user}} = conn, params) do
1806 participations = Participation.for_user_with_last_activity_id(user, params)
1809 Enum.map(participations, fn participation ->
1810 ConversationView.render("participation.json", %{participation: participation, user: user})
1814 |> add_link_headers(:conversations, participations)
1815 |> json(conversations)
1818 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1819 with %Participation{} = participation <-
1820 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1821 {:ok, participation} <- Participation.mark_as_read(participation) do
1822 participation_view =
1823 ConversationView.render("participation.json", %{participation: participation, user: user})
1826 |> json(participation_view)
1830 def try_render(conn, target, params)
1831 when is_binary(target) do
1832 res = render(conn, target, params)
1837 |> json(%{error: "Can't display this activity"})
1843 def try_render(conn, _, _) do
1846 |> json(%{error: "Can't display this activity"})
1849 defp present?(nil), do: false
1850 defp present?(false), do: false
1851 defp present?(_), do: true