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
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
17 alias Pleroma.Formatter
19 alias Pleroma.Notification
21 alias Pleroma.Pagination
22 alias Pleroma.Plugs.RateLimiter
24 alias Pleroma.ScheduledActivity
26 alias Pleroma.SubscriptionNotification
29 alias Pleroma.Web.ActivityPub.ActivityPub
30 alias Pleroma.Web.ActivityPub.Visibility
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.MastodonAPI.AccountView
33 alias Pleroma.Web.MastodonAPI.AppView
34 alias Pleroma.Web.MastodonAPI.ConversationView
35 alias Pleroma.Web.MastodonAPI.FilterView
36 alias Pleroma.Web.MastodonAPI.ListView
37 alias Pleroma.Web.MastodonAPI.MastodonAPI
38 alias Pleroma.Web.MastodonAPI.MastodonView
39 alias Pleroma.Web.MastodonAPI.NotificationView
40 alias Pleroma.Web.MastodonAPI.ReportView
41 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
42 alias Pleroma.Web.MastodonAPI.StatusView
43 alias Pleroma.Web.MastodonAPI.SubscriptionNotificationView
44 alias Pleroma.Web.MediaProxy
45 alias Pleroma.Web.OAuth.App
46 alias Pleroma.Web.OAuth.Authorization
47 alias Pleroma.Web.OAuth.Scopes
48 alias Pleroma.Web.OAuth.Token
49 alias Pleroma.Web.TwitterAPI.TwitterAPI
51 alias Pleroma.Web.ControllerHelper
55 require Pleroma.Constants
57 @rate_limited_relations_actions ~w(follow unfollow)a
59 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
60 post_status delete_status)a
64 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
65 when action in ~w(reblog_status unreblog_status)a
70 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
71 when action in ~w(fav_status unfav_status)a
76 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
79 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
80 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
81 plug(RateLimiter, :app_account_creation when action == :account_register)
82 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
83 plug(RateLimiter, :password_reset when action == :password_reset)
84 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
86 @local_mastodon_name "Mastodon-Local"
88 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
90 def create_app(conn, params) do
91 scopes = Scopes.fetch_scopes(params, ["read"])
95 |> Map.drop(["scope", "scopes"])
96 |> Map.put("scopes", scopes)
98 with cs <- App.register_changeset(%App{}, app_attrs),
99 false <- cs.changes[:client_name] == @local_mastodon_name,
100 {:ok, app} <- Repo.insert(cs) do
103 |> render("show.json", %{app: app})
112 value_function \\ fn x -> {:ok, x} end
114 if Map.has_key?(params, params_field) do
115 case value_function.(params[params_field]) do
116 {:ok, new_value} -> Map.put(map, map_field, new_value)
124 def update_credentials(%{assigns: %{user: user}} = conn, params) do
129 |> add_if_present(params, "display_name", :name)
130 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
131 |> add_if_present(params, "avatar", :avatar, fn value ->
132 with %Plug.Upload{} <- value,
133 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
140 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
144 |> Map.get(:emoji, [])
145 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
156 :skip_thread_containment
158 |> Enum.reduce(%{}, fn key, acc ->
159 add_if_present(acc, params, to_string(key), key, fn value ->
160 {:ok, ControllerHelper.truthy_param?(value)}
163 |> add_if_present(params, "default_scope", :default_scope)
164 |> add_if_present(params, "fields", :fields, fn fields ->
165 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
169 |> add_if_present(params, "fields", :raw_fields)
170 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
171 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
173 |> add_if_present(params, "header", :banner, fn value ->
174 with %Plug.Upload{} <- value,
175 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
181 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
182 with %Plug.Upload{} <- value,
183 {:ok, object} <- ActivityPub.upload(value, type: :background) do
189 |> Map.put(:emoji, user_info_emojis)
191 info_cng = User.Info.profile_update(user.info, info_params)
193 with changeset <- User.update_changeset(user, user_params),
194 changeset <- Changeset.put_embed(changeset, :info, info_cng),
195 {:ok, user} <- User.update_and_set_cache(changeset) do
196 if original_user != user do
197 CommonAPI.update(user)
202 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
205 _e -> render_error(conn, :forbidden, "Invalid request")
209 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
210 change = Changeset.change(user, %{avatar: nil})
211 {:ok, user} = User.update_and_set_cache(change)
212 CommonAPI.update(user)
214 json(conn, %{url: nil})
217 def update_avatar(%{assigns: %{user: user}} = conn, params) do
218 {:ok, object} = ActivityPub.upload(params, type: :avatar)
219 change = Changeset.change(user, %{avatar: object.data})
220 {:ok, user} = User.update_and_set_cache(change)
221 CommonAPI.update(user)
222 %{"url" => [%{"href" => href} | _]} = object.data
224 json(conn, %{url: href})
227 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
228 with new_info <- %{"banner" => %{}},
229 info_cng <- User.Info.profile_update(user.info, new_info),
230 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
231 {:ok, user} <- User.update_and_set_cache(changeset) do
232 CommonAPI.update(user)
234 json(conn, %{url: nil})
238 def update_banner(%{assigns: %{user: user}} = conn, params) do
239 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
240 new_info <- %{"banner" => object.data},
241 info_cng <- User.Info.profile_update(user.info, new_info),
242 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
243 {:ok, user} <- User.update_and_set_cache(changeset) do
244 CommonAPI.update(user)
245 %{"url" => [%{"href" => href} | _]} = object.data
247 json(conn, %{url: href})
251 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
252 with new_info <- %{"background" => %{}},
253 info_cng <- User.Info.profile_update(user.info, new_info),
254 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
255 {:ok, _user} <- User.update_and_set_cache(changeset) do
256 json(conn, %{url: nil})
260 def update_background(%{assigns: %{user: user}} = conn, params) do
261 with {:ok, object} <- ActivityPub.upload(params, type: :background),
262 new_info <- %{"background" => object.data},
263 info_cng <- User.Info.profile_update(user.info, new_info),
264 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
265 {:ok, _user} <- User.update_and_set_cache(changeset) do
266 %{"url" => [%{"href" => href} | _]} = object.data
268 json(conn, %{url: href})
272 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
273 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
276 AccountView.render("account.json", %{
279 with_pleroma_settings: true,
280 with_chat_token: chat_token
286 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
287 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
290 |> render("short.json", %{app: app})
294 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
295 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
296 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
297 account = AccountView.render("account.json", %{user: user, for: for_user})
300 _e -> render_error(conn, :not_found, "Can't find user")
304 @mastodon_api_level "2.7.2"
306 def masto_instance(conn, _params) do
307 instance = Config.get(:instance)
311 title: Keyword.get(instance, :name),
312 description: Keyword.get(instance, :description),
313 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
314 email: Keyword.get(instance, :email),
316 streaming_api: Pleroma.Web.Endpoint.websocket_url()
318 stats: Stats.get_stats(),
319 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
321 registrations: Pleroma.Config.get([:instance, :registrations_open]),
322 # Extra (not present in Mastodon):
323 max_toot_chars: Keyword.get(instance, :limit),
324 poll_limits: Keyword.get(instance, :poll_limits)
330 def peers(conn, _params) do
331 json(conn, Stats.get_peers())
334 defp mastodonized_emoji do
335 Pleroma.Emoji.get_all()
336 |> Enum.map(fn {shortcode, relative_url, tags} ->
337 url = to_string(URI.merge(Web.base_url(), relative_url))
340 "shortcode" => shortcode,
342 "visible_in_picker" => true,
345 # Assuming that a comma is authorized in the category name
346 "category" => (tags -- ["Custom"]) |> Enum.join(",")
351 def custom_emojis(conn, _params) do
352 mastodon_emoji = mastodonized_emoji()
353 json(conn, mastodon_emoji)
356 def home_timeline(%{assigns: %{user: user}} = conn, params) do
359 |> Map.put("type", ["Create", "Announce"])
360 |> Map.put("blocking_user", user)
361 |> Map.put("muting_user", user)
362 |> Map.put("user", user)
365 [user.ap_id | user.following]
366 |> ActivityPub.fetch_activities(params)
370 |> add_link_headers(activities)
371 |> put_view(StatusView)
372 |> render("index.json", %{activities: activities, for: user, as: :activity})
375 def public_timeline(%{assigns: %{user: user}} = conn, params) do
376 local_only = params["local"] in [true, "True", "true", "1"]
380 |> Map.put("type", ["Create", "Announce"])
381 |> Map.put("local_only", local_only)
382 |> Map.put("blocking_user", user)
383 |> Map.put("muting_user", user)
384 |> Map.put("user", user)
385 |> ActivityPub.fetch_public_activities()
389 |> add_link_headers(activities, %{"local" => local_only})
390 |> put_view(StatusView)
391 |> render("index.json", %{activities: activities, for: user, as: :activity})
394 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
395 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
398 |> Map.put("tag", params["tagged"])
400 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
403 |> add_link_headers(activities)
404 |> put_view(StatusView)
405 |> render("index.json", %{
406 activities: activities,
413 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
416 |> Map.put("type", "Create")
417 |> Map.put("blocking_user", user)
418 |> Map.put("user", user)
419 |> Map.put(:visibility, "direct")
423 |> ActivityPub.fetch_activities_query(params)
424 |> Pagination.fetch_paginated(params)
427 |> add_link_headers(activities)
428 |> put_view(StatusView)
429 |> render("index.json", %{activities: activities, for: user, as: :activity})
432 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
438 |> Activity.all_by_ids_with_object()
439 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
442 |> put_view(StatusView)
443 |> render("index.json", activities: activities, for: user, as: :activity)
446 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
447 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
448 true <- Visibility.visible_for_user?(activity, user) do
450 |> put_view(StatusView)
451 |> try_render("status.json", %{activity: activity, for: user})
455 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
456 with %Activity{} = activity <- Activity.get_by_id(id),
458 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
459 "blocking_user" => user,
461 "exclude_id" => activity.id
463 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
469 activities: grouped_activities[true] || [],
473 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
478 activities: grouped_activities[false] || [],
482 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
489 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
490 with %Object{} = object <- Object.get_by_id(id),
491 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
492 true <- Visibility.visible_for_user?(activity, user) do
494 |> put_view(StatusView)
495 |> try_render("poll.json", %{object: object, for: user})
497 error when is_nil(error) or error == false ->
498 render_error(conn, :not_found, "Record not found")
502 defp get_cached_vote_or_vote(user, object, choices) do
503 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
506 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
507 case CommonAPI.vote(user, object, choices) do
508 {:error, _message} = res -> {:ignore, res}
509 res -> {:commit, res}
516 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
517 with %Object{} = object <- Object.get_by_id(id),
518 true <- object.data["type"] == "Question",
519 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
520 true <- Visibility.visible_for_user?(activity, user),
521 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
523 |> put_view(StatusView)
524 |> try_render("poll.json", %{object: object, for: user})
527 render_error(conn, :not_found, "Record not found")
530 render_error(conn, :not_found, "Record not found")
534 |> put_status(:unprocessable_entity)
535 |> json(%{error: message})
539 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
540 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
542 |> add_link_headers(scheduled_activities)
543 |> put_view(ScheduledActivityView)
544 |> render("index.json", %{scheduled_activities: scheduled_activities})
548 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
549 with %ScheduledActivity{} = scheduled_activity <-
550 ScheduledActivity.get(user, scheduled_activity_id) do
552 |> put_view(ScheduledActivityView)
553 |> render("show.json", %{scheduled_activity: scheduled_activity})
555 _ -> {:error, :not_found}
559 def update_scheduled_status(
560 %{assigns: %{user: user}} = conn,
561 %{"id" => scheduled_activity_id} = params
563 with %ScheduledActivity{} = scheduled_activity <-
564 ScheduledActivity.get(user, scheduled_activity_id),
565 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
567 |> put_view(ScheduledActivityView)
568 |> render("show.json", %{scheduled_activity: scheduled_activity})
570 nil -> {:error, :not_found}
575 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
576 with %ScheduledActivity{} = scheduled_activity <-
577 ScheduledActivity.get(user, scheduled_activity_id),
578 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
580 |> put_view(ScheduledActivityView)
581 |> render("show.json", %{scheduled_activity: scheduled_activity})
583 nil -> {:error, :not_found}
588 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
591 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
593 scheduled_at = params["scheduled_at"]
595 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
596 with {:ok, scheduled_activity} <-
597 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
599 |> put_view(ScheduledActivityView)
600 |> render("show.json", %{scheduled_activity: scheduled_activity})
603 params = Map.drop(params, ["scheduled_at"])
605 case CommonAPI.post(user, params) do
608 |> put_status(:unprocessable_entity)
609 |> json(%{error: message})
613 |> put_view(StatusView)
614 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
619 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
620 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
623 _e -> render_error(conn, :forbidden, "Can't delete this post")
627 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
628 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
629 %Activity{} = announce <- Activity.normalize(announce.data) do
631 |> put_view(StatusView)
632 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
636 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
637 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
638 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
640 |> put_view(StatusView)
641 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
645 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
646 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
647 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
649 |> put_view(StatusView)
650 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
654 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
655 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(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 pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
664 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
666 |> put_view(StatusView)
667 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
671 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
672 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
674 |> put_view(StatusView)
675 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
679 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
680 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
681 %User{} = user <- User.get_cached_by_nickname(user.nickname),
682 true <- Visibility.visible_for_user?(activity, user),
683 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
685 |> put_view(StatusView)
686 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
690 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
691 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
692 %User{} = user <- User.get_cached_by_nickname(user.nickname),
693 true <- Visibility.visible_for_user?(activity, user),
694 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
696 |> put_view(StatusView)
697 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
701 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
702 activity = Activity.get_by_id(id)
704 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
706 |> put_view(StatusView)
707 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
711 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
712 activity = Activity.get_by_id(id)
714 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
716 |> put_view(StatusView)
717 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
721 def notifications(%{assigns: %{user: user}} = conn, params) do
722 notifications = MastodonAPI.get_notifications(user, params)
725 |> add_link_headers(notifications)
726 |> put_view(NotificationView)
727 |> render("index.json", %{notifications: notifications, for: user})
730 def subscription_notifications(%{assigns: %{user: user}} = conn, params) do
731 notifications = MastodonAPI.get_subscription_notifications(user, params)
734 |> add_link_headers(:subscription_notifications, notifications)
735 |> put_view(SubscriptionNotificationView)
736 |> render("index.json", %{notifications: notifications, for: user})
739 def get_subscription_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
740 with {:ok, notification} <- SubscriptionNotification.get(user, id) do
742 |> put_view(SubscriptionNotificationView)
743 |> render("show.json", %{subscription_notification: notification, for: user})
747 |> put_status(:forbidden)
748 |> json(%{"error" => reason})
752 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
753 with {:ok, notification} <- Notification.get(user, id) do
755 |> put_view(NotificationView)
756 |> render("show.json", %{notification: notification, for: user})
760 |> put_status(:forbidden)
761 |> json(%{"error" => reason})
765 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
766 Notification.clear(user)
770 def clear_subscription_notifications(%{assigns: %{user: user}} = conn, _params) do
771 SubscriptionNotification.clear(user)
775 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
776 with {:ok, _notif} <- Notification.dismiss(user, id) do
781 |> put_status(:forbidden)
782 |> json(%{"error" => reason})
786 def dismiss_subscription_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
787 with {:ok, _notif} <- SubscriptionNotification.dismiss(user, id) do
792 |> put_status(:forbidden)
793 |> json(%{"error" => reason})
797 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
798 Notification.destroy_multiple(user, ids)
802 def destroy_multiple_subscription_notifications(
803 %{assigns: %{user: user}} = conn,
804 %{"ids" => ids} = _params
806 SubscriptionNotification.destroy_multiple(user, ids)
810 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
812 q = from(u in User, where: u.id in ^id)
813 targets = Repo.all(q)
816 |> put_view(AccountView)
817 |> render("relationships.json", %{user: user, targets: targets})
820 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
821 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
823 def update_media(%{assigns: %{user: user}} = conn, data) do
824 with %Object{} = object <- Repo.get(Object, data["id"]),
825 true <- Object.authorize_mutation(object, user),
826 true <- is_binary(data["description"]),
827 description <- data["description"] do
828 new_data = %{object.data | "name" => description}
832 |> Object.change(%{data: new_data})
835 attachment_data = Map.put(new_data, "id", object.id)
838 |> put_view(StatusView)
839 |> render("attachment.json", %{attachment: attachment_data})
843 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
844 with {:ok, object} <-
847 actor: User.ap_id(user),
848 description: Map.get(data, "description")
850 attachment_data = Map.put(object.data, "id", object.id)
853 |> put_view(StatusView)
854 |> render("attachment.json", %{attachment: attachment_data})
858 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
859 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
860 %{} = attachment_data <- Map.put(object.data, "id", object.id),
861 %{type: type} = rendered <-
862 StatusView.render("attachment.json", %{attachment: attachment_data}) do
863 # Reject if not an image
864 if type == "image" do
866 # Save to the user's info
867 info_changeset = User.Info.mascot_update(user.info, rendered)
871 |> Changeset.change()
872 |> Changeset.put_embed(:info, info_changeset)
874 {:ok, _user} = User.update_and_set_cache(user_changeset)
879 render_error(conn, :unsupported_media_type, "mascots can only be images")
884 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
885 mascot = User.get_mascot(user)
891 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
892 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
893 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
894 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
895 q = from(u in User, where: u.ap_id in ^likes)
899 |> Enum.filter(&(not User.blocks?(user, &1)))
902 |> put_view(AccountView)
903 |> render("accounts.json", %{for: user, users: users, as: :user})
905 {:visible, false} -> {:error, :not_found}
910 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
911 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
912 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
913 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
914 q = from(u in User, where: u.ap_id in ^announces)
918 |> Enum.filter(&(not User.blocks?(user, &1)))
921 |> put_view(AccountView)
922 |> render("accounts.json", %{for: user, users: users, as: :user})
924 {:visible, false} -> {:error, :not_found}
929 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
930 local_only = params["local"] in [true, "True", "true", "1"]
933 [params["tag"], params["any"]]
937 |> Enum.map(&String.downcase(&1))
942 |> Enum.map(&String.downcase(&1))
947 |> Enum.map(&String.downcase(&1))
951 |> Map.put("type", "Create")
952 |> Map.put("local_only", local_only)
953 |> Map.put("blocking_user", user)
954 |> Map.put("muting_user", user)
955 |> Map.put("user", user)
956 |> Map.put("tag", tags)
957 |> Map.put("tag_all", tag_all)
958 |> Map.put("tag_reject", tag_reject)
959 |> ActivityPub.fetch_public_activities()
963 |> add_link_headers(activities, %{"local" => local_only})
964 |> put_view(StatusView)
965 |> render("index.json", %{activities: activities, for: user, as: :activity})
968 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
969 with %User{} = user <- User.get_cached_by_id(id),
970 followers <- MastodonAPI.get_followers(user, params) do
973 for_user && user.id == for_user.id -> followers
974 user.info.hide_followers -> []
979 |> add_link_headers(followers)
980 |> put_view(AccountView)
981 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
985 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
986 with %User{} = user <- User.get_cached_by_id(id),
987 followers <- MastodonAPI.get_friends(user, params) do
990 for_user && user.id == for_user.id -> followers
991 user.info.hide_follows -> []
996 |> add_link_headers(followers)
997 |> put_view(AccountView)
998 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1002 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
1003 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
1005 |> put_view(AccountView)
1006 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1010 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1011 with %User{} = follower <- User.get_cached_by_id(id),
1012 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1014 |> put_view(AccountView)
1015 |> render("relationship.json", %{user: followed, target: follower})
1017 {:error, message} ->
1019 |> put_status(:forbidden)
1020 |> json(%{error: message})
1024 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1025 with %User{} = follower <- User.get_cached_by_id(id),
1026 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1028 |> put_view(AccountView)
1029 |> render("relationship.json", %{user: followed, target: follower})
1031 {:error, message} ->
1033 |> put_status(:forbidden)
1034 |> json(%{error: message})
1038 def follow(%{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} <- MastodonAPI.follow(follower, followed, conn.params) do
1043 |> put_view(AccountView)
1044 |> render("relationship.json", %{user: follower, target: followed})
1047 {:error, :not_found}
1049 {:error, message} ->
1051 |> put_status(:forbidden)
1052 |> json(%{error: message})
1056 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1057 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1058 {_, true} <- {:followed, follower.id != followed.id},
1059 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1061 |> put_view(AccountView)
1062 |> render("account.json", %{user: followed, for: follower})
1065 {:error, :not_found}
1067 {:error, message} ->
1069 |> put_status(:forbidden)
1070 |> json(%{error: message})
1074 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1075 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1076 {_, true} <- {:followed, follower.id != followed.id},
1077 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1079 |> put_view(AccountView)
1080 |> render("relationship.json", %{user: follower, target: followed})
1083 {:error, :not_found}
1090 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1092 if Map.has_key?(params, "notifications"),
1093 do: params["notifications"] in [true, "True", "true", "1"],
1096 with %User{} = muted <- User.get_cached_by_id(id),
1097 {:ok, muter} <- User.mute(muter, muted, notifications) do
1099 |> put_view(AccountView)
1100 |> render("relationship.json", %{user: muter, target: muted})
1102 {:error, message} ->
1104 |> put_status(:forbidden)
1105 |> json(%{error: message})
1109 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1110 with %User{} = muted <- User.get_cached_by_id(id),
1111 {:ok, muter} <- User.unmute(muter, muted) do
1113 |> put_view(AccountView)
1114 |> render("relationship.json", %{user: muter, target: muted})
1116 {:error, message} ->
1118 |> put_status(:forbidden)
1119 |> json(%{error: message})
1123 def mutes(%{assigns: %{user: user}} = conn, _) do
1124 with muted_accounts <- User.muted_users(user) do
1125 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1130 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1131 with %User{} = blocked <- User.get_cached_by_id(id),
1132 {:ok, blocker} <- User.block(blocker, blocked),
1133 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1135 |> put_view(AccountView)
1136 |> render("relationship.json", %{user: blocker, target: blocked})
1138 {:error, message} ->
1140 |> put_status(:forbidden)
1141 |> json(%{error: message})
1145 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1146 with %User{} = blocked <- User.get_cached_by_id(id),
1147 {:ok, blocker} <- User.unblock(blocker, blocked),
1148 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1150 |> put_view(AccountView)
1151 |> render("relationship.json", %{user: blocker, target: blocked})
1153 {:error, message} ->
1155 |> put_status(:forbidden)
1156 |> json(%{error: message})
1160 def blocks(%{assigns: %{user: user}} = conn, _) do
1161 with blocked_accounts <- User.blocked_users(user) do
1162 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1167 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1168 json(conn, info.domain_blocks || [])
1171 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1172 User.block_domain(blocker, domain)
1176 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1177 User.unblock_domain(blocker, domain)
1181 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1182 with %User{} = subscription_target <- User.get_cached_by_id(id),
1183 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1185 |> put_view(AccountView)
1186 |> render("relationship.json", %{user: user, target: subscription_target})
1188 {:error, message} ->
1190 |> put_status(:forbidden)
1191 |> json(%{error: message})
1195 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1196 with %User{} = subscription_target <- User.get_cached_by_id(id),
1197 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1199 |> put_view(AccountView)
1200 |> render("relationship.json", %{user: user, target: subscription_target})
1202 {:error, message} ->
1204 |> put_status(:forbidden)
1205 |> json(%{error: message})
1209 def favourites(%{assigns: %{user: user}} = conn, params) do
1212 |> Map.put("type", "Create")
1213 |> Map.put("favorited_by", user.ap_id)
1214 |> Map.put("blocking_user", user)
1217 ActivityPub.fetch_activities([], params)
1221 |> add_link_headers(activities)
1222 |> put_view(StatusView)
1223 |> render("index.json", %{activities: activities, for: user, as: :activity})
1226 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1227 with %User{} = user <- User.get_by_id(id),
1228 false <- user.info.hide_favorites do
1231 |> Map.put("type", "Create")
1232 |> Map.put("favorited_by", user.ap_id)
1233 |> Map.put("blocking_user", for_user)
1237 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1239 [Pleroma.Constants.as_public()]
1244 |> ActivityPub.fetch_activities(params)
1248 |> add_link_headers(activities)
1249 |> put_view(StatusView)
1250 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1252 nil -> {:error, :not_found}
1253 true -> render_error(conn, :forbidden, "Can't get favorites")
1257 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1258 user = User.get_cached_by_id(user.id)
1261 Bookmark.for_user_query(user.id)
1262 |> Pagination.fetch_paginated(params)
1266 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1269 |> add_link_headers(bookmarks)
1270 |> put_view(StatusView)
1271 |> render("index.json", %{activities: activities, for: user, as: :activity})
1274 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1275 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1276 res = ListView.render("lists.json", lists: lists)
1280 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1281 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1284 |> Map.put("type", "Create")
1285 |> Map.put("blocking_user", user)
1286 |> Map.put("user", user)
1287 |> Map.put("muting_user", user)
1289 # we must filter the following list for the user to avoid leaking statuses the user
1290 # does not actually have permission to see (for more info, peruse security issue #270).
1293 |> Enum.filter(fn x -> x in user.following end)
1294 |> ActivityPub.fetch_activities_bounded(following, params)
1298 |> put_view(StatusView)
1299 |> render("index.json", %{activities: activities, for: user, as: :activity})
1301 _e -> render_error(conn, :forbidden, "Error.")
1305 def index(%{assigns: %{user: user}} = conn, _params) do
1306 token = get_session(conn, :oauth_token)
1309 mastodon_emoji = mastodonized_emoji()
1311 limit = Config.get([:instance, :limit])
1314 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1319 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1320 access_token: token,
1322 domain: Pleroma.Web.Endpoint.host(),
1325 unfollow_modal: false,
1328 auto_play_gif: false,
1329 display_sensitive_media: false,
1330 reduce_motion: false,
1331 max_toot_chars: limit,
1332 mascot: User.get_mascot(user)["url"]
1334 poll_limits: Config.get([:instance, :poll_limits]),
1336 delete_others_notice: present?(user.info.is_moderator),
1337 admin: present?(user.info.is_admin)
1341 default_privacy: user.info.default_scope,
1342 default_sensitive: false,
1343 allow_content_types: Config.get([:instance, :allowed_post_formats])
1345 media_attachments: %{
1346 accept_content_types: [
1362 user.info.settings ||
1392 push_subscription: nil,
1394 custom_emojis: mastodon_emoji,
1400 |> put_layout(false)
1401 |> put_view(MastodonView)
1402 |> render("index.html", %{initial_state: initial_state})
1405 |> put_session(:return_to, conn.request_path)
1406 |> redirect(to: "/web/login")
1410 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1411 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1413 with changeset <- Changeset.change(user),
1414 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1415 {:ok, _user} <- User.update_and_set_cache(changeset) do
1420 |> put_status(:internal_server_error)
1421 |> json(%{error: inspect(e)})
1425 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1426 redirect(conn, to: local_mastodon_root_path(conn))
1429 @doc "Local Mastodon FE login init action"
1430 def login(conn, %{"code" => auth_token}) do
1431 with {:ok, app} <- get_or_make_app(),
1432 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1433 {:ok, token} <- Token.exchange_token(app, auth) do
1435 |> put_session(:oauth_token, token.token)
1436 |> redirect(to: local_mastodon_root_path(conn))
1440 @doc "Local Mastodon FE callback action"
1441 def login(conn, _) do
1442 with {:ok, app} <- get_or_make_app() do
1447 response_type: "code",
1448 client_id: app.client_id,
1450 scope: Enum.join(app.scopes, " ")
1453 redirect(conn, to: path)
1457 defp local_mastodon_root_path(conn) do
1458 case get_session(conn, :return_to) do
1460 mastodon_api_path(conn, :index, ["getting-started"])
1463 delete_session(conn, :return_to)
1468 defp get_or_make_app do
1469 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1470 scopes = ["read", "write", "follow", "push"]
1472 with %App{} = app <- Repo.get_by(App, find_attrs) do
1474 if app.scopes == scopes do
1478 |> Changeset.change(%{scopes: scopes})
1486 App.register_changeset(
1488 Map.put(find_attrs, :scopes, scopes)
1495 def logout(conn, _) do
1498 |> redirect(to: "/")
1501 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1502 Logger.debug("Unimplemented, returning unmodified relationship")
1504 with %User{} = target <- User.get_cached_by_id(id) do
1506 |> put_view(AccountView)
1507 |> render("relationship.json", %{user: user, target: target})
1511 def empty_array(conn, _) do
1512 Logger.debug("Unimplemented, returning an empty array")
1516 def empty_object(conn, _) do
1517 Logger.debug("Unimplemented, returning an empty object")
1521 def get_filters(%{assigns: %{user: user}} = conn, _) do
1522 filters = Filter.get_filters(user)
1523 res = FilterView.render("filters.json", filters: filters)
1528 %{assigns: %{user: user}} = conn,
1529 %{"phrase" => phrase, "context" => context} = params
1535 hide: Map.get(params, "irreversible", false),
1536 whole_word: Map.get(params, "boolean", true)
1540 {:ok, response} = Filter.create(query)
1541 res = FilterView.render("filter.json", filter: response)
1545 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1546 filter = Filter.get(filter_id, user)
1547 res = FilterView.render("filter.json", filter: filter)
1552 %{assigns: %{user: user}} = conn,
1553 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1557 filter_id: filter_id,
1560 hide: Map.get(params, "irreversible", nil),
1561 whole_word: Map.get(params, "boolean", true)
1565 {:ok, response} = Filter.update(query)
1566 res = FilterView.render("filter.json", filter: response)
1570 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1573 filter_id: filter_id
1576 {:ok, _} = Filter.delete(query)
1580 def suggestions(%{assigns: %{user: user}} = conn, _) do
1581 suggestions = Config.get(:suggestions)
1583 if Keyword.get(suggestions, :enabled, false) do
1584 api = Keyword.get(suggestions, :third_party_engine, "")
1585 timeout = Keyword.get(suggestions, :timeout, 5000)
1586 limit = Keyword.get(suggestions, :limit, 23)
1588 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1590 user = user.nickname
1594 |> String.replace("{{host}}", host)
1595 |> String.replace("{{user}}", user)
1597 with {:ok, %{status: 200, body: body}} <-
1598 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1599 {:ok, data} <- Jason.decode(body) do
1602 |> Enum.slice(0, limit)
1605 |> Map.put("id", fetch_suggestion_id(x))
1606 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1607 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1613 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1620 defp fetch_suggestion_id(attrs) do
1621 case User.get_or_fetch(attrs["acct"]) do
1622 {:ok, %User{id: id}} -> id
1627 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1628 with %Activity{} = activity <- Activity.get_by_id(status_id),
1629 true <- Visibility.visible_for_user?(activity, user) do
1633 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1643 def reports(%{assigns: %{user: user}} = conn, params) do
1644 case CommonAPI.report(user, params) do
1647 |> put_view(ReportView)
1648 |> try_render("report.json", %{activity: activity})
1652 |> put_status(:bad_request)
1653 |> json(%{error: err})
1657 def account_register(
1658 %{assigns: %{app: app}} = conn,
1659 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1667 "captcha_answer_data",
1671 |> Map.put("nickname", nickname)
1672 |> Map.put("fullname", params["fullname"] || nickname)
1673 |> Map.put("bio", params["bio"] || "")
1674 |> Map.put("confirm", params["password"])
1676 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1677 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1679 token_type: "Bearer",
1680 access_token: token.token,
1682 created_at: Token.Utils.format_created_at(token)
1687 |> put_status(:bad_request)
1692 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1693 render_error(conn, :bad_request, "Missing parameters")
1696 def account_register(conn, _) do
1697 render_error(conn, :forbidden, "Invalid credentials")
1700 def conversations(%{assigns: %{user: user}} = conn, params) do
1701 participations = Participation.for_user_with_last_activity_id(user, params)
1704 Enum.map(participations, fn participation ->
1705 ConversationView.render("participation.json", %{participation: participation, for: user})
1709 |> add_link_headers(participations)
1710 |> json(conversations)
1713 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1714 with %Participation{} = participation <-
1715 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1716 {:ok, participation} <- Participation.mark_as_read(participation) do
1717 participation_view =
1718 ConversationView.render("participation.json", %{participation: participation, for: user})
1721 |> json(participation_view)
1725 def password_reset(conn, params) do
1726 nickname_or_email = params["email"] || params["nickname"]
1728 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1730 |> put_status(:no_content)
1733 {:error, "unknown user"} ->
1734 send_resp(conn, :not_found, "")
1737 send_resp(conn, :bad_request, "")
1741 def account_confirmation_resend(conn, params) do
1742 nickname_or_email = params["email"] || params["nickname"]
1744 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1745 {:ok, _} <- User.try_send_confirmation_email(user) do
1747 |> json_response(:no_content, "")
1751 def try_render(conn, target, params)
1752 when is_binary(target) do
1753 case render(conn, target, params) do
1754 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1759 def try_render(conn, _, _) do
1760 render_error(conn, :not_implemented, "Can't display this activity")
1763 defp present?(nil), do: false
1764 defp present?(false), do: false
1765 defp present?(_), do: true