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,
303 # Assuming that a comma is authorized in the category name
304 "category" => (tags -- ["Custom"]) |> Enum.join(",")
309 def custom_emojis(conn, _params) do
310 mastodon_emoji = mastodonized_emoji()
311 json(conn, mastodon_emoji)
314 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
317 |> Map.drop(["since_id", "max_id", "min_id"])
320 last = List.last(activities)
327 |> Map.get("limit", "20")
328 |> String.to_integer()
331 if length(activities) <= limit do
337 |> Enum.at(limit * -1)
341 {next_url, prev_url} =
345 Pleroma.Web.Endpoint,
348 Map.merge(params, %{max_id: max_id})
351 Pleroma.Web.Endpoint,
354 Map.merge(params, %{min_id: min_id})
360 Pleroma.Web.Endpoint,
362 Map.merge(params, %{max_id: max_id})
365 Pleroma.Web.Endpoint,
367 Map.merge(params, %{min_id: min_id})
373 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
379 def home_timeline(%{assigns: %{user: user}} = conn, params) do
382 |> Map.put("type", ["Create", "Announce"])
383 |> Map.put("blocking_user", user)
384 |> Map.put("muting_user", user)
385 |> Map.put("user", user)
388 [user.ap_id | user.following]
389 |> ActivityPub.fetch_activities(params)
393 |> add_link_headers(:home_timeline, activities)
394 |> put_view(StatusView)
395 |> render("index.json", %{activities: activities, for: user, as: :activity})
398 def public_timeline(%{assigns: %{user: user}} = conn, params) do
399 local_only = params["local"] in [true, "True", "true", "1"]
403 |> Map.put("type", ["Create", "Announce"])
404 |> Map.put("local_only", local_only)
405 |> Map.put("blocking_user", user)
406 |> Map.put("muting_user", user)
407 |> ActivityPub.fetch_public_activities()
411 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
412 |> put_view(StatusView)
413 |> render("index.json", %{activities: activities, for: user, as: :activity})
416 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
417 with %User{} = user <- User.get_cached_by_id(params["id"]) do
420 |> Map.put("tag", params["tagged"])
422 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
425 |> add_link_headers(:user_statuses, activities, params["id"])
426 |> put_view(StatusView)
427 |> render("index.json", %{
428 activities: activities,
435 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
438 |> Map.put("type", "Create")
439 |> Map.put("blocking_user", user)
440 |> Map.put("user", user)
441 |> Map.put(:visibility, "direct")
445 |> ActivityPub.fetch_activities_query(params)
446 |> Pagination.fetch_paginated(params)
449 |> add_link_headers(:dm_timeline, activities)
450 |> put_view(StatusView)
451 |> render("index.json", %{activities: activities, for: user, as: :activity})
454 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
455 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
456 true <- Visibility.visible_for_user?(activity, user) do
458 |> put_view(StatusView)
459 |> try_render("status.json", %{activity: activity, for: user})
463 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
464 with %Activity{} = activity <- Activity.get_by_id(id),
466 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
467 "blocking_user" => user,
471 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
473 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
474 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
480 activities: grouped_activities[true] || [],
484 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
489 activities: grouped_activities[false] || [],
493 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
500 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
501 with %Object{} = object <- Object.get_by_id(id),
502 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
503 true <- Visibility.visible_for_user?(activity, user) do
505 |> put_view(StatusView)
506 |> try_render("poll.json", %{object: object, for: user})
508 nil -> render_error(conn, :not_found, "Record not found")
509 false -> render_error(conn, :not_found, "Record not found")
513 defp get_cached_vote_or_vote(user, object, choices) do
514 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
517 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
518 case CommonAPI.vote(user, object, choices) do
519 {:error, _message} = res -> {:ignore, res}
520 res -> {:commit, res}
527 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
528 with %Object{} = object <- Object.get_by_id(id),
529 true <- object.data["type"] == "Question",
530 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
531 true <- Visibility.visible_for_user?(activity, user),
532 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
534 |> put_view(StatusView)
535 |> try_render("poll.json", %{object: object, for: user})
538 render_error(conn, :not_found, "Record not found")
541 render_error(conn, :not_found, "Record not found")
545 |> put_status(:unprocessable_entity)
546 |> json(%{error: message})
550 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
551 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
553 |> add_link_headers(:scheduled_statuses, scheduled_activities)
554 |> put_view(ScheduledActivityView)
555 |> render("index.json", %{scheduled_activities: scheduled_activities})
559 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
560 with %ScheduledActivity{} = scheduled_activity <-
561 ScheduledActivity.get(user, scheduled_activity_id) do
563 |> put_view(ScheduledActivityView)
564 |> render("show.json", %{scheduled_activity: scheduled_activity})
566 _ -> {:error, :not_found}
570 def update_scheduled_status(
571 %{assigns: %{user: user}} = conn,
572 %{"id" => scheduled_activity_id} = params
574 with %ScheduledActivity{} = scheduled_activity <-
575 ScheduledActivity.get(user, scheduled_activity_id),
576 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
578 |> put_view(ScheduledActivityView)
579 |> render("show.json", %{scheduled_activity: scheduled_activity})
581 nil -> {:error, :not_found}
586 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
587 with %ScheduledActivity{} = scheduled_activity <-
588 ScheduledActivity.get(user, scheduled_activity_id),
589 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
591 |> put_view(ScheduledActivityView)
592 |> render("show.json", %{scheduled_activity: scheduled_activity})
594 nil -> {:error, :not_found}
599 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
602 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
604 scheduled_at = params["scheduled_at"]
606 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
607 with {:ok, scheduled_activity} <-
608 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
610 |> put_view(ScheduledActivityView)
611 |> render("show.json", %{scheduled_activity: scheduled_activity})
614 params = Map.drop(params, ["scheduled_at"])
616 case CommonAPI.post(user, params) do
619 |> put_status(:unprocessable_entity)
620 |> json(%{error: message})
624 |> put_view(StatusView)
625 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
630 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
631 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
634 _e -> render_error(conn, :forbidden, "Can't delete this post")
638 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
639 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
640 %Activity{} = announce <- Activity.normalize(announce.data) do
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
647 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
648 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
649 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
651 |> put_view(StatusView)
652 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
656 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
657 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
658 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
665 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
666 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
667 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
675 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
677 |> put_view(StatusView)
678 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
682 |> put_status(:bad_request)
683 |> json(%{"error" => reason})
687 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
688 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
695 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
697 %User{} = user <- User.get_cached_by_nickname(user.nickname),
698 true <- Visibility.visible_for_user?(activity, user),
699 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
701 |> put_view(StatusView)
702 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
706 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
707 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
708 %User{} = user <- User.get_cached_by_nickname(user.nickname),
709 true <- Visibility.visible_for_user?(activity, user),
710 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
712 |> put_view(StatusView)
713 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
717 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
718 activity = Activity.get_by_id(id)
720 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
722 |> put_view(StatusView)
723 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
727 |> put_resp_content_type("application/json")
728 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
732 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
733 activity = Activity.get_by_id(id)
735 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
737 |> put_view(StatusView)
738 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
742 def notifications(%{assigns: %{user: user}} = conn, params) do
743 notifications = MastodonAPI.get_notifications(user, params)
746 |> add_link_headers(:notifications, notifications)
747 |> put_view(NotificationView)
748 |> render("index.json", %{notifications: notifications, for: user})
751 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
752 with {:ok, notification} <- Notification.get(user, id) do
754 |> put_view(NotificationView)
755 |> render("show.json", %{notification: notification, for: user})
759 |> put_status(:forbidden)
760 |> json(%{"error" => reason})
764 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
765 Notification.clear(user)
769 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
770 with {:ok, _notif} <- Notification.dismiss(user, id) do
775 |> put_status(:forbidden)
776 |> json(%{"error" => reason})
780 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
781 Notification.destroy_multiple(user, ids)
785 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
787 q = from(u in User, where: u.id in ^id)
788 targets = Repo.all(q)
791 |> put_view(AccountView)
792 |> render("relationships.json", %{user: user, targets: targets})
795 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
796 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
798 def update_media(%{assigns: %{user: user}} = conn, data) do
799 with %Object{} = object <- Repo.get(Object, data["id"]),
800 true <- Object.authorize_mutation(object, user),
801 true <- is_binary(data["description"]),
802 description <- data["description"] do
803 new_data = %{object.data | "name" => description}
807 |> Object.change(%{data: new_data})
810 attachment_data = Map.put(new_data, "id", object.id)
813 |> put_view(StatusView)
814 |> render("attachment.json", %{attachment: attachment_data})
818 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
819 with {:ok, object} <-
822 actor: User.ap_id(user),
823 description: Map.get(data, "description")
825 attachment_data = Map.put(object.data, "id", object.id)
828 |> put_view(StatusView)
829 |> render("attachment.json", %{attachment: attachment_data})
833 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
834 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
835 %{} = attachment_data <- Map.put(object.data, "id", object.id),
836 %{type: type} = rendered <-
837 StatusView.render("attachment.json", %{attachment: attachment_data}) do
838 # Reject if not an image
839 if type == "image" do
841 # Save to the user's info
842 info_changeset = User.Info.mascot_update(user.info, rendered)
846 |> Ecto.Changeset.change()
847 |> Ecto.Changeset.put_embed(:info, info_changeset)
849 {:ok, _user} = User.update_and_set_cache(user_changeset)
854 render_error(conn, :unsupported_media_type, "mascots can only be images")
859 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
860 mascot = User.get_mascot(user)
866 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
867 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
868 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
869 q = from(u in User, where: u.ap_id in ^likes)
873 |> put_view(AccountView)
874 |> render("accounts.json", %{for: user, users: users, as: :user})
880 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
881 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
882 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
883 q = from(u in User, where: u.ap_id in ^announces)
887 |> put_view(AccountView)
888 |> render("accounts.json", %{for: user, users: users, as: :user})
894 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
895 local_only = params["local"] in [true, "True", "true", "1"]
898 [params["tag"], params["any"]]
902 |> Enum.map(&String.downcase(&1))
907 |> Enum.map(&String.downcase(&1))
912 |> Enum.map(&String.downcase(&1))
916 |> Map.put("type", "Create")
917 |> Map.put("local_only", local_only)
918 |> Map.put("blocking_user", user)
919 |> Map.put("muting_user", user)
920 |> Map.put("tag", tags)
921 |> Map.put("tag_all", tag_all)
922 |> Map.put("tag_reject", tag_reject)
923 |> ActivityPub.fetch_public_activities()
927 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
928 |> put_view(StatusView)
929 |> render("index.json", %{activities: activities, for: user, as: :activity})
932 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
933 with %User{} = user <- User.get_cached_by_id(id),
934 followers <- MastodonAPI.get_followers(user, params) do
937 for_user && user.id == for_user.id -> followers
938 user.info.hide_followers -> []
943 |> add_link_headers(:followers, followers, user)
944 |> put_view(AccountView)
945 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
949 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
950 with %User{} = user <- User.get_cached_by_id(id),
951 followers <- MastodonAPI.get_friends(user, params) do
954 for_user && user.id == for_user.id -> followers
955 user.info.hide_follows -> []
960 |> add_link_headers(:following, followers, user)
961 |> put_view(AccountView)
962 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
966 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
967 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
969 |> put_view(AccountView)
970 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
974 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
975 with %User{} = follower <- User.get_cached_by_id(id),
976 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
978 |> put_view(AccountView)
979 |> render("relationship.json", %{user: followed, target: follower})
983 |> put_status(:forbidden)
984 |> json(%{error: message})
988 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
989 with %User{} = follower <- User.get_cached_by_id(id),
990 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
992 |> put_view(AccountView)
993 |> render("relationship.json", %{user: followed, target: follower})
997 |> put_status(:forbidden)
998 |> json(%{error: message})
1002 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1003 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1004 {_, true} <- {:followed, follower.id != followed.id},
1005 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1007 |> put_view(AccountView)
1008 |> render("relationship.json", %{user: follower, target: followed})
1011 {:error, :not_found}
1013 {:error, message} ->
1015 |> put_status(:forbidden)
1016 |> json(%{error: message})
1020 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1021 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1022 {_, true} <- {:followed, follower.id != followed.id},
1023 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1025 |> put_view(AccountView)
1026 |> render("account.json", %{user: followed, for: follower})
1029 {:error, :not_found}
1031 {:error, message} ->
1033 |> put_status(:forbidden)
1034 |> json(%{error: message})
1038 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1039 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1040 {_, true} <- {:followed, follower.id != followed.id},
1041 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1043 |> put_view(AccountView)
1044 |> render("relationship.json", %{user: follower, target: followed})
1047 {:error, :not_found}
1054 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1055 with %User{} = muted <- User.get_cached_by_id(id),
1056 {:ok, muter} <- User.mute(muter, muted) do
1058 |> put_view(AccountView)
1059 |> render("relationship.json", %{user: muter, target: muted})
1061 {:error, message} ->
1063 |> put_status(:forbidden)
1064 |> json(%{error: message})
1068 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1069 with %User{} = muted <- User.get_cached_by_id(id),
1070 {:ok, muter} <- User.unmute(muter, muted) do
1072 |> put_view(AccountView)
1073 |> render("relationship.json", %{user: muter, target: muted})
1075 {:error, message} ->
1077 |> put_status(:forbidden)
1078 |> json(%{error: message})
1082 def mutes(%{assigns: %{user: user}} = conn, _) do
1083 with muted_accounts <- User.muted_users(user) do
1084 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1089 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1090 with %User{} = blocked <- User.get_cached_by_id(id),
1091 {:ok, blocker} <- User.block(blocker, blocked),
1092 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1094 |> put_view(AccountView)
1095 |> render("relationship.json", %{user: blocker, target: blocked})
1097 {:error, message} ->
1099 |> put_status(:forbidden)
1100 |> json(%{error: message})
1104 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1105 with %User{} = blocked <- User.get_cached_by_id(id),
1106 {:ok, blocker} <- User.unblock(blocker, blocked),
1107 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1109 |> put_view(AccountView)
1110 |> render("relationship.json", %{user: blocker, target: blocked})
1112 {:error, message} ->
1114 |> put_status(:forbidden)
1115 |> json(%{error: message})
1119 def blocks(%{assigns: %{user: user}} = conn, _) do
1120 with blocked_accounts <- User.blocked_users(user) do
1121 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1126 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1127 json(conn, info.domain_blocks || [])
1130 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1131 User.block_domain(blocker, domain)
1135 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1136 User.unblock_domain(blocker, domain)
1140 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1141 with %User{} = subscription_target <- User.get_cached_by_id(id),
1142 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1144 |> put_view(AccountView)
1145 |> render("relationship.json", %{user: user, target: subscription_target})
1147 {:error, message} ->
1149 |> put_status(:forbidden)
1150 |> json(%{error: message})
1154 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1155 with %User{} = subscription_target <- User.get_cached_by_id(id),
1156 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1158 |> put_view(AccountView)
1159 |> render("relationship.json", %{user: user, target: subscription_target})
1161 {:error, message} ->
1163 |> put_status(:forbidden)
1164 |> json(%{error: message})
1168 def favourites(%{assigns: %{user: user}} = conn, params) do
1171 |> Map.put("type", "Create")
1172 |> Map.put("favorited_by", user.ap_id)
1173 |> Map.put("blocking_user", user)
1176 ActivityPub.fetch_activities([], params)
1180 |> add_link_headers(:favourites, activities)
1181 |> put_view(StatusView)
1182 |> render("index.json", %{activities: activities, for: user, as: :activity})
1185 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1186 with %User{} = user <- User.get_by_id(id),
1187 false <- user.info.hide_favorites do
1190 |> Map.put("type", "Create")
1191 |> Map.put("favorited_by", user.ap_id)
1192 |> Map.put("blocking_user", for_user)
1196 ["https://www.w3.org/ns/activitystreams#Public"] ++
1197 [for_user.ap_id | for_user.following]
1199 ["https://www.w3.org/ns/activitystreams#Public"]
1204 |> ActivityPub.fetch_activities(params)
1208 |> add_link_headers(:favourites, activities)
1209 |> put_view(StatusView)
1210 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1212 nil -> {:error, :not_found}
1213 true -> render_error(conn, :forbidden, "Can't get favorites")
1217 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1218 user = User.get_cached_by_id(user.id)
1221 Bookmark.for_user_query(user.id)
1222 |> Pagination.fetch_paginated(params)
1226 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1229 |> add_link_headers(:bookmarks, bookmarks)
1230 |> put_view(StatusView)
1231 |> render("index.json", %{activities: activities, for: user, as: :activity})
1234 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1235 lists = Pleroma.List.for_user(user, opts)
1236 res = ListView.render("lists.json", lists: lists)
1240 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1241 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1242 res = ListView.render("list.json", list: list)
1245 _e -> render_error(conn, :not_found, "Record not found")
1249 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1250 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1251 res = ListView.render("lists.json", lists: lists)
1255 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1256 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1257 {:ok, _list} <- Pleroma.List.delete(list) do
1261 json(conn, dgettext("errors", "error"))
1265 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1266 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1267 res = ListView.render("list.json", list: list)
1272 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1274 |> Enum.each(fn account_id ->
1275 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1276 %User{} = followed <- User.get_cached_by_id(account_id) do
1277 Pleroma.List.follow(list, followed)
1284 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1286 |> Enum.each(fn account_id ->
1287 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1288 %User{} = followed <- User.get_cached_by_id(account_id) do
1289 Pleroma.List.unfollow(list, followed)
1296 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1297 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1298 {:ok, users} = Pleroma.List.get_following(list) do
1300 |> put_view(AccountView)
1301 |> render("accounts.json", %{for: user, users: users, as: :user})
1305 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1306 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1307 {:ok, list} <- Pleroma.List.rename(list, title) do
1308 res = ListView.render("list.json", list: list)
1312 json(conn, dgettext("errors", "error"))
1316 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1317 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1320 |> Map.put("type", "Create")
1321 |> Map.put("blocking_user", user)
1322 |> Map.put("muting_user", user)
1324 # we must filter the following list for the user to avoid leaking statuses the user
1325 # does not actually have permission to see (for more info, peruse security issue #270).
1328 |> Enum.filter(fn x -> x in user.following end)
1329 |> ActivityPub.fetch_activities_bounded(following, params)
1333 |> put_view(StatusView)
1334 |> render("index.json", %{activities: activities, for: user, as: :activity})
1336 _e -> render_error(conn, :forbidden, "Error.")
1340 def index(%{assigns: %{user: user}} = conn, _params) do
1341 token = get_session(conn, :oauth_token)
1344 mastodon_emoji = mastodonized_emoji()
1346 limit = Config.get([:instance, :limit])
1349 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1354 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1355 access_token: token,
1357 domain: Pleroma.Web.Endpoint.host(),
1360 unfollow_modal: false,
1363 auto_play_gif: false,
1364 display_sensitive_media: false,
1365 reduce_motion: false,
1366 max_toot_chars: limit,
1367 mascot: User.get_mascot(user)["url"]
1369 poll_limits: Config.get([:instance, :poll_limits]),
1371 delete_others_notice: present?(user.info.is_moderator),
1372 admin: present?(user.info.is_admin)
1376 default_privacy: user.info.default_scope,
1377 default_sensitive: false,
1378 allow_content_types: Config.get([:instance, :allowed_post_formats])
1380 media_attachments: %{
1381 accept_content_types: [
1397 user.info.settings ||
1427 push_subscription: nil,
1429 custom_emojis: mastodon_emoji,
1435 |> put_layout(false)
1436 |> put_view(MastodonView)
1437 |> render("index.html", %{initial_state: initial_state})
1440 |> put_session(:return_to, conn.request_path)
1441 |> redirect(to: "/web/login")
1445 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1446 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1448 with changeset <- Ecto.Changeset.change(user),
1449 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1450 {:ok, _user} <- User.update_and_set_cache(changeset) do
1455 |> put_status(:internal_server_error)
1456 |> json(%{error: inspect(e)})
1460 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1461 redirect(conn, to: local_mastodon_root_path(conn))
1464 @doc "Local Mastodon FE login init action"
1465 def login(conn, %{"code" => auth_token}) do
1466 with {:ok, app} <- get_or_make_app(),
1467 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1468 {:ok, token} <- Token.exchange_token(app, auth) do
1470 |> put_session(:oauth_token, token.token)
1471 |> redirect(to: local_mastodon_root_path(conn))
1475 @doc "Local Mastodon FE callback action"
1476 def login(conn, _) do
1477 with {:ok, app} <- get_or_make_app() do
1482 response_type: "code",
1483 client_id: app.client_id,
1485 scope: Enum.join(app.scopes, " ")
1488 redirect(conn, to: path)
1492 defp local_mastodon_root_path(conn) do
1493 case get_session(conn, :return_to) do
1495 mastodon_api_path(conn, :index, ["getting-started"])
1498 delete_session(conn, :return_to)
1503 defp get_or_make_app do
1504 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1505 scopes = ["read", "write", "follow", "push"]
1507 with %App{} = app <- Repo.get_by(App, find_attrs) do
1509 if app.scopes == scopes do
1513 |> Ecto.Changeset.change(%{scopes: scopes})
1521 App.register_changeset(
1523 Map.put(find_attrs, :scopes, scopes)
1530 def logout(conn, _) do
1533 |> redirect(to: "/")
1536 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1537 Logger.debug("Unimplemented, returning unmodified relationship")
1539 with %User{} = target <- User.get_cached_by_id(id) do
1541 |> put_view(AccountView)
1542 |> render("relationship.json", %{user: user, target: target})
1546 def empty_array(conn, _) do
1547 Logger.debug("Unimplemented, returning an empty array")
1551 def empty_object(conn, _) do
1552 Logger.debug("Unimplemented, returning an empty object")
1556 def get_filters(%{assigns: %{user: user}} = conn, _) do
1557 filters = Filter.get_filters(user)
1558 res = FilterView.render("filters.json", filters: filters)
1563 %{assigns: %{user: user}} = conn,
1564 %{"phrase" => phrase, "context" => context} = params
1570 hide: Map.get(params, "irreversible", false),
1571 whole_word: Map.get(params, "boolean", true)
1575 {:ok, response} = Filter.create(query)
1576 res = FilterView.render("filter.json", filter: response)
1580 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1581 filter = Filter.get(filter_id, user)
1582 res = FilterView.render("filter.json", filter: filter)
1587 %{assigns: %{user: user}} = conn,
1588 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1592 filter_id: filter_id,
1595 hide: Map.get(params, "irreversible", nil),
1596 whole_word: Map.get(params, "boolean", true)
1600 {:ok, response} = Filter.update(query)
1601 res = FilterView.render("filter.json", filter: response)
1605 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1608 filter_id: filter_id
1611 {:ok, _} = Filter.delete(query)
1617 def errors(conn, {:error, %Changeset{} = changeset}) do
1620 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1621 |> Enum.map_join(", ", fn {_k, v} -> v end)
1624 |> put_status(:unprocessable_entity)
1625 |> json(%{error: error_message})
1628 def errors(conn, {:error, :not_found}) do
1629 render_error(conn, :not_found, "Record not found")
1632 def errors(conn, _) do
1634 |> put_status(:internal_server_error)
1635 |> json(dgettext("errors", "Something went wrong"))
1638 def suggestions(%{assigns: %{user: user}} = conn, _) do
1639 suggestions = Config.get(:suggestions)
1641 if Keyword.get(suggestions, :enabled, false) do
1642 api = Keyword.get(suggestions, :third_party_engine, "")
1643 timeout = Keyword.get(suggestions, :timeout, 5000)
1644 limit = Keyword.get(suggestions, :limit, 23)
1646 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1648 user = user.nickname
1652 |> String.replace("{{host}}", host)
1653 |> String.replace("{{user}}", user)
1655 with {:ok, %{status: 200, body: body}} <-
1660 recv_timeout: timeout,
1664 {:ok, data} <- Jason.decode(body) do
1667 |> Enum.slice(0, limit)
1672 case User.get_or_fetch(x["acct"]) do
1673 {:ok, %User{id: id}} -> id
1679 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1682 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1688 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1695 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1696 with %Activity{} = activity <- Activity.get_by_id(status_id),
1697 true <- Visibility.visible_for_user?(activity, user) do
1701 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1711 def reports(%{assigns: %{user: user}} = conn, params) do
1712 case CommonAPI.report(user, params) do
1715 |> put_view(ReportView)
1716 |> try_render("report.json", %{activity: activity})
1720 |> put_status(:bad_request)
1721 |> json(%{error: err})
1725 def account_register(
1726 %{assigns: %{app: app}} = conn,
1727 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1735 "captcha_answer_data",
1739 |> Map.put("nickname", nickname)
1740 |> Map.put("fullname", params["fullname"] || nickname)
1741 |> Map.put("bio", params["bio"] || "")
1742 |> Map.put("confirm", params["password"])
1744 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1745 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1747 token_type: "Bearer",
1748 access_token: token.token,
1750 created_at: Token.Utils.format_created_at(token)
1755 |> put_status(:bad_request)
1760 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1761 render_error(conn, :bad_request, "Missing parameters")
1764 def account_register(conn, _) do
1765 render_error(conn, :forbidden, "Invalid credentials")
1768 def conversations(%{assigns: %{user: user}} = conn, params) do
1769 participations = Participation.for_user_with_last_activity_id(user, params)
1772 Enum.map(participations, fn participation ->
1773 ConversationView.render("participation.json", %{participation: participation, user: user})
1777 |> add_link_headers(:conversations, participations)
1778 |> json(conversations)
1781 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1782 with %Participation{} = participation <-
1783 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1784 {:ok, participation} <- Participation.mark_as_read(participation) do
1785 participation_view =
1786 ConversationView.render("participation.json", %{participation: participation, user: user})
1789 |> json(participation_view)
1793 def try_render(conn, target, params)
1794 when is_binary(target) do
1795 case render(conn, target, params) do
1796 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1801 def try_render(conn, _, _) do
1802 render_error(conn, :not_implemented, "Can't display this activity")
1805 defp present?(nil), do: false
1806 defp present?(false), do: false
1807 defp present?(_), do: true