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})
163 _e -> render_error(conn, :forbidden, "Invalid request")
167 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
168 change = Changeset.change(user, %{avatar: nil})
169 {:ok, user} = User.update_and_set_cache(change)
170 CommonAPI.update(user)
172 json(conn, %{url: nil})
175 def update_avatar(%{assigns: %{user: user}} = conn, params) do
176 {:ok, object} = ActivityPub.upload(params, type: :avatar)
177 change = Changeset.change(user, %{avatar: object.data})
178 {:ok, user} = User.update_and_set_cache(change)
179 CommonAPI.update(user)
180 %{"url" => [%{"href" => href} | _]} = object.data
182 json(conn, %{url: href})
185 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
186 with new_info <- %{"banner" => %{}},
187 info_cng <- User.Info.profile_update(user.info, new_info),
188 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
189 {:ok, user} <- User.update_and_set_cache(changeset) do
190 CommonAPI.update(user)
192 json(conn, %{url: nil})
196 def update_banner(%{assigns: %{user: user}} = conn, params) do
197 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
198 new_info <- %{"banner" => object.data},
199 info_cng <- User.Info.profile_update(user.info, new_info),
200 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
201 {:ok, user} <- User.update_and_set_cache(changeset) do
202 CommonAPI.update(user)
203 %{"url" => [%{"href" => href} | _]} = object.data
205 json(conn, %{url: href})
209 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
210 with new_info <- %{"background" => %{}},
211 info_cng <- User.Info.profile_update(user.info, new_info),
212 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
213 {:ok, _user} <- User.update_and_set_cache(changeset) do
214 json(conn, %{url: nil})
218 def update_background(%{assigns: %{user: user}} = conn, params) do
219 with {:ok, object} <- ActivityPub.upload(params, type: :background),
220 new_info <- %{"background" => object.data},
221 info_cng <- User.Info.profile_update(user.info, new_info),
222 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
223 {:ok, _user} <- User.update_and_set_cache(changeset) do
224 %{"url" => [%{"href" => href} | _]} = object.data
226 json(conn, %{url: href})
230 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
231 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
234 AccountView.render("account.json", %{
237 with_pleroma_settings: true,
238 with_chat_token: chat_token
244 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
245 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
248 |> render("short.json", %{app: app})
252 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
253 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
254 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
255 account = AccountView.render("account.json", %{user: user, for: for_user})
258 _e -> render_error(conn, :not_found, "Can't find user")
262 @mastodon_api_level "2.7.2"
264 def masto_instance(conn, _params) do
265 instance = Config.get(:instance)
269 title: Keyword.get(instance, :name),
270 description: Keyword.get(instance, :description),
271 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
272 email: Keyword.get(instance, :email),
274 streaming_api: Pleroma.Web.Endpoint.websocket_url()
276 stats: Stats.get_stats(),
277 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
279 registrations: Pleroma.Config.get([:instance, :registrations_open]),
280 # Extra (not present in Mastodon):
281 max_toot_chars: Keyword.get(instance, :limit),
282 poll_limits: Keyword.get(instance, :poll_limits)
288 def peers(conn, _params) do
289 json(conn, Stats.get_peers())
292 defp mastodonized_emoji do
293 Pleroma.Emoji.get_all()
294 |> Enum.map(fn {shortcode, relative_url, tags} ->
295 url = to_string(URI.merge(Web.base_url(), relative_url))
298 "shortcode" => shortcode,
300 "visible_in_picker" => true,
307 def custom_emojis(conn, _params) do
308 mastodon_emoji = mastodonized_emoji()
309 json(conn, mastodon_emoji)
312 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
315 |> Map.drop(["since_id", "max_id", "min_id"])
318 last = List.last(activities)
325 |> Map.get("limit", "20")
326 |> String.to_integer()
329 if length(activities) <= limit do
335 |> Enum.at(limit * -1)
339 {next_url, prev_url} =
343 Pleroma.Web.Endpoint,
346 Map.merge(params, %{max_id: max_id})
349 Pleroma.Web.Endpoint,
352 Map.merge(params, %{min_id: min_id})
358 Pleroma.Web.Endpoint,
360 Map.merge(params, %{max_id: max_id})
363 Pleroma.Web.Endpoint,
365 Map.merge(params, %{min_id: min_id})
371 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
377 def home_timeline(%{assigns: %{user: user}} = conn, params) do
380 |> Map.put("type", ["Create", "Announce"])
381 |> Map.put("blocking_user", user)
382 |> Map.put("muting_user", user)
383 |> Map.put("user", user)
386 [user.ap_id | user.following]
387 |> ActivityPub.fetch_activities(params)
391 |> add_link_headers(:home_timeline, activities)
392 |> put_view(StatusView)
393 |> render("index.json", %{activities: activities, for: user, as: :activity})
396 def public_timeline(%{assigns: %{user: user}} = conn, params) do
397 local_only = params["local"] in [true, "True", "true", "1"]
401 |> Map.put("type", ["Create", "Announce"])
402 |> Map.put("local_only", local_only)
403 |> Map.put("blocking_user", user)
404 |> Map.put("muting_user", user)
405 |> ActivityPub.fetch_public_activities()
409 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
410 |> put_view(StatusView)
411 |> render("index.json", %{activities: activities, for: user, as: :activity})
414 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
415 with %User{} = user <- User.get_cached_by_id(params["id"]) do
418 |> Map.put("tag", params["tagged"])
420 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
423 |> add_link_headers(:user_statuses, activities, params["id"])
424 |> put_view(StatusView)
425 |> render("index.json", %{
426 activities: activities,
433 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
436 |> Map.put("type", "Create")
437 |> Map.put("blocking_user", user)
438 |> Map.put("user", user)
439 |> Map.put(:visibility, "direct")
443 |> ActivityPub.fetch_activities_query(params)
444 |> Pagination.fetch_paginated(params)
447 |> add_link_headers(:dm_timeline, activities)
448 |> put_view(StatusView)
449 |> render("index.json", %{activities: activities, for: user, as: :activity})
452 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
453 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
454 true <- Visibility.visible_for_user?(activity, user) do
456 |> put_view(StatusView)
457 |> try_render("status.json", %{activity: activity, for: user})
461 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
462 with %Activity{} = activity <- Activity.get_by_id(id),
464 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
465 "blocking_user" => user,
469 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
471 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
472 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
478 activities: grouped_activities[true] || [],
482 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
487 activities: grouped_activities[false] || [],
491 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
498 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
499 with %Object{} = object <- Object.get_by_id(id),
500 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
501 true <- Visibility.visible_for_user?(activity, user) do
503 |> put_view(StatusView)
504 |> try_render("poll.json", %{object: object, for: user})
506 nil -> render_error(conn, :not_found, "Record not found")
507 false -> render_error(conn, :not_found, "Record not found")
511 defp get_cached_vote_or_vote(user, object, choices) do
512 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
515 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
516 case CommonAPI.vote(user, object, choices) do
517 {:error, _message} = res -> {:ignore, res}
518 res -> {:commit, res}
525 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
526 with %Object{} = object <- Object.get_by_id(id),
527 true <- object.data["type"] == "Question",
528 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
529 true <- Visibility.visible_for_user?(activity, user),
530 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
532 |> put_view(StatusView)
533 |> try_render("poll.json", %{object: object, for: user})
536 render_error(conn, :not_found, "Record not found")
539 render_error(conn, :not_found, "Record not found")
543 |> put_status(:unprocessable_entity)
544 |> json(%{error: message})
548 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
549 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
551 |> add_link_headers(:scheduled_statuses, scheduled_activities)
552 |> put_view(ScheduledActivityView)
553 |> render("index.json", %{scheduled_activities: scheduled_activities})
557 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
558 with %ScheduledActivity{} = scheduled_activity <-
559 ScheduledActivity.get(user, scheduled_activity_id) do
561 |> put_view(ScheduledActivityView)
562 |> render("show.json", %{scheduled_activity: scheduled_activity})
564 _ -> {:error, :not_found}
568 def update_scheduled_status(
569 %{assigns: %{user: user}} = conn,
570 %{"id" => scheduled_activity_id} = params
572 with %ScheduledActivity{} = scheduled_activity <-
573 ScheduledActivity.get(user, scheduled_activity_id),
574 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
576 |> put_view(ScheduledActivityView)
577 |> render("show.json", %{scheduled_activity: scheduled_activity})
579 nil -> {:error, :not_found}
584 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
585 with %ScheduledActivity{} = scheduled_activity <-
586 ScheduledActivity.get(user, scheduled_activity_id),
587 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
589 |> put_view(ScheduledActivityView)
590 |> render("show.json", %{scheduled_activity: scheduled_activity})
592 nil -> {:error, :not_found}
597 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
600 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
602 scheduled_at = params["scheduled_at"]
604 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
605 with {:ok, scheduled_activity} <-
606 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
608 |> put_view(ScheduledActivityView)
609 |> render("show.json", %{scheduled_activity: scheduled_activity})
612 params = Map.drop(params, ["scheduled_at"])
614 case CommonAPI.post(user, params) do
617 |> put_status(:unprocessable_entity)
618 |> json(%{error: message})
622 |> put_view(StatusView)
623 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
628 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
629 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
632 _e -> render_error(conn, :forbidden, "Can't delete this post")
636 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
637 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
638 %Activity{} = announce <- Activity.normalize(announce.data) do
640 |> put_view(StatusView)
641 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
645 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
646 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
647 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
649 |> put_view(StatusView)
650 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
654 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
655 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
656 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
658 |> put_view(StatusView)
659 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
663 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
664 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
665 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
667 |> put_view(StatusView)
668 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
672 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
673 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
675 |> put_view(StatusView)
676 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
680 |> put_status(:bad_request)
681 |> json(%{"error" => reason})
685 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
686 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
688 |> put_view(StatusView)
689 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
693 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
694 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
695 %User{} = user <- User.get_cached_by_nickname(user.nickname),
696 true <- Visibility.visible_for_user?(activity, user),
697 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
699 |> put_view(StatusView)
700 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
704 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
705 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
706 %User{} = user <- User.get_cached_by_nickname(user.nickname),
707 true <- Visibility.visible_for_user?(activity, user),
708 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
710 |> put_view(StatusView)
711 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
715 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
716 activity = Activity.get_by_id(id)
718 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
720 |> put_view(StatusView)
721 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
725 |> put_resp_content_type("application/json")
726 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
730 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
731 activity = Activity.get_by_id(id)
733 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
735 |> put_view(StatusView)
736 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
740 def notifications(%{assigns: %{user: user}} = conn, params) do
741 notifications = MastodonAPI.get_notifications(user, params)
744 |> add_link_headers(:notifications, notifications)
745 |> put_view(NotificationView)
746 |> render("index.json", %{notifications: notifications, for: user})
749 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
750 with {:ok, notification} <- Notification.get(user, id) do
752 |> put_view(NotificationView)
753 |> render("show.json", %{notification: notification, for: user})
757 |> put_status(:forbidden)
758 |> json(%{"error" => reason})
762 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
763 Notification.clear(user)
767 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
768 with {:ok, _notif} <- Notification.dismiss(user, id) do
773 |> put_status(:forbidden)
774 |> json(%{"error" => reason})
778 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
779 Notification.destroy_multiple(user, ids)
783 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
785 q = from(u in User, where: u.id in ^id)
786 targets = Repo.all(q)
789 |> put_view(AccountView)
790 |> render("relationships.json", %{user: user, targets: targets})
793 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
794 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
796 def update_media(%{assigns: %{user: user}} = conn, data) do
797 with %Object{} = object <- Repo.get(Object, data["id"]),
798 true <- Object.authorize_mutation(object, user),
799 true <- is_binary(data["description"]),
800 description <- data["description"] do
801 new_data = %{object.data | "name" => description}
805 |> Object.change(%{data: new_data})
808 attachment_data = Map.put(new_data, "id", object.id)
811 |> put_view(StatusView)
812 |> render("attachment.json", %{attachment: attachment_data})
816 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
817 with {:ok, object} <-
820 actor: User.ap_id(user),
821 description: Map.get(data, "description")
823 attachment_data = Map.put(object.data, "id", object.id)
826 |> put_view(StatusView)
827 |> render("attachment.json", %{attachment: attachment_data})
831 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
832 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
833 %{} = attachment_data <- Map.put(object.data, "id", object.id),
834 %{type: type} = rendered <-
835 StatusView.render("attachment.json", %{attachment: attachment_data}) do
836 # Reject if not an image
837 if type == "image" do
839 # Save to the user's info
840 info_changeset = User.Info.mascot_update(user.info, rendered)
844 |> Ecto.Changeset.change()
845 |> Ecto.Changeset.put_embed(:info, info_changeset)
847 {:ok, _user} = User.update_and_set_cache(user_changeset)
852 render_error(conn, :unsupported_media_type, "mascots can only be images")
857 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
858 mascot = User.get_mascot(user)
864 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
865 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
866 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
867 q = from(u in User, where: u.ap_id in ^likes)
871 |> put_view(AccountView)
872 |> render("accounts.json", %{for: user, users: users, as: :user})
878 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
879 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
880 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
881 q = from(u in User, where: u.ap_id in ^announces)
885 |> put_view(AccountView)
886 |> render("accounts.json", %{for: user, users: users, as: :user})
892 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
893 local_only = params["local"] in [true, "True", "true", "1"]
896 [params["tag"], params["any"]]
900 |> Enum.map(&String.downcase(&1))
905 |> Enum.map(&String.downcase(&1))
910 |> Enum.map(&String.downcase(&1))
914 |> Map.put("type", "Create")
915 |> Map.put("local_only", local_only)
916 |> Map.put("blocking_user", user)
917 |> Map.put("muting_user", user)
918 |> Map.put("tag", tags)
919 |> Map.put("tag_all", tag_all)
920 |> Map.put("tag_reject", tag_reject)
921 |> ActivityPub.fetch_public_activities()
925 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
926 |> put_view(StatusView)
927 |> render("index.json", %{activities: activities, for: user, as: :activity})
930 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
931 with %User{} = user <- User.get_cached_by_id(id),
932 followers <- MastodonAPI.get_followers(user, params) do
935 for_user && user.id == for_user.id -> followers
936 user.info.hide_followers -> []
941 |> add_link_headers(:followers, followers, user)
942 |> put_view(AccountView)
943 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
947 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
948 with %User{} = user <- User.get_cached_by_id(id),
949 followers <- MastodonAPI.get_friends(user, params) do
952 for_user && user.id == for_user.id -> followers
953 user.info.hide_follows -> []
958 |> add_link_headers(:following, followers, user)
959 |> put_view(AccountView)
960 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
964 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
965 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
967 |> put_view(AccountView)
968 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
972 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
973 with %User{} = follower <- User.get_cached_by_id(id),
974 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: followed, target: follower})
981 |> put_status(:forbidden)
982 |> json(%{error: message})
986 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
987 with %User{} = follower <- User.get_cached_by_id(id),
988 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
990 |> put_view(AccountView)
991 |> render("relationship.json", %{user: followed, target: follower})
995 |> put_status(:forbidden)
996 |> json(%{error: message})
1000 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1001 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1002 {_, true} <- {:followed, follower.id != followed.id},
1003 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1005 |> put_view(AccountView)
1006 |> render("relationship.json", %{user: follower, target: followed})
1009 {:error, :not_found}
1011 {:error, message} ->
1013 |> put_status(:forbidden)
1014 |> json(%{error: message})
1018 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1019 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1020 {_, true} <- {:followed, follower.id != followed.id},
1021 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1023 |> put_view(AccountView)
1024 |> render("account.json", %{user: followed, for: follower})
1027 {:error, :not_found}
1029 {:error, message} ->
1031 |> put_status(:forbidden)
1032 |> json(%{error: message})
1036 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1037 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1038 {_, true} <- {:followed, follower.id != followed.id},
1039 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1041 |> put_view(AccountView)
1042 |> render("relationship.json", %{user: follower, target: followed})
1045 {:error, :not_found}
1052 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1053 with %User{} = muted <- User.get_cached_by_id(id),
1054 {:ok, muter} <- User.mute(muter, muted) do
1056 |> put_view(AccountView)
1057 |> render("relationship.json", %{user: muter, target: muted})
1059 {:error, message} ->
1061 |> put_status(:forbidden)
1062 |> json(%{error: message})
1066 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1067 with %User{} = muted <- User.get_cached_by_id(id),
1068 {:ok, muter} <- User.unmute(muter, muted) do
1070 |> put_view(AccountView)
1071 |> render("relationship.json", %{user: muter, target: muted})
1073 {:error, message} ->
1075 |> put_status(:forbidden)
1076 |> json(%{error: message})
1080 def mutes(%{assigns: %{user: user}} = conn, _) do
1081 with muted_accounts <- User.muted_users(user) do
1082 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1087 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1088 with %User{} = blocked <- User.get_cached_by_id(id),
1089 {:ok, blocker} <- User.block(blocker, blocked),
1090 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1092 |> put_view(AccountView)
1093 |> render("relationship.json", %{user: blocker, target: blocked})
1095 {:error, message} ->
1097 |> put_status(:forbidden)
1098 |> json(%{error: message})
1102 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1103 with %User{} = blocked <- User.get_cached_by_id(id),
1104 {:ok, blocker} <- User.unblock(blocker, blocked),
1105 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1107 |> put_view(AccountView)
1108 |> render("relationship.json", %{user: blocker, target: blocked})
1110 {:error, message} ->
1112 |> put_status(:forbidden)
1113 |> json(%{error: message})
1117 def blocks(%{assigns: %{user: user}} = conn, _) do
1118 with blocked_accounts <- User.blocked_users(user) do
1119 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1124 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1125 json(conn, info.domain_blocks || [])
1128 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1129 User.block_domain(blocker, domain)
1133 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1134 User.unblock_domain(blocker, domain)
1138 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1139 with %User{} = subscription_target <- User.get_cached_by_id(id),
1140 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1142 |> put_view(AccountView)
1143 |> render("relationship.json", %{user: user, target: subscription_target})
1145 {:error, message} ->
1147 |> put_status(:forbidden)
1148 |> json(%{error: message})
1152 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1153 with %User{} = subscription_target <- User.get_cached_by_id(id),
1154 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1156 |> put_view(AccountView)
1157 |> render("relationship.json", %{user: user, target: subscription_target})
1159 {:error, message} ->
1161 |> put_status(:forbidden)
1162 |> json(%{error: message})
1166 def favourites(%{assigns: %{user: user}} = conn, params) do
1169 |> Map.put("type", "Create")
1170 |> Map.put("favorited_by", user.ap_id)
1171 |> Map.put("blocking_user", user)
1174 ActivityPub.fetch_activities([], params)
1178 |> add_link_headers(:favourites, activities)
1179 |> put_view(StatusView)
1180 |> render("index.json", %{activities: activities, for: user, as: :activity})
1183 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1184 with %User{} = user <- User.get_by_id(id),
1185 false <- user.info.hide_favorites do
1188 |> Map.put("type", "Create")
1189 |> Map.put("favorited_by", user.ap_id)
1190 |> Map.put("blocking_user", for_user)
1194 ["https://www.w3.org/ns/activitystreams#Public"] ++
1195 [for_user.ap_id | for_user.following]
1197 ["https://www.w3.org/ns/activitystreams#Public"]
1202 |> ActivityPub.fetch_activities(params)
1206 |> add_link_headers(:favourites, activities)
1207 |> put_view(StatusView)
1208 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1210 nil -> {:error, :not_found}
1211 true -> render_error(conn, :forbidden, "Can't get favorites")
1215 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1216 user = User.get_cached_by_id(user.id)
1219 Bookmark.for_user_query(user.id)
1220 |> Pagination.fetch_paginated(params)
1224 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1227 |> add_link_headers(:bookmarks, bookmarks)
1228 |> put_view(StatusView)
1229 |> render("index.json", %{activities: activities, for: user, as: :activity})
1232 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1233 lists = Pleroma.List.for_user(user, opts)
1234 res = ListView.render("lists.json", lists: lists)
1238 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1239 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1240 res = ListView.render("list.json", list: list)
1243 _e -> render_error(conn, :not_found, "Record not found")
1247 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1248 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1249 res = ListView.render("lists.json", lists: lists)
1253 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1254 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1255 {:ok, _list} <- Pleroma.List.delete(list) do
1259 json(conn, dgettext("errors", "error"))
1263 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1264 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1265 res = ListView.render("list.json", list: list)
1270 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1272 |> Enum.each(fn account_id ->
1273 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1274 %User{} = followed <- User.get_cached_by_id(account_id) do
1275 Pleroma.List.follow(list, followed)
1282 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1284 |> Enum.each(fn account_id ->
1285 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1286 %User{} = followed <- User.get_cached_by_id(account_id) do
1287 Pleroma.List.unfollow(list, followed)
1294 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1295 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1296 {:ok, users} = Pleroma.List.get_following(list) do
1298 |> put_view(AccountView)
1299 |> render("accounts.json", %{for: user, users: users, as: :user})
1303 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1304 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1305 {:ok, list} <- Pleroma.List.rename(list, title) do
1306 res = ListView.render("list.json", list: list)
1310 json(conn, dgettext("errors", "error"))
1314 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1315 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1318 |> Map.put("type", "Create")
1319 |> Map.put("blocking_user", user)
1320 |> Map.put("muting_user", user)
1322 # we must filter the following list for the user to avoid leaking statuses the user
1323 # does not actually have permission to see (for more info, peruse security issue #270).
1326 |> Enum.filter(fn x -> x in user.following end)
1327 |> ActivityPub.fetch_activities_bounded(following, params)
1331 |> put_view(StatusView)
1332 |> render("index.json", %{activities: activities, for: user, as: :activity})
1334 _e -> render_error(conn, :forbidden, "Error.")
1338 def index(%{assigns: %{user: user}} = conn, _params) do
1339 token = get_session(conn, :oauth_token)
1342 mastodon_emoji = mastodonized_emoji()
1344 limit = Config.get([:instance, :limit])
1347 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1352 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1353 access_token: token,
1355 domain: Pleroma.Web.Endpoint.host(),
1358 unfollow_modal: false,
1361 auto_play_gif: false,
1362 display_sensitive_media: false,
1363 reduce_motion: false,
1364 max_toot_chars: limit,
1365 mascot: User.get_mascot(user)["url"]
1367 poll_limits: Config.get([:instance, :poll_limits]),
1369 delete_others_notice: present?(user.info.is_moderator),
1370 admin: present?(user.info.is_admin)
1374 default_privacy: user.info.default_scope,
1375 default_sensitive: false,
1376 allow_content_types: Config.get([:instance, :allowed_post_formats])
1378 media_attachments: %{
1379 accept_content_types: [
1395 user.info.settings ||
1425 push_subscription: nil,
1427 custom_emojis: mastodon_emoji,
1433 |> put_layout(false)
1434 |> put_view(MastodonView)
1435 |> render("index.html", %{initial_state: initial_state})
1438 |> put_session(:return_to, conn.request_path)
1439 |> redirect(to: "/web/login")
1443 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1444 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1446 with changeset <- Ecto.Changeset.change(user),
1447 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1448 {:ok, _user} <- User.update_and_set_cache(changeset) do
1453 |> put_status(:internal_server_error)
1454 |> json(%{error: inspect(e)})
1458 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1459 redirect(conn, to: local_mastodon_root_path(conn))
1462 @doc "Local Mastodon FE login init action"
1463 def login(conn, %{"code" => auth_token}) do
1464 with {:ok, app} <- get_or_make_app(),
1465 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1466 {:ok, token} <- Token.exchange_token(app, auth) do
1468 |> put_session(:oauth_token, token.token)
1469 |> redirect(to: local_mastodon_root_path(conn))
1473 @doc "Local Mastodon FE callback action"
1474 def login(conn, _) do
1475 with {:ok, app} <- get_or_make_app() do
1480 response_type: "code",
1481 client_id: app.client_id,
1483 scope: Enum.join(app.scopes, " ")
1486 redirect(conn, to: path)
1490 defp local_mastodon_root_path(conn) do
1491 case get_session(conn, :return_to) do
1493 mastodon_api_path(conn, :index, ["getting-started"])
1496 delete_session(conn, :return_to)
1501 defp get_or_make_app do
1502 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1503 scopes = ["read", "write", "follow", "push"]
1505 with %App{} = app <- Repo.get_by(App, find_attrs) do
1507 if app.scopes == scopes do
1511 |> Ecto.Changeset.change(%{scopes: scopes})
1519 App.register_changeset(
1521 Map.put(find_attrs, :scopes, scopes)
1528 def logout(conn, _) do
1531 |> redirect(to: "/")
1534 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1535 Logger.debug("Unimplemented, returning unmodified relationship")
1537 with %User{} = target <- User.get_cached_by_id(id) do
1539 |> put_view(AccountView)
1540 |> render("relationship.json", %{user: user, target: target})
1544 def empty_array(conn, _) do
1545 Logger.debug("Unimplemented, returning an empty array")
1549 def empty_object(conn, _) do
1550 Logger.debug("Unimplemented, returning an empty object")
1554 def get_filters(%{assigns: %{user: user}} = conn, _) do
1555 filters = Filter.get_filters(user)
1556 res = FilterView.render("filters.json", filters: filters)
1561 %{assigns: %{user: user}} = conn,
1562 %{"phrase" => phrase, "context" => context} = params
1568 hide: Map.get(params, "irreversible", false),
1569 whole_word: Map.get(params, "boolean", true)
1573 {:ok, response} = Filter.create(query)
1574 res = FilterView.render("filter.json", filter: response)
1578 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1579 filter = Filter.get(filter_id, user)
1580 res = FilterView.render("filter.json", filter: filter)
1585 %{assigns: %{user: user}} = conn,
1586 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1590 filter_id: filter_id,
1593 hide: Map.get(params, "irreversible", nil),
1594 whole_word: Map.get(params, "boolean", true)
1598 {:ok, response} = Filter.update(query)
1599 res = FilterView.render("filter.json", filter: response)
1603 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1606 filter_id: filter_id
1609 {:ok, _} = Filter.delete(query)
1615 def errors(conn, {:error, %Changeset{} = changeset}) do
1618 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1619 |> Enum.map_join(", ", fn {_k, v} -> v end)
1622 |> put_status(:unprocessable_entity)
1623 |> json(%{error: error_message})
1626 def errors(conn, {:error, :not_found}) do
1627 render_error(conn, :not_found, "Record not found")
1630 def errors(conn, _) do
1632 |> put_status(:internal_server_error)
1633 |> json(dgettext("errors", "Something went wrong"))
1636 def suggestions(%{assigns: %{user: user}} = conn, _) do
1637 suggestions = Config.get(:suggestions)
1639 if Keyword.get(suggestions, :enabled, false) do
1640 api = Keyword.get(suggestions, :third_party_engine, "")
1641 timeout = Keyword.get(suggestions, :timeout, 5000)
1642 limit = Keyword.get(suggestions, :limit, 23)
1644 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1646 user = user.nickname
1650 |> String.replace("{{host}}", host)
1651 |> String.replace("{{user}}", user)
1653 with {:ok, %{status: 200, body: body}} <-
1658 recv_timeout: timeout,
1662 {:ok, data} <- Jason.decode(body) do
1665 |> Enum.slice(0, limit)
1670 case User.get_or_fetch(x["acct"]) do
1671 {:ok, %User{id: id}} -> id
1677 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1680 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1686 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1693 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1694 with %Activity{} = activity <- Activity.get_by_id(status_id),
1695 true <- Visibility.visible_for_user?(activity, user) do
1699 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1709 def reports(%{assigns: %{user: user}} = conn, params) do
1710 case CommonAPI.report(user, params) do
1713 |> put_view(ReportView)
1714 |> try_render("report.json", %{activity: activity})
1718 |> put_status(:bad_request)
1719 |> json(%{error: err})
1723 def account_register(
1724 %{assigns: %{app: app}} = conn,
1725 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1733 "captcha_answer_data",
1737 |> Map.put("nickname", nickname)
1738 |> Map.put("fullname", params["fullname"] || nickname)
1739 |> Map.put("bio", params["bio"] || "")
1740 |> Map.put("confirm", params["password"])
1742 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1743 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1745 token_type: "Bearer",
1746 access_token: token.token,
1748 created_at: Token.Utils.format_created_at(token)
1753 |> put_status(:bad_request)
1758 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1759 render_error(conn, :bad_request, "Missing parameters")
1762 def account_register(conn, _) do
1763 render_error(conn, :forbidden, "Invalid credentials")
1766 def conversations(%{assigns: %{user: user}} = conn, params) do
1767 participations = Participation.for_user_with_last_activity_id(user, params)
1770 Enum.map(participations, fn participation ->
1771 ConversationView.render("participation.json", %{participation: participation, user: user})
1775 |> add_link_headers(:conversations, participations)
1776 |> json(conversations)
1779 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1780 with %Participation{} = participation <-
1781 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1782 {:ok, participation} <- Participation.mark_as_read(participation) do
1783 participation_view =
1784 ConversationView.render("participation.json", %{participation: participation, user: user})
1787 |> json(participation_view)
1791 def try_render(conn, target, params)
1792 when is_binary(target) do
1793 case render(conn, target, params) do
1794 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1799 def try_render(conn, _, _) do
1800 render_error(conn, :not_implemented, "Can't display this activity")
1803 defp present?(nil), do: false
1804 defp present?(false), do: false
1805 defp present?(_), do: true