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
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Visibility
30 alias Pleroma.Web.CommonAPI
31 alias Pleroma.Web.MastodonAPI.AccountView
32 alias Pleroma.Web.MastodonAPI.AppView
33 alias Pleroma.Web.MastodonAPI.ConversationView
34 alias Pleroma.Web.MastodonAPI.FilterView
35 alias Pleroma.Web.MastodonAPI.ListView
36 alias Pleroma.Web.MastodonAPI.MastodonAPI
37 alias Pleroma.Web.MastodonAPI.MastodonView
38 alias Pleroma.Web.MastodonAPI.NotificationView
39 alias Pleroma.Web.MastodonAPI.ReportView
40 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
41 alias Pleroma.Web.MastodonAPI.StatusView
42 alias Pleroma.Web.MediaProxy
43 alias Pleroma.Web.OAuth.App
44 alias Pleroma.Web.OAuth.Authorization
45 alias Pleroma.Web.OAuth.Scopes
46 alias Pleroma.Web.OAuth.Token
47 alias Pleroma.Web.TwitterAPI.TwitterAPI
49 alias Pleroma.Web.ControllerHelper
53 require Pleroma.Constants
55 @rate_limited_relations_actions ~w(follow unfollow)a
57 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
58 post_status delete_status)a
62 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
63 when action in ~w(reblog_status unreblog_status)a
68 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
69 when action in ~w(fav_status unfav_status)a
74 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
77 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
78 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
79 plug(RateLimiter, :app_account_creation when action == :account_register)
80 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
81 plug(RateLimiter, :password_reset when action == :password_reset)
82 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
84 @local_mastodon_name "Mastodon-Local"
86 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
88 def create_app(conn, params) do
89 scopes = Scopes.fetch_scopes(params, ["read"])
93 |> Map.drop(["scope", "scopes"])
94 |> Map.put("scopes", scopes)
96 with cs <- App.register_changeset(%App{}, app_attrs),
97 false <- cs.changes[:client_name] == @local_mastodon_name,
98 {:ok, app} <- Repo.insert(cs) do
101 |> render("show.json", %{app: app})
110 value_function \\ fn x -> {:ok, x} end
112 if Map.has_key?(params, params_field) do
113 case value_function.(params[params_field]) do
114 {:ok, new_value} -> Map.put(map, map_field, new_value)
122 def update_credentials(%{assigns: %{user: user}} = conn, params) do
127 |> add_if_present(params, "display_name", :name)
128 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
129 |> add_if_present(params, "avatar", :avatar, fn value ->
130 with %Plug.Upload{} <- value,
131 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
138 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
142 |> Map.get(:emoji, [])
143 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
150 :hide_followers_count,
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 |> ActivityPub.fetch_public_activities()
388 |> add_link_headers(activities, %{"local" => local_only})
389 |> put_view(StatusView)
390 |> render("index.json", %{activities: activities, for: user, as: :activity})
393 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
394 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
397 |> Map.put("tag", params["tagged"])
399 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
402 |> add_link_headers(activities)
403 |> put_view(StatusView)
404 |> render("index.json", %{
405 activities: activities,
412 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
415 |> Map.put("type", "Create")
416 |> Map.put("blocking_user", user)
417 |> Map.put("user", user)
418 |> Map.put(:visibility, "direct")
422 |> ActivityPub.fetch_activities_query(params)
423 |> Pagination.fetch_paginated(params)
426 |> add_link_headers(activities)
427 |> put_view(StatusView)
428 |> render("index.json", %{activities: activities, for: user, as: :activity})
431 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
437 |> Activity.all_by_ids_with_object()
438 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
441 |> put_view(StatusView)
442 |> render("index.json", activities: activities, for: user, as: :activity)
445 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
446 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
447 true <- Visibility.visible_for_user?(activity, user) do
449 |> put_view(StatusView)
450 |> try_render("status.json", %{activity: activity, for: user})
454 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
455 with %Activity{} = activity <- Activity.get_by_id(id),
457 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
458 "blocking_user" => user,
460 "exclude_id" => activity.id
462 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
468 activities: grouped_activities[true] || [],
472 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
477 activities: grouped_activities[false] || [],
481 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
488 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
489 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
490 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
491 true <- Visibility.visible_for_user?(activity, user) do
493 |> put_view(StatusView)
494 |> try_render("poll.json", %{object: object, for: user})
496 error when is_nil(error) or error == false ->
497 render_error(conn, :not_found, "Record not found")
501 defp get_cached_vote_or_vote(user, object, choices) do
502 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
505 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
506 case CommonAPI.vote(user, object, choices) do
507 {:error, _message} = res -> {:ignore, res}
508 res -> {:commit, res}
515 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
516 with %Object{} = object <- Object.get_by_id(id),
517 true <- object.data["type"] == "Question",
518 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
519 true <- Visibility.visible_for_user?(activity, user),
520 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
522 |> put_view(StatusView)
523 |> try_render("poll.json", %{object: object, for: user})
526 render_error(conn, :not_found, "Record not found")
529 render_error(conn, :not_found, "Record not found")
533 |> put_status(:unprocessable_entity)
534 |> json(%{error: message})
538 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
539 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
541 |> add_link_headers(scheduled_activities)
542 |> put_view(ScheduledActivityView)
543 |> render("index.json", %{scheduled_activities: scheduled_activities})
547 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
548 with %ScheduledActivity{} = scheduled_activity <-
549 ScheduledActivity.get(user, scheduled_activity_id) do
551 |> put_view(ScheduledActivityView)
552 |> render("show.json", %{scheduled_activity: scheduled_activity})
554 _ -> {:error, :not_found}
558 def update_scheduled_status(
559 %{assigns: %{user: user}} = conn,
560 %{"id" => scheduled_activity_id} = params
562 with %ScheduledActivity{} = scheduled_activity <-
563 ScheduledActivity.get(user, scheduled_activity_id),
564 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
566 |> put_view(ScheduledActivityView)
567 |> render("show.json", %{scheduled_activity: scheduled_activity})
569 nil -> {:error, :not_found}
574 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
575 with %ScheduledActivity{} = scheduled_activity <-
576 ScheduledActivity.get(user, scheduled_activity_id),
577 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
579 |> put_view(ScheduledActivityView)
580 |> render("show.json", %{scheduled_activity: scheduled_activity})
582 nil -> {:error, :not_found}
587 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
590 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
592 scheduled_at = params["scheduled_at"]
594 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
595 with {:ok, scheduled_activity} <-
596 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
598 |> put_view(ScheduledActivityView)
599 |> render("show.json", %{scheduled_activity: scheduled_activity})
602 params = Map.drop(params, ["scheduled_at"])
604 case CommonAPI.post(user, params) do
607 |> put_status(:unprocessable_entity)
608 |> json(%{error: message})
612 |> put_view(StatusView)
613 |> try_render("status.json", %{
617 with_direct_conversation_id: true
623 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
624 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
627 _e -> render_error(conn, :forbidden, "Can't delete this post")
631 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
632 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
633 %Activity{} = announce <- Activity.normalize(announce.data) do
635 |> put_view(StatusView)
636 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
640 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
641 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
642 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
644 |> put_view(StatusView)
645 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
649 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
650 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
651 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
653 |> put_view(StatusView)
654 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
658 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
659 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
660 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
662 |> put_view(StatusView)
663 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
667 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
668 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
670 |> put_view(StatusView)
671 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
675 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
676 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
678 |> put_view(StatusView)
679 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
683 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
684 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
685 %User{} = user <- User.get_cached_by_nickname(user.nickname),
686 true <- Visibility.visible_for_user?(activity, user),
687 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
689 |> put_view(StatusView)
690 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
694 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
695 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
696 %User{} = user <- User.get_cached_by_nickname(user.nickname),
697 true <- Visibility.visible_for_user?(activity, user),
698 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
700 |> put_view(StatusView)
701 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
705 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
706 activity = Activity.get_by_id(id)
708 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
710 |> put_view(StatusView)
711 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
715 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
716 activity = Activity.get_by_id(id)
718 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
720 |> put_view(StatusView)
721 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
725 def notifications(%{assigns: %{user: user}} = conn, params) do
726 notifications = MastodonAPI.get_notifications(user, params)
729 |> add_link_headers(notifications)
730 |> put_view(NotificationView)
731 |> render("index.json", %{notifications: notifications, for: user})
734 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
735 with {:ok, notification} <- Notification.get(user, id) do
737 |> put_view(NotificationView)
738 |> render("show.json", %{notification: notification, for: user})
742 |> put_status(:forbidden)
743 |> json(%{"error" => reason})
747 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
748 Notification.clear(user)
752 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
753 with {:ok, _notif} <- Notification.dismiss(user, id) do
758 |> put_status(:forbidden)
759 |> json(%{"error" => reason})
763 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
764 Notification.destroy_multiple(user, ids)
768 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
770 q = from(u in User, where: u.id in ^id)
771 targets = Repo.all(q)
774 |> put_view(AccountView)
775 |> render("relationships.json", %{user: user, targets: targets})
778 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
779 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
781 def update_media(%{assigns: %{user: user}} = conn, data) do
782 with %Object{} = object <- Repo.get(Object, data["id"]),
783 true <- Object.authorize_mutation(object, user),
784 true <- is_binary(data["description"]),
785 description <- data["description"] do
786 new_data = %{object.data | "name" => description}
790 |> Object.change(%{data: new_data})
793 attachment_data = Map.put(new_data, "id", object.id)
796 |> put_view(StatusView)
797 |> render("attachment.json", %{attachment: attachment_data})
801 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
802 with {:ok, object} <-
805 actor: User.ap_id(user),
806 description: Map.get(data, "description")
808 attachment_data = Map.put(object.data, "id", object.id)
811 |> put_view(StatusView)
812 |> render("attachment.json", %{attachment: attachment_data})
816 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
817 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
818 %{} = attachment_data <- Map.put(object.data, "id", object.id),
819 %{type: type} = rendered <-
820 StatusView.render("attachment.json", %{attachment: attachment_data}) do
821 # Reject if not an image
822 if type == "image" do
824 # Save to the user's info
825 info_changeset = User.Info.mascot_update(user.info, rendered)
829 |> Changeset.change()
830 |> Changeset.put_embed(:info, info_changeset)
832 {:ok, _user} = User.update_and_set_cache(user_changeset)
837 render_error(conn, :unsupported_media_type, "mascots can only be images")
842 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
843 mascot = User.get_mascot(user)
849 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
850 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
851 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
852 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
853 q = from(u in User, where: u.ap_id in ^likes)
857 |> Enum.filter(&(not User.blocks?(user, &1)))
860 |> put_view(AccountView)
861 |> render("accounts.json", %{for: user, users: users, as: :user})
863 {:visible, false} -> {:error, :not_found}
868 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
869 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
870 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
871 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
872 q = from(u in User, where: u.ap_id in ^announces)
876 |> Enum.filter(&(not User.blocks?(user, &1)))
879 |> put_view(AccountView)
880 |> render("accounts.json", %{for: user, users: users, as: :user})
882 {:visible, false} -> {:error, :not_found}
887 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
888 local_only = params["local"] in [true, "True", "true", "1"]
891 [params["tag"], params["any"]]
895 |> Enum.map(&String.downcase(&1))
900 |> Enum.map(&String.downcase(&1))
905 |> Enum.map(&String.downcase(&1))
909 |> Map.put("type", "Create")
910 |> Map.put("local_only", local_only)
911 |> Map.put("blocking_user", user)
912 |> Map.put("muting_user", user)
913 |> Map.put("user", user)
914 |> Map.put("tag", tags)
915 |> Map.put("tag_all", tag_all)
916 |> Map.put("tag_reject", tag_reject)
917 |> ActivityPub.fetch_public_activities()
921 |> add_link_headers(activities, %{"local" => local_only})
922 |> put_view(StatusView)
923 |> render("index.json", %{activities: activities, for: user, as: :activity})
926 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
927 with %User{} = user <- User.get_cached_by_id(id),
928 followers <- MastodonAPI.get_followers(user, params) do
931 for_user && user.id == for_user.id -> followers
932 user.info.hide_followers -> []
937 |> add_link_headers(followers)
938 |> put_view(AccountView)
939 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
943 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
944 with %User{} = user <- User.get_cached_by_id(id),
945 followers <- MastodonAPI.get_friends(user, params) do
948 for_user && user.id == for_user.id -> followers
949 user.info.hide_follows -> []
954 |> add_link_headers(followers)
955 |> put_view(AccountView)
956 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
960 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
961 follow_requests = User.get_follow_requests(followed)
964 |> put_view(AccountView)
965 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
968 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
969 with %User{} = follower <- User.get_cached_by_id(id),
970 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
972 |> put_view(AccountView)
973 |> render("relationship.json", %{user: followed, target: follower})
977 |> put_status(:forbidden)
978 |> json(%{error: message})
982 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
983 with %User{} = follower <- User.get_cached_by_id(id),
984 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
986 |> put_view(AccountView)
987 |> render("relationship.json", %{user: followed, target: follower})
991 |> put_status(:forbidden)
992 |> json(%{error: message})
996 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
997 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
998 {_, true} <- {:followed, follower.id != followed.id},
999 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1001 |> put_view(AccountView)
1002 |> render("relationship.json", %{user: follower, target: followed})
1005 {:error, :not_found}
1007 {:error, message} ->
1009 |> put_status(:forbidden)
1010 |> json(%{error: message})
1014 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1015 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1016 {_, true} <- {:followed, follower.id != followed.id},
1017 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1019 |> put_view(AccountView)
1020 |> render("account.json", %{user: followed, for: follower})
1023 {:error, :not_found}
1025 {:error, message} ->
1027 |> put_status(:forbidden)
1028 |> json(%{error: message})
1032 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1033 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1034 {_, true} <- {:followed, follower.id != followed.id},
1035 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1037 |> put_view(AccountView)
1038 |> render("relationship.json", %{user: follower, target: followed})
1041 {:error, :not_found}
1048 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1050 if Map.has_key?(params, "notifications"),
1051 do: params["notifications"] in [true, "True", "true", "1"],
1054 with %User{} = muted <- User.get_cached_by_id(id),
1055 {:ok, muter} <- User.mute(muter, muted, notifications) do
1057 |> put_view(AccountView)
1058 |> render("relationship.json", %{user: muter, target: muted})
1060 {:error, message} ->
1062 |> put_status(:forbidden)
1063 |> json(%{error: message})
1067 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1068 with %User{} = muted <- User.get_cached_by_id(id),
1069 {:ok, muter} <- User.unmute(muter, muted) do
1071 |> put_view(AccountView)
1072 |> render("relationship.json", %{user: muter, target: muted})
1074 {:error, message} ->
1076 |> put_status(:forbidden)
1077 |> json(%{error: message})
1081 def mutes(%{assigns: %{user: user}} = conn, _) do
1082 with muted_accounts <- User.muted_users(user) do
1083 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1088 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1089 with %User{} = blocked <- User.get_cached_by_id(id),
1090 {:ok, blocker} <- User.block(blocker, blocked),
1091 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1093 |> put_view(AccountView)
1094 |> render("relationship.json", %{user: blocker, target: blocked})
1096 {:error, message} ->
1098 |> put_status(:forbidden)
1099 |> json(%{error: message})
1103 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1104 with %User{} = blocked <- User.get_cached_by_id(id),
1105 {:ok, blocker} <- User.unblock(blocker, blocked),
1106 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1108 |> put_view(AccountView)
1109 |> render("relationship.json", %{user: blocker, target: blocked})
1111 {:error, message} ->
1113 |> put_status(:forbidden)
1114 |> json(%{error: message})
1118 def blocks(%{assigns: %{user: user}} = conn, _) do
1119 with blocked_accounts <- User.blocked_users(user) do
1120 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1125 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1126 json(conn, info.domain_blocks || [])
1129 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1130 User.block_domain(blocker, domain)
1134 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1135 User.unblock_domain(blocker, domain)
1139 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1140 with %User{} = subscription_target <- User.get_cached_by_id(id),
1141 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1143 |> put_view(AccountView)
1144 |> render("relationship.json", %{user: user, target: subscription_target})
1146 {:error, message} ->
1148 |> put_status(:forbidden)
1149 |> json(%{error: message})
1153 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1154 with %User{} = subscription_target <- User.get_cached_by_id(id),
1155 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1157 |> put_view(AccountView)
1158 |> render("relationship.json", %{user: user, target: subscription_target})
1160 {:error, message} ->
1162 |> put_status(:forbidden)
1163 |> json(%{error: message})
1167 def favourites(%{assigns: %{user: user}} = conn, params) do
1170 |> Map.put("type", "Create")
1171 |> Map.put("favorited_by", user.ap_id)
1172 |> Map.put("blocking_user", user)
1175 ActivityPub.fetch_activities([], params)
1179 |> add_link_headers(activities)
1180 |> put_view(StatusView)
1181 |> render("index.json", %{activities: activities, for: user, as: :activity})
1184 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1185 with %User{} = user <- User.get_by_id(id),
1186 false <- user.info.hide_favorites do
1189 |> Map.put("type", "Create")
1190 |> Map.put("favorited_by", user.ap_id)
1191 |> Map.put("blocking_user", for_user)
1195 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1197 [Pleroma.Constants.as_public()]
1202 |> ActivityPub.fetch_activities(params)
1206 |> add_link_headers(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)
1228 |> put_view(StatusView)
1229 |> render("index.json", %{activities: activities, for: user, as: :activity})
1232 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1233 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1234 res = ListView.render("lists.json", lists: lists)
1238 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1239 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1242 |> Map.put("type", "Create")
1243 |> Map.put("blocking_user", user)
1244 |> Map.put("user", user)
1245 |> Map.put("muting_user", user)
1247 # we must filter the following list for the user to avoid leaking statuses the user
1248 # does not actually have permission to see (for more info, peruse security issue #270).
1251 |> Enum.filter(fn x -> x in user.following end)
1252 |> ActivityPub.fetch_activities_bounded(following, params)
1256 |> put_view(StatusView)
1257 |> render("index.json", %{activities: activities, for: user, as: :activity})
1259 _e -> render_error(conn, :forbidden, "Error.")
1263 def index(%{assigns: %{user: user}} = conn, _params) do
1264 token = get_session(conn, :oauth_token)
1267 mastodon_emoji = mastodonized_emoji()
1269 limit = Config.get([:instance, :limit])
1272 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1277 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1278 access_token: token,
1280 domain: Pleroma.Web.Endpoint.host(),
1283 unfollow_modal: false,
1286 auto_play_gif: false,
1287 display_sensitive_media: false,
1288 reduce_motion: false,
1289 max_toot_chars: limit,
1290 mascot: User.get_mascot(user)["url"]
1292 poll_limits: Config.get([:instance, :poll_limits]),
1294 delete_others_notice: present?(user.info.is_moderator),
1295 admin: present?(user.info.is_admin)
1299 default_privacy: user.info.default_scope,
1300 default_sensitive: false,
1301 allow_content_types: Config.get([:instance, :allowed_post_formats])
1303 media_attachments: %{
1304 accept_content_types: [
1320 user.info.settings ||
1350 push_subscription: nil,
1352 custom_emojis: mastodon_emoji,
1358 |> put_layout(false)
1359 |> put_view(MastodonView)
1360 |> render("index.html", %{initial_state: initial_state})
1363 |> put_session(:return_to, conn.request_path)
1364 |> redirect(to: "/web/login")
1368 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1369 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1371 with changeset <- Changeset.change(user),
1372 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1373 {:ok, _user} <- User.update_and_set_cache(changeset) do
1378 |> put_status(:internal_server_error)
1379 |> json(%{error: inspect(e)})
1383 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1384 redirect(conn, to: local_mastodon_root_path(conn))
1387 @doc "Local Mastodon FE login init action"
1388 def login(conn, %{"code" => auth_token}) do
1389 with {:ok, app} <- get_or_make_app(),
1390 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1391 {:ok, token} <- Token.exchange_token(app, auth) do
1393 |> put_session(:oauth_token, token.token)
1394 |> redirect(to: local_mastodon_root_path(conn))
1398 @doc "Local Mastodon FE callback action"
1399 def login(conn, _) do
1400 with {:ok, app} <- get_or_make_app() do
1405 response_type: "code",
1406 client_id: app.client_id,
1408 scope: Enum.join(app.scopes, " ")
1411 redirect(conn, to: path)
1415 defp local_mastodon_root_path(conn) do
1416 case get_session(conn, :return_to) do
1418 mastodon_api_path(conn, :index, ["getting-started"])
1421 delete_session(conn, :return_to)
1426 defp get_or_make_app do
1427 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1428 scopes = ["read", "write", "follow", "push"]
1430 with %App{} = app <- Repo.get_by(App, find_attrs) do
1432 if app.scopes == scopes do
1436 |> Changeset.change(%{scopes: scopes})
1444 App.register_changeset(
1446 Map.put(find_attrs, :scopes, scopes)
1453 def logout(conn, _) do
1456 |> redirect(to: "/")
1459 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1460 Logger.debug("Unimplemented, returning unmodified relationship")
1462 with %User{} = target <- User.get_cached_by_id(id) do
1464 |> put_view(AccountView)
1465 |> render("relationship.json", %{user: user, target: target})
1469 def empty_array(conn, _) do
1470 Logger.debug("Unimplemented, returning an empty array")
1474 def empty_object(conn, _) do
1475 Logger.debug("Unimplemented, returning an empty object")
1479 def get_filters(%{assigns: %{user: user}} = conn, _) do
1480 filters = Filter.get_filters(user)
1481 res = FilterView.render("filters.json", filters: filters)
1486 %{assigns: %{user: user}} = conn,
1487 %{"phrase" => phrase, "context" => context} = params
1493 hide: Map.get(params, "irreversible", false),
1494 whole_word: Map.get(params, "boolean", true)
1498 {:ok, response} = Filter.create(query)
1499 res = FilterView.render("filter.json", filter: response)
1503 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1504 filter = Filter.get(filter_id, user)
1505 res = FilterView.render("filter.json", filter: filter)
1510 %{assigns: %{user: user}} = conn,
1511 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1515 filter_id: filter_id,
1518 hide: Map.get(params, "irreversible", nil),
1519 whole_word: Map.get(params, "boolean", true)
1523 {:ok, response} = Filter.update(query)
1524 res = FilterView.render("filter.json", filter: response)
1528 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1531 filter_id: filter_id
1534 {:ok, _} = Filter.delete(query)
1538 def suggestions(%{assigns: %{user: user}} = conn, _) do
1539 suggestions = Config.get(:suggestions)
1541 if Keyword.get(suggestions, :enabled, false) do
1542 api = Keyword.get(suggestions, :third_party_engine, "")
1543 timeout = Keyword.get(suggestions, :timeout, 5000)
1544 limit = Keyword.get(suggestions, :limit, 23)
1546 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1548 user = user.nickname
1552 |> String.replace("{{host}}", host)
1553 |> String.replace("{{user}}", user)
1555 with {:ok, %{status: 200, body: body}} <-
1556 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1557 {:ok, data} <- Jason.decode(body) do
1560 |> Enum.slice(0, limit)
1563 |> Map.put("id", fetch_suggestion_id(x))
1564 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1565 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1571 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1578 defp fetch_suggestion_id(attrs) do
1579 case User.get_or_fetch(attrs["acct"]) do
1580 {:ok, %User{id: id}} -> id
1585 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1586 with %Activity{} = activity <- Activity.get_by_id(status_id),
1587 true <- Visibility.visible_for_user?(activity, user) do
1591 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1601 def reports(%{assigns: %{user: user}} = conn, params) do
1602 case CommonAPI.report(user, params) do
1605 |> put_view(ReportView)
1606 |> try_render("report.json", %{activity: activity})
1610 |> put_status(:bad_request)
1611 |> json(%{error: err})
1615 def account_register(
1616 %{assigns: %{app: app}} = conn,
1617 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1625 "captcha_answer_data",
1629 |> Map.put("nickname", nickname)
1630 |> Map.put("fullname", params["fullname"] || nickname)
1631 |> Map.put("bio", params["bio"] || "")
1632 |> Map.put("confirm", params["password"])
1634 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1635 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1637 token_type: "Bearer",
1638 access_token: token.token,
1640 created_at: Token.Utils.format_created_at(token)
1645 |> put_status(:bad_request)
1650 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1651 render_error(conn, :bad_request, "Missing parameters")
1654 def account_register(conn, _) do
1655 render_error(conn, :forbidden, "Invalid credentials")
1658 def conversations(%{assigns: %{user: user}} = conn, params) do
1659 participations = Participation.for_user_with_last_activity_id(user, params)
1662 Enum.map(participations, fn participation ->
1663 ConversationView.render("participation.json", %{participation: participation, for: user})
1667 |> add_link_headers(participations)
1668 |> json(conversations)
1671 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1672 with %Participation{} = participation <-
1673 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1674 {:ok, participation} <- Participation.mark_as_read(participation) do
1675 participation_view =
1676 ConversationView.render("participation.json", %{participation: participation, for: user})
1679 |> json(participation_view)
1683 def password_reset(conn, params) do
1684 nickname_or_email = params["email"] || params["nickname"]
1686 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1688 |> put_status(:no_content)
1691 {:error, "unknown user"} ->
1692 send_resp(conn, :not_found, "")
1695 send_resp(conn, :bad_request, "")
1699 def account_confirmation_resend(conn, params) do
1700 nickname_or_email = params["email"] || params["nickname"]
1702 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1703 {:ok, _} <- User.try_send_confirmation_email(user) do
1705 |> json_response(:no_content, "")
1709 def try_render(conn, target, params)
1710 when is_binary(target) do
1711 case render(conn, target, params) do
1712 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1717 def try_render(conn, _, _) do
1718 render_error(conn, :not_implemented, "Can't display this activity")
1721 defp present?(nil), do: false
1722 defp present?(false), do: false
1723 defp present?(_), do: true