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, truthy_param?: 1]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
20 alias Pleroma.Pagination
21 alias Pleroma.Plugs.RateLimiter
23 alias Pleroma.ScheduledActivity
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Visibility
29 alias Pleroma.Web.CommonAPI
30 alias Pleroma.Web.MastodonAPI.AccountView
31 alias Pleroma.Web.MastodonAPI.AppView
32 alias Pleroma.Web.MastodonAPI.ConversationView
33 alias Pleroma.Web.MastodonAPI.FilterView
34 alias Pleroma.Web.MastodonAPI.ListView
35 alias Pleroma.Web.MastodonAPI.MastodonAPI
36 alias Pleroma.Web.MastodonAPI.MastodonView
37 alias Pleroma.Web.MastodonAPI.ReportView
38 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
39 alias Pleroma.Web.MastodonAPI.StatusView
40 alias Pleroma.Web.MediaProxy
41 alias Pleroma.Web.OAuth.App
42 alias Pleroma.Web.OAuth.Authorization
43 alias Pleroma.Web.OAuth.Scopes
44 alias Pleroma.Web.OAuth.Token
45 alias Pleroma.Web.TwitterAPI.TwitterAPI
50 require Pleroma.Constants
52 @rate_limited_relations_actions ~w(follow unfollow)a
54 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
55 post_status delete_status)a
59 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
60 when action in ~w(reblog_status unreblog_status)a
65 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
66 when action in ~w(fav_status unfav_status)a
71 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
74 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
75 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
76 plug(RateLimiter, :app_account_creation when action == :account_register)
77 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
78 plug(RateLimiter, :password_reset when action == :password_reset)
79 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
81 @local_mastodon_name "Mastodon-Local"
83 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
85 def create_app(conn, params) do
86 scopes = Scopes.fetch_scopes(params, ["read"])
90 |> Map.drop(["scope", "scopes"])
91 |> Map.put("scopes", scopes)
93 with cs <- App.register_changeset(%App{}, app_attrs),
94 false <- cs.changes[:client_name] == @local_mastodon_name,
95 {:ok, app} <- Repo.insert(cs) do
98 |> render("show.json", %{app: app})
107 value_function \\ fn x -> {:ok, x} end
109 if Map.has_key?(params, params_field) do
110 case value_function.(params[params_field]) do
111 {:ok, new_value} -> Map.put(map, map_field, new_value)
119 def update_credentials(%{assigns: %{user: user}} = conn, params) do
124 |> add_if_present(params, "display_name", :name)
125 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
126 |> add_if_present(params, "avatar", :avatar, fn value ->
127 with %Plug.Upload{} <- value,
128 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
135 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
139 |> Map.get(:emoji, [])
140 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
147 :hide_followers_count,
153 :skip_thread_containment,
156 |> Enum.reduce(%{}, fn key, acc ->
157 add_if_present(acc, params, to_string(key), key, fn value ->
158 {:ok, truthy_param?(value)}
161 |> add_if_present(params, "default_scope", :default_scope)
162 |> add_if_present(params, "fields", :fields, fn fields ->
163 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
167 |> add_if_present(params, "fields", :raw_fields)
168 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
169 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
171 |> add_if_present(params, "header", :banner, fn value ->
172 with %Plug.Upload{} <- value,
173 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
179 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
180 with %Plug.Upload{} <- value,
181 {:ok, object} <- ActivityPub.upload(value, type: :background) do
187 |> Map.put(:emoji, user_info_emojis)
191 |> User.update_changeset(user_params)
192 |> User.change_info(&User.Info.profile_update(&1, info_params))
194 with {:ok, user} <- User.update_and_set_cache(changeset) do
195 if original_user != user, do: CommonAPI.update(user)
199 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
202 _e -> render_error(conn, :forbidden, "Invalid request")
206 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
207 change = Changeset.change(user, %{avatar: nil})
208 {:ok, user} = User.update_and_set_cache(change)
209 CommonAPI.update(user)
211 json(conn, %{url: nil})
214 def update_avatar(%{assigns: %{user: user}} = conn, params) do
215 {:ok, object} = ActivityPub.upload(params, type: :avatar)
216 change = Changeset.change(user, %{avatar: object.data})
217 {:ok, user} = User.update_and_set_cache(change)
218 CommonAPI.update(user)
219 %{"url" => [%{"href" => href} | _]} = object.data
221 json(conn, %{url: href})
224 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
225 new_info = %{"banner" => %{}}
227 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
228 CommonAPI.update(user)
229 json(conn, %{url: nil})
233 def update_banner(%{assigns: %{user: user}} = conn, params) do
234 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
235 new_info <- %{"banner" => object.data},
236 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
237 CommonAPI.update(user)
238 %{"url" => [%{"href" => href} | _]} = object.data
240 json(conn, %{url: href})
244 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
245 new_info = %{"background" => %{}}
247 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
248 json(conn, %{url: nil})
252 def update_background(%{assigns: %{user: user}} = conn, params) do
253 with {:ok, object} <- ActivityPub.upload(params, type: :background),
254 new_info <- %{"background" => object.data},
255 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
256 %{"url" => [%{"href" => href} | _]} = object.data
258 json(conn, %{url: href})
262 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
263 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
266 AccountView.render("account.json", %{
269 with_pleroma_settings: true,
270 with_chat_token: chat_token
276 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
277 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
280 |> render("short.json", %{app: app})
284 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
285 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
286 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
287 account = AccountView.render("account.json", %{user: user, for: for_user})
290 _e -> render_error(conn, :not_found, "Can't find user")
294 @mastodon_api_level "2.7.2"
296 def masto_instance(conn, _params) do
297 instance = Config.get(:instance)
301 title: Keyword.get(instance, :name),
302 description: Keyword.get(instance, :description),
303 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
304 email: Keyword.get(instance, :email),
306 streaming_api: Pleroma.Web.Endpoint.websocket_url()
308 stats: Stats.get_stats(),
309 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
311 registrations: Pleroma.Config.get([:instance, :registrations_open]),
312 # Extra (not present in Mastodon):
313 max_toot_chars: Keyword.get(instance, :limit),
314 poll_limits: Keyword.get(instance, :poll_limits)
320 def peers(conn, _params) do
321 json(conn, Stats.get_peers())
324 defp mastodonized_emoji do
325 Pleroma.Emoji.get_all()
326 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
327 url = to_string(URI.merge(Web.base_url(), relative_url))
330 "shortcode" => shortcode,
332 "visible_in_picker" => true,
335 # Assuming that a comma is authorized in the category name
336 "category" => (tags -- ["Custom"]) |> Enum.join(",")
341 def custom_emojis(conn, _params) do
342 mastodon_emoji = mastodonized_emoji()
343 json(conn, mastodon_emoji)
346 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
347 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
350 |> Map.put("tag", params["tagged"])
352 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
355 |> add_link_headers(activities)
356 |> put_view(StatusView)
357 |> render("index.json", %{
358 activities: activities,
365 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
371 |> Activity.all_by_ids_with_object()
372 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
375 |> put_view(StatusView)
376 |> render("index.json", activities: activities, for: user, as: :activity)
379 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
380 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
381 true <- Visibility.visible_for_user?(activity, user) do
383 |> put_view(StatusView)
384 |> try_render("status.json", %{activity: activity, for: user})
388 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
389 with %Activity{} = activity <- Activity.get_by_id(id),
391 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
392 "blocking_user" => user,
394 "exclude_id" => activity.id
396 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
402 activities: grouped_activities[true] || [],
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
411 activities: grouped_activities[false] || [],
415 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
422 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
423 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
424 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
425 true <- Visibility.visible_for_user?(activity, user) do
427 |> put_view(StatusView)
428 |> try_render("poll.json", %{object: object, for: user})
430 error when is_nil(error) or error == false ->
431 render_error(conn, :not_found, "Record not found")
435 defp get_cached_vote_or_vote(user, object, choices) do
436 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
439 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
440 case CommonAPI.vote(user, object, choices) do
441 {:error, _message} = res -> {:ignore, res}
442 res -> {:commit, res}
449 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
450 with %Object{} = object <- Object.get_by_id(id),
451 true <- object.data["type"] == "Question",
452 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
453 true <- Visibility.visible_for_user?(activity, user),
454 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
456 |> put_view(StatusView)
457 |> try_render("poll.json", %{object: object, for: user})
460 render_error(conn, :not_found, "Record not found")
463 render_error(conn, :not_found, "Record not found")
467 |> put_status(:unprocessable_entity)
468 |> json(%{error: message})
472 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
473 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
475 |> add_link_headers(scheduled_activities)
476 |> put_view(ScheduledActivityView)
477 |> render("index.json", %{scheduled_activities: scheduled_activities})
481 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
482 with %ScheduledActivity{} = scheduled_activity <-
483 ScheduledActivity.get(user, scheduled_activity_id) do
485 |> put_view(ScheduledActivityView)
486 |> render("show.json", %{scheduled_activity: scheduled_activity})
488 _ -> {:error, :not_found}
492 def update_scheduled_status(
493 %{assigns: %{user: user}} = conn,
494 %{"id" => scheduled_activity_id} = params
496 with %ScheduledActivity{} = scheduled_activity <-
497 ScheduledActivity.get(user, scheduled_activity_id),
498 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
500 |> put_view(ScheduledActivityView)
501 |> render("show.json", %{scheduled_activity: scheduled_activity})
503 nil -> {:error, :not_found}
508 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
509 with %ScheduledActivity{} = scheduled_activity <-
510 ScheduledActivity.get(user, scheduled_activity_id),
511 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
513 |> put_view(ScheduledActivityView)
514 |> render("show.json", %{scheduled_activity: scheduled_activity})
516 nil -> {:error, :not_found}
521 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
524 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
526 scheduled_at = params["scheduled_at"]
528 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
529 with {:ok, scheduled_activity} <-
530 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
532 |> put_view(ScheduledActivityView)
533 |> render("show.json", %{scheduled_activity: scheduled_activity})
536 params = Map.drop(params, ["scheduled_at"])
538 case CommonAPI.post(user, params) do
541 |> put_status(:unprocessable_entity)
542 |> json(%{error: message})
546 |> put_view(StatusView)
547 |> try_render("status.json", %{
551 with_direct_conversation_id: true
557 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
558 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
561 _e -> render_error(conn, :forbidden, "Can't delete this post")
565 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
566 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
567 %Activity{} = announce <- Activity.normalize(announce.data) do
569 |> put_view(StatusView)
570 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
574 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
575 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
576 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
578 |> put_view(StatusView)
579 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
583 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
584 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
585 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
587 |> put_view(StatusView)
588 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
592 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
593 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
594 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
596 |> put_view(StatusView)
597 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
601 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
602 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
604 |> put_view(StatusView)
605 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
609 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
610 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
612 |> put_view(StatusView)
613 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
617 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
618 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
619 %User{} = user <- User.get_cached_by_nickname(user.nickname),
620 true <- Visibility.visible_for_user?(activity, user),
621 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
623 |> put_view(StatusView)
624 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
628 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
629 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
630 %User{} = user <- User.get_cached_by_nickname(user.nickname),
631 true <- Visibility.visible_for_user?(activity, user),
632 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
634 |> put_view(StatusView)
635 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
639 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
640 activity = Activity.get_by_id(id)
642 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
644 |> put_view(StatusView)
645 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
649 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
650 activity = Activity.get_by_id(id)
652 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
654 |> put_view(StatusView)
655 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
659 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
661 q = from(u in User, where: u.id in ^id)
662 targets = Repo.all(q)
665 |> put_view(AccountView)
666 |> render("relationships.json", %{user: user, targets: targets})
669 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
670 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
672 def update_media(%{assigns: %{user: user}} = conn, data) do
673 with %Object{} = object <- Repo.get(Object, data["id"]),
674 true <- Object.authorize_mutation(object, user),
675 true <- is_binary(data["description"]),
676 description <- data["description"] do
677 new_data = %{object.data | "name" => description}
681 |> Object.change(%{data: new_data})
684 attachment_data = Map.put(new_data, "id", object.id)
687 |> put_view(StatusView)
688 |> render("attachment.json", %{attachment: attachment_data})
692 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
693 with {:ok, object} <-
696 actor: User.ap_id(user),
697 description: Map.get(data, "description")
699 attachment_data = Map.put(object.data, "id", object.id)
702 |> put_view(StatusView)
703 |> render("attachment.json", %{attachment: attachment_data})
707 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
708 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
709 %{} = attachment_data <- Map.put(object.data, "id", object.id),
710 # Reject if not an image
711 %{type: "image"} = rendered <-
712 StatusView.render("attachment.json", %{attachment: attachment_data}) do
714 # Save to the user's info
715 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
719 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
723 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
724 mascot = User.get_mascot(user)
730 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
731 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
732 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
733 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
734 q = from(u in User, where: u.ap_id in ^likes)
738 |> Enum.filter(&(not User.blocks?(user, &1)))
741 |> put_view(AccountView)
742 |> render("accounts.json", %{for: user, users: users, as: :user})
744 {:visible, false} -> {:error, :not_found}
749 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
750 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
751 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
752 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
753 q = from(u in User, where: u.ap_id in ^announces)
757 |> Enum.filter(&(not User.blocks?(user, &1)))
760 |> put_view(AccountView)
761 |> render("accounts.json", %{for: user, users: users, as: :user})
763 {:visible, false} -> {:error, :not_found}
768 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
769 with %User{} = user <- User.get_cached_by_id(id),
770 followers <- MastodonAPI.get_followers(user, params) do
773 for_user && user.id == for_user.id -> followers
774 user.info.hide_followers -> []
779 |> add_link_headers(followers)
780 |> put_view(AccountView)
781 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
785 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
786 with %User{} = user <- User.get_cached_by_id(id),
787 followers <- MastodonAPI.get_friends(user, params) do
790 for_user && user.id == for_user.id -> followers
791 user.info.hide_follows -> []
796 |> add_link_headers(followers)
797 |> put_view(AccountView)
798 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
802 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
803 follow_requests = User.get_follow_requests(followed)
806 |> put_view(AccountView)
807 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
810 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
811 with %User{} = follower <- User.get_cached_by_id(id),
812 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
814 |> put_view(AccountView)
815 |> render("relationship.json", %{user: followed, target: follower})
819 |> put_status(:forbidden)
820 |> json(%{error: message})
824 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
825 with %User{} = follower <- User.get_cached_by_id(id),
826 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
828 |> put_view(AccountView)
829 |> render("relationship.json", %{user: followed, target: follower})
833 |> put_status(:forbidden)
834 |> json(%{error: message})
838 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
839 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
840 {_, true} <- {:followed, follower.id != followed.id},
841 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
843 |> put_view(AccountView)
844 |> render("relationship.json", %{user: follower, target: followed})
851 |> put_status(:forbidden)
852 |> json(%{error: message})
856 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
857 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
858 {_, true} <- {:followed, follower.id != followed.id},
859 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
861 |> put_view(AccountView)
862 |> render("account.json", %{user: followed, for: follower})
869 |> put_status(:forbidden)
870 |> json(%{error: message})
874 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
875 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
876 {_, true} <- {:followed, follower.id != followed.id},
877 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
879 |> put_view(AccountView)
880 |> render("relationship.json", %{user: follower, target: followed})
890 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
892 if Map.has_key?(params, "notifications"),
893 do: params["notifications"] in [true, "True", "true", "1"],
896 with %User{} = muted <- User.get_cached_by_id(id),
897 {:ok, muter} <- User.mute(muter, muted, notifications) do
899 |> put_view(AccountView)
900 |> render("relationship.json", %{user: muter, target: muted})
904 |> put_status(:forbidden)
905 |> json(%{error: message})
909 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
910 with %User{} = muted <- User.get_cached_by_id(id),
911 {:ok, muter} <- User.unmute(muter, muted) do
913 |> put_view(AccountView)
914 |> render("relationship.json", %{user: muter, target: muted})
918 |> put_status(:forbidden)
919 |> json(%{error: message})
923 def mutes(%{assigns: %{user: user}} = conn, _) do
924 with muted_accounts <- User.muted_users(user) do
925 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
930 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
931 with %User{} = blocked <- User.get_cached_by_id(id),
932 {:ok, blocker} <- User.block(blocker, blocked),
933 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
935 |> put_view(AccountView)
936 |> render("relationship.json", %{user: blocker, target: blocked})
940 |> put_status(:forbidden)
941 |> json(%{error: message})
945 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
946 with %User{} = blocked <- User.get_cached_by_id(id),
947 {:ok, blocker} <- User.unblock(blocker, blocked),
948 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
950 |> put_view(AccountView)
951 |> render("relationship.json", %{user: blocker, target: blocked})
955 |> put_status(:forbidden)
956 |> json(%{error: message})
960 def blocks(%{assigns: %{user: user}} = conn, _) do
961 with blocked_accounts <- User.blocked_users(user) do
962 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
967 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
968 json(conn, info.domain_blocks || [])
971 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
972 User.block_domain(blocker, domain)
976 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
977 User.unblock_domain(blocker, domain)
981 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
982 with %User{} = subscription_target <- User.get_cached_by_id(id),
983 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
985 |> put_view(AccountView)
986 |> render("relationship.json", %{user: user, target: subscription_target})
990 |> put_status(:forbidden)
991 |> json(%{error: message})
995 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
996 with %User{} = subscription_target <- User.get_cached_by_id(id),
997 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
999 |> put_view(AccountView)
1000 |> render("relationship.json", %{user: user, target: subscription_target})
1002 {:error, message} ->
1004 |> put_status(:forbidden)
1005 |> json(%{error: message})
1009 def favourites(%{assigns: %{user: user}} = conn, params) do
1012 |> Map.put("type", "Create")
1013 |> Map.put("favorited_by", user.ap_id)
1014 |> Map.put("blocking_user", user)
1017 ActivityPub.fetch_activities([], params)
1021 |> add_link_headers(activities)
1022 |> put_view(StatusView)
1023 |> render("index.json", %{activities: activities, for: user, as: :activity})
1026 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1027 with %User{} = user <- User.get_by_id(id),
1028 false <- user.info.hide_favorites do
1031 |> Map.put("type", "Create")
1032 |> Map.put("favorited_by", user.ap_id)
1033 |> Map.put("blocking_user", for_user)
1037 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1039 [Pleroma.Constants.as_public()]
1044 |> ActivityPub.fetch_activities(params)
1048 |> add_link_headers(activities)
1049 |> put_view(StatusView)
1050 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1052 nil -> {:error, :not_found}
1053 true -> render_error(conn, :forbidden, "Can't get favorites")
1057 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1058 user = User.get_cached_by_id(user.id)
1061 Bookmark.for_user_query(user.id)
1062 |> Pagination.fetch_paginated(params)
1066 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1069 |> add_link_headers(bookmarks)
1070 |> put_view(StatusView)
1071 |> render("index.json", %{activities: activities, for: user, as: :activity})
1074 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1075 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1076 res = ListView.render("lists.json", lists: lists)
1080 def index(%{assigns: %{user: user}} = conn, _params) do
1081 token = get_session(conn, :oauth_token)
1084 mastodon_emoji = mastodonized_emoji()
1086 limit = Config.get([:instance, :limit])
1089 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1094 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1095 access_token: token,
1097 domain: Pleroma.Web.Endpoint.host(),
1100 unfollow_modal: false,
1103 auto_play_gif: false,
1104 display_sensitive_media: false,
1105 reduce_motion: false,
1106 max_toot_chars: limit,
1107 mascot: User.get_mascot(user)["url"]
1109 poll_limits: Config.get([:instance, :poll_limits]),
1111 delete_others_notice: present?(user.info.is_moderator),
1112 admin: present?(user.info.is_admin)
1116 default_privacy: user.info.default_scope,
1117 default_sensitive: false,
1118 allow_content_types: Config.get([:instance, :allowed_post_formats])
1120 media_attachments: %{
1121 accept_content_types: [
1137 user.info.settings ||
1167 push_subscription: nil,
1169 custom_emojis: mastodon_emoji,
1175 |> put_layout(false)
1176 |> put_view(MastodonView)
1177 |> render("index.html", %{initial_state: initial_state})
1180 |> put_session(:return_to, conn.request_path)
1181 |> redirect(to: "/web/login")
1185 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1186 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
1191 |> put_status(:internal_server_error)
1192 |> json(%{error: inspect(e)})
1196 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1197 redirect(conn, to: local_mastodon_root_path(conn))
1200 @doc "Local Mastodon FE login init action"
1201 def login(conn, %{"code" => auth_token}) do
1202 with {:ok, app} <- get_or_make_app(),
1203 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1204 {:ok, token} <- Token.exchange_token(app, auth) do
1206 |> put_session(:oauth_token, token.token)
1207 |> redirect(to: local_mastodon_root_path(conn))
1211 @doc "Local Mastodon FE callback action"
1212 def login(conn, _) do
1213 with {:ok, app} <- get_or_make_app() do
1218 response_type: "code",
1219 client_id: app.client_id,
1221 scope: Enum.join(app.scopes, " ")
1224 redirect(conn, to: path)
1228 defp local_mastodon_root_path(conn) do
1229 case get_session(conn, :return_to) do
1231 mastodon_api_path(conn, :index, ["getting-started"])
1234 delete_session(conn, :return_to)
1239 defp get_or_make_app do
1240 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1241 scopes = ["read", "write", "follow", "push"]
1243 with %App{} = app <- Repo.get_by(App, find_attrs) do
1245 if app.scopes == scopes do
1249 |> Changeset.change(%{scopes: scopes})
1257 App.register_changeset(
1259 Map.put(find_attrs, :scopes, scopes)
1266 def logout(conn, _) do
1269 |> redirect(to: "/")
1272 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1273 Logger.debug("Unimplemented, returning unmodified relationship")
1275 with %User{} = target <- User.get_cached_by_id(id) do
1277 |> put_view(AccountView)
1278 |> render("relationship.json", %{user: user, target: target})
1282 def empty_array(conn, _) do
1283 Logger.debug("Unimplemented, returning an empty array")
1287 def empty_object(conn, _) do
1288 Logger.debug("Unimplemented, returning an empty object")
1292 def get_filters(%{assigns: %{user: user}} = conn, _) do
1293 filters = Filter.get_filters(user)
1294 res = FilterView.render("filters.json", filters: filters)
1299 %{assigns: %{user: user}} = conn,
1300 %{"phrase" => phrase, "context" => context} = params
1306 hide: Map.get(params, "irreversible", false),
1307 whole_word: Map.get(params, "boolean", true)
1311 {:ok, response} = Filter.create(query)
1312 res = FilterView.render("filter.json", filter: response)
1316 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1317 filter = Filter.get(filter_id, user)
1318 res = FilterView.render("filter.json", filter: filter)
1323 %{assigns: %{user: user}} = conn,
1324 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1328 filter_id: filter_id,
1331 hide: Map.get(params, "irreversible", nil),
1332 whole_word: Map.get(params, "boolean", true)
1336 {:ok, response} = Filter.update(query)
1337 res = FilterView.render("filter.json", filter: response)
1341 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1344 filter_id: filter_id
1347 {:ok, _} = Filter.delete(query)
1351 def suggestions(%{assigns: %{user: user}} = conn, _) do
1352 suggestions = Config.get(:suggestions)
1354 if Keyword.get(suggestions, :enabled, false) do
1355 api = Keyword.get(suggestions, :third_party_engine, "")
1356 timeout = Keyword.get(suggestions, :timeout, 5000)
1357 limit = Keyword.get(suggestions, :limit, 23)
1359 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1361 user = user.nickname
1365 |> String.replace("{{host}}", host)
1366 |> String.replace("{{user}}", user)
1368 with {:ok, %{status: 200, body: body}} <-
1369 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1370 {:ok, data} <- Jason.decode(body) do
1373 |> Enum.slice(0, limit)
1376 |> Map.put("id", fetch_suggestion_id(x))
1377 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1378 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1384 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1391 defp fetch_suggestion_id(attrs) do
1392 case User.get_or_fetch(attrs["acct"]) do
1393 {:ok, %User{id: id}} -> id
1398 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1399 with %Activity{} = activity <- Activity.get_by_id(status_id),
1400 true <- Visibility.visible_for_user?(activity, user) do
1404 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1414 def reports(%{assigns: %{user: user}} = conn, params) do
1415 case CommonAPI.report(user, params) do
1418 |> put_view(ReportView)
1419 |> try_render("report.json", %{activity: activity})
1423 |> put_status(:bad_request)
1424 |> json(%{error: err})
1428 def account_register(
1429 %{assigns: %{app: app}} = conn,
1430 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1438 "captcha_answer_data",
1442 |> Map.put("nickname", nickname)
1443 |> Map.put("fullname", params["fullname"] || nickname)
1444 |> Map.put("bio", params["bio"] || "")
1445 |> Map.put("confirm", params["password"])
1447 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1448 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1450 token_type: "Bearer",
1451 access_token: token.token,
1453 created_at: Token.Utils.format_created_at(token)
1458 |> put_status(:bad_request)
1463 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1464 render_error(conn, :bad_request, "Missing parameters")
1467 def account_register(conn, _) do
1468 render_error(conn, :forbidden, "Invalid credentials")
1471 def conversations(%{assigns: %{user: user}} = conn, params) do
1472 participations = Participation.for_user_with_last_activity_id(user, params)
1475 Enum.map(participations, fn participation ->
1476 ConversationView.render("participation.json", %{participation: participation, for: user})
1480 |> add_link_headers(participations)
1481 |> json(conversations)
1484 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1485 with %Participation{} = participation <-
1486 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1487 {:ok, participation} <- Participation.mark_as_read(participation) do
1488 participation_view =
1489 ConversationView.render("participation.json", %{participation: participation, for: user})
1492 |> json(participation_view)
1496 def password_reset(conn, params) do
1497 nickname_or_email = params["email"] || params["nickname"]
1499 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1501 |> put_status(:no_content)
1504 {:error, "unknown user"} ->
1505 send_resp(conn, :not_found, "")
1508 send_resp(conn, :bad_request, "")
1512 def account_confirmation_resend(conn, params) do
1513 nickname_or_email = params["email"] || params["nickname"]
1515 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1516 {:ok, _} <- User.try_send_confirmation_email(user) do
1518 |> json_response(:no_content, "")
1522 def try_render(conn, target, params)
1523 when is_binary(target) do
1524 case render(conn, target, params) do
1525 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1530 def try_render(conn, _, _) do
1531 render_error(conn, :not_implemented, "Can't display this activity")
1534 defp present?(nil), do: false
1535 defp present?(false), do: false
1536 defp present?(_), do: true