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.RichMedia
48 alias Pleroma.Web.TwitterAPI.TwitterAPI
50 alias Pleroma.Web.ControllerHelper
54 require Pleroma.Constants
56 @rate_limited_relations_actions ~w(follow unfollow)a
58 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
59 post_status delete_status)a
63 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
64 when action in ~w(reblog_status unreblog_status)a
69 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
70 when action in ~w(fav_status unfav_status)a
75 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
78 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
79 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
80 plug(RateLimiter, :app_account_creation when action == :account_register)
81 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
82 plug(RateLimiter, :password_reset when action == :password_reset)
83 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
85 @local_mastodon_name "Mastodon-Local"
87 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
89 def create_app(conn, params) do
90 scopes = Scopes.fetch_scopes(params, ["read"])
94 |> Map.drop(["scope", "scopes"])
95 |> Map.put("scopes", scopes)
97 with cs <- App.register_changeset(%App{}, app_attrs),
98 false <- cs.changes[:client_name] == @local_mastodon_name,
99 {:ok, app} <- Repo.insert(cs) do
102 |> render("show.json", %{app: app})
111 value_function \\ fn x -> {:ok, x} end
113 if Map.has_key?(params, params_field) do
114 case value_function.(params[params_field]) do
115 {:ok, new_value} -> Map.put(map, map_field, new_value)
123 def update_credentials(%{assigns: %{user: user}} = conn, params) do
128 |> add_if_present(params, "display_name", :name)
129 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
130 |> add_if_present(params, "avatar", :avatar, fn value ->
131 with %Plug.Upload{} <- value,
132 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
139 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
143 |> Map.get(:emoji, [])
144 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
151 :hide_followers_count,
157 :skip_thread_containment
159 |> Enum.reduce(%{}, fn key, acc ->
160 add_if_present(acc, params, to_string(key), key, fn value ->
161 {:ok, ControllerHelper.truthy_param?(value)}
164 |> add_if_present(params, "default_scope", :default_scope)
165 |> add_if_present(params, "fields", :fields, fn fields ->
166 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
170 |> add_if_present(params, "fields", :raw_fields)
171 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
172 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
174 |> add_if_present(params, "header", :banner, fn value ->
175 with %Plug.Upload{} <- value,
176 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
182 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
183 with %Plug.Upload{} <- value,
184 {:ok, object} <- ActivityPub.upload(value, type: :background) do
190 |> Map.put(:emoji, user_info_emojis)
192 info_cng = User.Info.profile_update(user.info, info_params)
194 with changeset <- User.update_changeset(user, user_params),
195 changeset <- Changeset.put_embed(changeset, :info, info_cng),
196 {:ok, user} <- User.update_and_set_cache(changeset) do
197 if original_user != user do
198 CommonAPI.update(user)
203 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
206 _e -> render_error(conn, :forbidden, "Invalid request")
210 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
211 change = Changeset.change(user, %{avatar: nil})
212 {:ok, user} = User.update_and_set_cache(change)
213 CommonAPI.update(user)
215 json(conn, %{url: nil})
218 def update_avatar(%{assigns: %{user: user}} = conn, params) do
219 {:ok, object} = ActivityPub.upload(params, type: :avatar)
220 change = Changeset.change(user, %{avatar: object.data})
221 {:ok, user} = User.update_and_set_cache(change)
222 CommonAPI.update(user)
223 %{"url" => [%{"href" => href} | _]} = object.data
225 json(conn, %{url: href})
228 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
229 with new_info <- %{"banner" => %{}},
230 info_cng <- User.Info.profile_update(user.info, new_info),
231 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
232 {:ok, user} <- User.update_and_set_cache(changeset) do
233 CommonAPI.update(user)
235 json(conn, %{url: nil})
239 def update_banner(%{assigns: %{user: user}} = conn, params) do
240 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
241 new_info <- %{"banner" => object.data},
242 info_cng <- User.Info.profile_update(user.info, new_info),
243 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
244 {:ok, user} <- User.update_and_set_cache(changeset) do
245 CommonAPI.update(user)
246 %{"url" => [%{"href" => href} | _]} = object.data
248 json(conn, %{url: href})
252 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
253 with new_info <- %{"background" => %{}},
254 info_cng <- User.Info.profile_update(user.info, new_info),
255 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
256 {:ok, _user} <- User.update_and_set_cache(changeset) do
257 json(conn, %{url: nil})
261 def update_background(%{assigns: %{user: user}} = conn, params) do
262 with {:ok, object} <- ActivityPub.upload(params, type: :background),
263 new_info <- %{"background" => object.data},
264 info_cng <- User.Info.profile_update(user.info, new_info),
265 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
266 {:ok, _user} <- User.update_and_set_cache(changeset) do
267 %{"url" => [%{"href" => href} | _]} = object.data
269 json(conn, %{url: href})
273 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
274 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
277 AccountView.render("account.json", %{
280 with_pleroma_settings: true,
281 with_chat_token: chat_token
287 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
288 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
291 |> render("short.json", %{app: app})
295 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
296 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
297 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
298 account = AccountView.render("account.json", %{user: user, for: for_user})
301 _e -> render_error(conn, :not_found, "Can't find user")
305 @mastodon_api_level "2.7.2"
307 def masto_instance(conn, _params) do
308 instance = Config.get(:instance)
312 title: Keyword.get(instance, :name),
313 description: Keyword.get(instance, :description),
314 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
315 email: Keyword.get(instance, :email),
317 streaming_api: Pleroma.Web.Endpoint.websocket_url()
319 stats: Stats.get_stats(),
320 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
322 registrations: Pleroma.Config.get([:instance, :registrations_open]),
323 # Extra (not present in Mastodon):
324 max_toot_chars: Keyword.get(instance, :limit),
325 poll_limits: Keyword.get(instance, :poll_limits)
331 def peers(conn, _params) do
332 json(conn, Stats.get_peers())
335 defp mastodonized_emoji do
336 Pleroma.Emoji.get_all()
337 |> Enum.map(fn {shortcode, relative_url, tags} ->
338 url = to_string(URI.merge(Web.base_url(), relative_url))
341 "shortcode" => shortcode,
343 "visible_in_picker" => true,
346 # Assuming that a comma is authorized in the category name
347 "category" => (tags -- ["Custom"]) |> Enum.join(",")
352 def custom_emojis(conn, _params) do
353 mastodon_emoji = mastodonized_emoji()
354 json(conn, mastodon_emoji)
357 def home_timeline(%{assigns: %{user: user}} = conn, params) do
360 |> Map.put("type", ["Create", "Announce"])
361 |> Map.put("blocking_user", user)
362 |> Map.put("muting_user", user)
363 |> Map.put("user", user)
366 [user.ap_id | user.following]
367 |> ActivityPub.fetch_activities(params)
371 |> add_link_headers(activities)
372 |> put_view(StatusView)
373 |> render("index.json", %{activities: activities, for: user, as: :activity})
376 def public_timeline(%{assigns: %{user: user}} = conn, params) do
377 local_only = params["local"] in [true, "True", "true", "1"]
381 |> Map.put("type", ["Create", "Announce"])
382 |> Map.put("local_only", local_only)
383 |> Map.put("blocking_user", user)
384 |> Map.put("muting_user", user)
385 |> Map.put("user", user)
386 |> ActivityPub.fetch_public_activities()
390 |> add_link_headers(activities, %{"local" => local_only})
391 |> put_view(StatusView)
392 |> render("index.json", %{activities: activities, for: user, as: :activity})
395 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
396 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
399 |> Map.put("tag", params["tagged"])
401 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
404 |> add_link_headers(activities)
405 |> put_view(StatusView)
406 |> render("index.json", %{
407 activities: activities,
414 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
417 |> Map.put("type", "Create")
418 |> Map.put("blocking_user", user)
419 |> Map.put("user", user)
420 |> Map.put(:visibility, "direct")
424 |> ActivityPub.fetch_activities_query(params)
425 |> Pagination.fetch_paginated(params)
428 |> add_link_headers(activities)
429 |> put_view(StatusView)
430 |> render("index.json", %{activities: activities, for: user, as: :activity})
433 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
439 |> Activity.all_by_ids_with_object()
440 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
443 |> put_view(StatusView)
444 |> render("index.json", activities: activities, for: user, as: :activity)
447 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
448 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
449 true <- Visibility.visible_for_user?(activity, user) do
451 |> put_view(StatusView)
452 |> try_render("status.json", %{activity: activity, for: user})
456 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
457 with %Activity{} = activity <- Activity.get_by_id(id),
459 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
460 "blocking_user" => user,
462 "exclude_id" => activity.id
464 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
467 StatusView.render("index.json",
469 activities: grouped_activities[true] || [],
473 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
475 StatusView.render("index.json",
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
769 targets = User.get_all_by_ids(List.wrap(id))
772 |> put_view(AccountView)
773 |> render("relationships.json", %{user: user, targets: targets})
776 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
777 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
780 %{assigns: %{user: user}} = conn,
781 %{"id" => id, "description" => description} = _
783 when is_binary(description) do
784 with %Object{} = object <- Repo.get(Object, id),
785 true <- Object.authorize_mutation(object, user),
786 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
787 attachment_data = Map.put(data, "id", object.id)
790 |> put_view(StatusView)
791 |> render("attachment.json", %{attachment: attachment_data})
795 def update_media(_conn, _data), do: {:error, :bad_request}
797 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
798 with {:ok, object} <-
801 actor: User.ap_id(user),
802 description: Map.get(data, "description")
804 attachment_data = Map.put(object.data, "id", object.id)
807 |> put_view(StatusView)
808 |> render("attachment.json", %{attachment: attachment_data})
812 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
813 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
814 %{} = attachment_data <- Map.put(object.data, "id", object.id),
815 %{type: "image"} = rendered <-
816 StatusView.render("attachment.json", %{attachment: attachment_data}),
817 {:ok, _user} = User.update_mascot(user, rendered) do
820 %{type: _type} = _ ->
821 render_error(conn, :unsupported_media_type, "mascots can only be images")
828 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
829 mascot = User.get_mascot(user)
834 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
835 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
836 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
837 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
838 q = from(u in User, where: u.ap_id in ^likes)
842 |> Enum.filter(&(not User.blocks?(user, &1)))
845 |> put_view(AccountView)
846 |> render("accounts.json", %{for: user, users: users, as: :user})
848 {:visible, false} -> {:error, :not_found}
853 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
854 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
855 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
856 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
857 q = from(u in User, where: u.ap_id in ^announces)
861 |> Enum.filter(&(not User.blocks?(user, &1)))
864 |> put_view(AccountView)
865 |> render("accounts.json", %{for: user, users: users, as: :user})
867 {:visible, false} -> {:error, :not_found}
872 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
873 local_only = params["local"] in [true, "True", "true", "1"]
876 [params["tag"], params["any"]]
880 |> Enum.map(&String.downcase(&1))
885 |> Enum.map(&String.downcase(&1))
890 |> Enum.map(&String.downcase(&1))
894 |> Map.put("type", "Create")
895 |> Map.put("local_only", local_only)
896 |> Map.put("blocking_user", user)
897 |> Map.put("muting_user", user)
898 |> Map.put("user", user)
899 |> Map.put("tag", tags)
900 |> Map.put("tag_all", tag_all)
901 |> Map.put("tag_reject", tag_reject)
902 |> ActivityPub.fetch_public_activities()
906 |> add_link_headers(activities, %{"local" => local_only})
907 |> put_view(StatusView)
908 |> render("index.json", %{activities: activities, for: user, as: :activity})
911 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
912 with %User{} = user <- User.get_cached_by_id(id),
913 followers <- MastodonAPI.get_followers(user, params) do
916 for_user && user.id == for_user.id -> followers
917 user.info.hide_followers -> []
922 |> add_link_headers(followers)
923 |> put_view(AccountView)
924 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
928 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
929 with %User{} = user <- User.get_cached_by_id(id),
930 followers <- MastodonAPI.get_friends(user, params) do
933 for_user && user.id == for_user.id -> followers
934 user.info.hide_follows -> []
939 |> add_link_headers(followers)
940 |> put_view(AccountView)
941 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
945 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
946 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
948 |> put_view(AccountView)
949 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
953 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
954 with %User{} = follower <- User.get_cached_by_id(id),
955 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
957 |> put_view(AccountView)
958 |> render("relationship.json", %{user: followed, target: follower})
962 |> put_status(:forbidden)
963 |> json(%{error: message})
967 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
968 with %User{} = follower <- User.get_cached_by_id(id),
969 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
971 |> put_view(AccountView)
972 |> render("relationship.json", %{user: followed, target: follower})
976 |> put_status(:forbidden)
977 |> json(%{error: message})
981 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
982 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
983 {_, true} <- {:followed, follower.id != followed.id},
984 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
986 |> put_view(AccountView)
987 |> render("relationship.json", %{user: follower, target: followed})
994 |> put_status(:forbidden)
995 |> json(%{error: message})
999 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1000 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1001 {_, true} <- {:followed, follower.id != followed.id},
1002 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1004 |> put_view(AccountView)
1005 |> render("account.json", %{user: followed, for: follower})
1008 {:error, :not_found}
1010 {:error, message} ->
1012 |> put_status(:forbidden)
1013 |> json(%{error: message})
1017 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1018 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1019 {_, true} <- {:followed, follower.id != followed.id},
1020 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1022 |> put_view(AccountView)
1023 |> render("relationship.json", %{user: follower, target: followed})
1026 {:error, :not_found}
1033 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1035 if Map.has_key?(params, "notifications"),
1036 do: params["notifications"] in [true, "True", "true", "1"],
1039 with %User{} = muted <- User.get_cached_by_id(id),
1040 {:ok, muter} <- User.mute(muter, muted, notifications) do
1042 |> put_view(AccountView)
1043 |> render("relationship.json", %{user: muter, target: muted})
1045 {:error, message} ->
1047 |> put_status(:forbidden)
1048 |> json(%{error: message})
1052 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1053 with %User{} = muted <- User.get_cached_by_id(id),
1054 {:ok, muter} <- User.unmute(muter, muted) do
1056 |> put_view(AccountView)
1057 |> render("relationship.json", %{user: muter, target: muted})
1059 {:error, message} ->
1061 |> put_status(:forbidden)
1062 |> json(%{error: message})
1066 def mutes(%{assigns: %{user: user}} = conn, _) do
1067 with muted_accounts <- User.muted_users(user) do
1068 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1073 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1074 with %User{} = blocked <- User.get_cached_by_id(id),
1075 {:ok, blocker} <- User.block(blocker, blocked),
1076 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1078 |> put_view(AccountView)
1079 |> render("relationship.json", %{user: blocker, target: blocked})
1081 {:error, message} ->
1083 |> put_status(:forbidden)
1084 |> json(%{error: message})
1088 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1089 with %User{} = blocked <- User.get_cached_by_id(id),
1090 {:ok, blocker} <- User.unblock(blocker, blocked),
1091 {:ok, _activity} <- ActivityPub.unblock(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 blocks(%{assigns: %{user: user}} = conn, _) do
1104 with blocked_accounts <- User.blocked_users(user) do
1105 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1110 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1111 json(conn, info.domain_blocks || [])
1114 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1115 User.block_domain(blocker, domain)
1119 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1120 User.unblock_domain(blocker, domain)
1124 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1125 with %User{} = subscription_target <- User.get_cached_by_id(id),
1126 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1128 |> put_view(AccountView)
1129 |> render("relationship.json", %{user: user, target: subscription_target})
1131 nil -> {:error, :not_found}
1136 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1137 with %User{} = subscription_target <- User.get_cached_by_id(id),
1138 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1140 |> put_view(AccountView)
1141 |> render("relationship.json", %{user: user, target: subscription_target})
1143 nil -> {:error, :not_found}
1148 def favourites(%{assigns: %{user: user}} = conn, params) do
1151 |> Map.put("type", "Create")
1152 |> Map.put("favorited_by", user.ap_id)
1153 |> Map.put("blocking_user", user)
1156 ActivityPub.fetch_activities([], params)
1160 |> add_link_headers(activities)
1161 |> put_view(StatusView)
1162 |> render("index.json", %{activities: activities, for: user, as: :activity})
1165 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1166 with %User{} = user <- User.get_by_id(id),
1167 false <- user.info.hide_favorites do
1170 |> Map.put("type", "Create")
1171 |> Map.put("favorited_by", user.ap_id)
1172 |> Map.put("blocking_user", for_user)
1176 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1178 [Pleroma.Constants.as_public()]
1183 |> ActivityPub.fetch_activities(params)
1187 |> add_link_headers(activities)
1188 |> put_view(StatusView)
1189 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1191 nil -> {:error, :not_found}
1192 true -> render_error(conn, :forbidden, "Can't get favorites")
1196 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1197 user = User.get_cached_by_id(user.id)
1200 Bookmark.for_user_query(user.id)
1201 |> Pagination.fetch_paginated(params)
1205 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1208 |> add_link_headers(bookmarks)
1209 |> put_view(StatusView)
1210 |> render("index.json", %{activities: activities, for: user, as: :activity})
1213 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1214 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1217 |> put_view(ListView)
1218 |> render("index.json", %{lists: lists})
1221 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1222 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1225 |> Map.put("type", "Create")
1226 |> Map.put("blocking_user", user)
1227 |> Map.put("user", user)
1228 |> Map.put("muting_user", user)
1230 # we must filter the following list for the user to avoid leaking statuses the user
1231 # does not actually have permission to see (for more info, peruse security issue #270).
1234 |> Enum.filter(fn x -> x in user.following end)
1235 |> ActivityPub.fetch_activities_bounded(following, params)
1239 |> put_view(StatusView)
1240 |> render("index.json", %{activities: activities, for: user, as: :activity})
1242 _e -> render_error(conn, :forbidden, "Error.")
1246 def index(%{assigns: %{user: user}} = conn, _params) do
1247 token = get_session(conn, :oauth_token)
1250 mastodon_emoji = mastodonized_emoji()
1252 limit = Config.get([:instance, :limit])
1255 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1260 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1261 access_token: token,
1263 domain: Pleroma.Web.Endpoint.host(),
1266 unfollow_modal: false,
1269 auto_play_gif: false,
1270 display_sensitive_media: false,
1271 reduce_motion: false,
1272 max_toot_chars: limit,
1273 mascot: User.get_mascot(user)["url"]
1275 poll_limits: Config.get([:instance, :poll_limits]),
1277 delete_others_notice: present?(user.info.is_moderator),
1278 admin: present?(user.info.is_admin)
1282 default_privacy: user.info.default_scope,
1283 default_sensitive: false,
1284 allow_content_types: Config.get([:instance, :allowed_post_formats])
1286 media_attachments: %{
1287 accept_content_types: [
1303 user.info.settings ||
1333 push_subscription: nil,
1335 custom_emojis: mastodon_emoji,
1341 |> put_layout(false)
1342 |> put_view(MastodonView)
1343 |> render("index.html", %{initial_state: initial_state})
1346 |> put_session(:return_to, conn.request_path)
1347 |> redirect(to: "/web/login")
1351 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1352 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1354 with changeset <- Changeset.change(user),
1355 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1356 {:ok, _user} <- User.update_and_set_cache(changeset) do
1361 |> put_status(:internal_server_error)
1362 |> json(%{error: inspect(e)})
1366 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1367 redirect(conn, to: local_mastodon_root_path(conn))
1370 @doc "Local Mastodon FE login init action"
1371 def login(conn, %{"code" => auth_token}) do
1372 with {:ok, app} <- get_or_make_app(),
1373 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
1374 {:ok, token} <- Token.exchange_token(app, auth) do
1376 |> put_session(:oauth_token, token.token)
1377 |> redirect(to: local_mastodon_root_path(conn))
1381 @doc "Local Mastodon FE callback action"
1382 def login(conn, _) do
1383 with {:ok, app} <- get_or_make_app() do
1385 o_auth_path(conn, :authorize,
1386 response_type: "code",
1387 client_id: app.client_id,
1389 scope: Enum.join(app.scopes, " ")
1392 redirect(conn, to: path)
1396 defp local_mastodon_root_path(conn) do
1397 case get_session(conn, :return_to) do
1399 mastodon_api_path(conn, :index, ["getting-started"])
1402 delete_session(conn, :return_to)
1407 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
1408 defp get_or_make_app do
1410 %{client_name: @local_mastodon_name, redirect_uris: "."},
1411 ["read", "write", "follow", "push"]
1415 def logout(conn, _) do
1418 |> redirect(to: "/")
1421 # Stubs for unimplemented mastodon api
1423 def empty_array(conn, _) do
1424 Logger.debug("Unimplemented, returning an empty array")
1428 def get_filters(%{assigns: %{user: user}} = conn, _) do
1429 filters = Filter.get_filters(user)
1430 res = FilterView.render("filters.json", filters: filters)
1435 %{assigns: %{user: user}} = conn,
1436 %{"phrase" => phrase, "context" => context} = params
1442 hide: Map.get(params, "irreversible", false),
1443 whole_word: Map.get(params, "boolean", true)
1447 {:ok, response} = Filter.create(query)
1448 res = FilterView.render("filter.json", filter: response)
1452 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1453 filter = Filter.get(filter_id, user)
1454 res = FilterView.render("filter.json", filter: filter)
1459 %{assigns: %{user: user}} = conn,
1460 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1464 filter_id: filter_id,
1467 hide: Map.get(params, "irreversible", nil),
1468 whole_word: Map.get(params, "boolean", true)
1472 {:ok, response} = Filter.update(query)
1473 res = FilterView.render("filter.json", filter: response)
1477 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1480 filter_id: filter_id
1483 {:ok, _} = Filter.delete(query)
1487 def suggestions(%{assigns: %{user: user}} = conn, _) do
1488 suggestions = Config.get(:suggestions)
1490 if Keyword.get(suggestions, :enabled, false) do
1491 api = Keyword.get(suggestions, :third_party_engine, "")
1492 timeout = Keyword.get(suggestions, :timeout, 5000)
1493 limit = Keyword.get(suggestions, :limit, 23)
1495 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1497 user = user.nickname
1501 |> String.replace("{{host}}", host)
1502 |> String.replace("{{user}}", user)
1504 with {:ok, %{status: 200, body: body}} <-
1505 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1506 {:ok, data} <- Jason.decode(body) do
1509 |> Enum.slice(0, limit)
1512 |> Map.put("id", fetch_suggestion_id(x))
1513 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1514 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1520 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1527 defp fetch_suggestion_id(attrs) do
1528 case User.get_or_fetch(attrs["acct"]) do
1529 {:ok, %User{id: id}} -> id
1534 def status_card(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1535 with %Activity{} = activity <- Activity.get_by_id(id),
1536 true <- Visibility.visible_for_user?(activity, user) do
1537 data = RichMedia.Helpers.fetch_data_for_activity(activity)
1540 |> put_view(StatusView)
1541 |> render("card.json", data)
1543 _e -> {:error, :not_found}
1547 def reports(%{assigns: %{user: user}} = conn, params) do
1548 case CommonAPI.report(user, params) do
1551 |> put_view(ReportView)
1552 |> try_render("report.json", %{activity: activity})
1556 |> put_status(:bad_request)
1557 |> json(%{error: err})
1561 def account_register(
1562 %{assigns: %{app: app}} = conn,
1563 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1571 "captcha_answer_data",
1575 |> Map.put("nickname", nickname)
1576 |> Map.put("fullname", params["fullname"] || nickname)
1577 |> Map.put("bio", params["bio"] || "")
1578 |> Map.put("confirm", params["password"])
1580 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1581 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1583 token_type: "Bearer",
1584 access_token: token.token,
1586 created_at: Token.Utils.format_created_at(token)
1591 |> put_status(:bad_request)
1596 def account_register(%{assigns: %{app: _app}} = conn, _) do
1597 render_error(conn, :bad_request, "Missing parameters")
1600 def account_register(conn, _) do
1601 render_error(conn, :forbidden, "Invalid credentials")
1604 def conversations(%{assigns: %{user: user}} = conn, params) do
1605 participations = Participation.for_user_with_last_activity_id(user, params)
1608 Enum.map(participations, fn participation ->
1609 ConversationView.render("participation.json", %{participation: participation, for: user})
1613 |> add_link_headers(participations)
1614 |> json(conversations)
1617 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1618 with %Participation{} = participation <-
1619 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1620 {:ok, participation} <- Participation.mark_as_read(participation) do
1621 participation_view =
1622 ConversationView.render("participation.json", %{participation: participation, for: user})
1625 |> json(participation_view)
1629 def password_reset(conn, params) do
1630 nickname_or_email = params["email"] || params["nickname"]
1632 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1634 |> put_status(:no_content)
1637 {:error, "unknown user"} ->
1638 send_resp(conn, :not_found, "")
1641 send_resp(conn, :bad_request, "")
1645 def account_confirmation_resend(conn, params) do
1646 nickname_or_email = params["email"] || params["nickname"]
1648 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1649 {:ok, _} <- User.try_send_confirmation_email(user) do
1651 |> json_response(:no_content, "")
1655 defp try_render(conn, target, params)
1656 when is_binary(target) do
1657 case render(conn, target, params) do
1658 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1663 defp try_render(conn, _, _) do
1664 render_error(conn, :not_implemented, "Can't display this activity")
1667 defp present?(nil), do: false
1668 defp present?(false), do: false
1669 defp present?(_), do: true