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}
522 %{assigns: %{user: user}} = conn,
523 %{"status" => _, "scheduled_at" => scheduled_at} = params
525 if ScheduledActivity.far_enough?(scheduled_at) do
526 with {:ok, scheduled_activity} <-
527 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
529 |> put_view(ScheduledActivityView)
530 |> render("show.json", %{scheduled_activity: scheduled_activity})
533 post_status(conn, Map.drop(params, ["scheduled_at"]))
537 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
538 case CommonAPI.post(user, params) do
541 |> put_view(StatusView)
542 |> try_render("status.json", %{
546 with_direct_conversation_id: true
551 |> put_status(:unprocessable_entity)
552 |> json(%{error: message})
556 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
557 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
560 _e -> render_error(conn, :forbidden, "Can't delete this post")
564 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
565 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
566 %Activity{} = announce <- Activity.normalize(announce.data) do
568 |> put_view(StatusView)
569 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
573 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
574 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
575 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
577 |> put_view(StatusView)
578 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
582 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
583 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
584 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
586 |> put_view(StatusView)
587 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
591 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
592 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
593 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
595 |> put_view(StatusView)
596 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
600 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
601 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
603 |> put_view(StatusView)
604 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
608 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
609 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
611 |> put_view(StatusView)
612 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
616 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
617 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
618 %User{} = user <- User.get_cached_by_nickname(user.nickname),
619 true <- Visibility.visible_for_user?(activity, user),
620 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
622 |> put_view(StatusView)
623 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
627 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
628 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
629 %User{} = user <- User.get_cached_by_nickname(user.nickname),
630 true <- Visibility.visible_for_user?(activity, user),
631 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
633 |> put_view(StatusView)
634 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
638 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
639 activity = Activity.get_by_id(id)
641 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
643 |> put_view(StatusView)
644 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
648 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
649 activity = Activity.get_by_id(id)
651 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
653 |> put_view(StatusView)
654 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
658 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
660 q = from(u in User, where: u.id in ^id)
661 targets = Repo.all(q)
664 |> put_view(AccountView)
665 |> render("relationships.json", %{user: user, targets: targets})
668 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
669 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
671 def update_media(%{assigns: %{user: user}} = conn, data) do
672 with %Object{} = object <- Repo.get(Object, data["id"]),
673 true <- Object.authorize_mutation(object, user),
674 true <- is_binary(data["description"]),
675 description <- data["description"] do
676 new_data = %{object.data | "name" => description}
680 |> Object.change(%{data: new_data})
683 attachment_data = Map.put(new_data, "id", object.id)
686 |> put_view(StatusView)
687 |> render("attachment.json", %{attachment: attachment_data})
691 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
692 with {:ok, object} <-
695 actor: User.ap_id(user),
696 description: Map.get(data, "description")
698 attachment_data = Map.put(object.data, "id", object.id)
701 |> put_view(StatusView)
702 |> render("attachment.json", %{attachment: attachment_data})
706 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
707 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
708 %{} = attachment_data <- Map.put(object.data, "id", object.id),
709 # Reject if not an image
710 %{type: "image"} = rendered <-
711 StatusView.render("attachment.json", %{attachment: attachment_data}) do
713 # Save to the user's info
714 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
718 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
722 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
723 mascot = User.get_mascot(user)
729 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
730 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
731 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
732 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
733 q = from(u in User, where: u.ap_id in ^likes)
737 |> Enum.filter(&(not User.blocks?(user, &1)))
740 |> put_view(AccountView)
741 |> render("accounts.json", %{for: user, users: users, as: :user})
743 {:visible, false} -> {:error, :not_found}
748 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
749 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
750 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
751 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
752 q = from(u in User, where: u.ap_id in ^announces)
756 |> Enum.filter(&(not User.blocks?(user, &1)))
759 |> put_view(AccountView)
760 |> render("accounts.json", %{for: user, users: users, as: :user})
762 {:visible, false} -> {:error, :not_found}
767 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
768 with %User{} = user <- User.get_cached_by_id(id),
769 followers <- MastodonAPI.get_followers(user, params) do
772 for_user && user.id == for_user.id -> followers
773 user.info.hide_followers -> []
778 |> add_link_headers(followers)
779 |> put_view(AccountView)
780 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
784 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
785 with %User{} = user <- User.get_cached_by_id(id),
786 followers <- MastodonAPI.get_friends(user, params) do
789 for_user && user.id == for_user.id -> followers
790 user.info.hide_follows -> []
795 |> add_link_headers(followers)
796 |> put_view(AccountView)
797 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
801 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
802 follow_requests = User.get_follow_requests(followed)
805 |> put_view(AccountView)
806 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
809 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
810 with %User{} = follower <- User.get_cached_by_id(id),
811 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
813 |> put_view(AccountView)
814 |> render("relationship.json", %{user: followed, target: follower})
818 |> put_status(:forbidden)
819 |> json(%{error: message})
823 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
824 with %User{} = follower <- User.get_cached_by_id(id),
825 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
827 |> put_view(AccountView)
828 |> render("relationship.json", %{user: followed, target: follower})
832 |> put_status(:forbidden)
833 |> json(%{error: message})
837 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
838 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
839 {_, true} <- {:followed, follower.id != followed.id},
840 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
842 |> put_view(AccountView)
843 |> render("relationship.json", %{user: follower, target: followed})
850 |> put_status(:forbidden)
851 |> json(%{error: message})
855 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
856 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
857 {_, true} <- {:followed, follower.id != followed.id},
858 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
860 |> put_view(AccountView)
861 |> render("account.json", %{user: followed, for: follower})
868 |> put_status(:forbidden)
869 |> json(%{error: message})
873 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
874 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
875 {_, true} <- {:followed, follower.id != followed.id},
876 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
878 |> put_view(AccountView)
879 |> render("relationship.json", %{user: follower, target: followed})
889 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
891 if Map.has_key?(params, "notifications"),
892 do: params["notifications"] in [true, "True", "true", "1"],
895 with %User{} = muted <- User.get_cached_by_id(id),
896 {:ok, muter} <- User.mute(muter, muted, notifications) do
898 |> put_view(AccountView)
899 |> render("relationship.json", %{user: muter, target: muted})
903 |> put_status(:forbidden)
904 |> json(%{error: message})
908 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
909 with %User{} = muted <- User.get_cached_by_id(id),
910 {:ok, muter} <- User.unmute(muter, muted) do
912 |> put_view(AccountView)
913 |> render("relationship.json", %{user: muter, target: muted})
917 |> put_status(:forbidden)
918 |> json(%{error: message})
922 def mutes(%{assigns: %{user: user}} = conn, _) do
923 with muted_accounts <- User.muted_users(user) do
924 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
929 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
930 with %User{} = blocked <- User.get_cached_by_id(id),
931 {:ok, blocker} <- User.block(blocker, blocked),
932 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
934 |> put_view(AccountView)
935 |> render("relationship.json", %{user: blocker, target: blocked})
939 |> put_status(:forbidden)
940 |> json(%{error: message})
944 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
945 with %User{} = blocked <- User.get_cached_by_id(id),
946 {:ok, blocker} <- User.unblock(blocker, blocked),
947 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
949 |> put_view(AccountView)
950 |> render("relationship.json", %{user: blocker, target: blocked})
954 |> put_status(:forbidden)
955 |> json(%{error: message})
959 def blocks(%{assigns: %{user: user}} = conn, _) do
960 with blocked_accounts <- User.blocked_users(user) do
961 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
966 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
967 json(conn, info.domain_blocks || [])
970 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
971 User.block_domain(blocker, domain)
975 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
976 User.unblock_domain(blocker, domain)
980 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
981 with %User{} = subscription_target <- User.get_cached_by_id(id),
982 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
984 |> put_view(AccountView)
985 |> render("relationship.json", %{user: user, target: subscription_target})
989 |> put_status(:forbidden)
990 |> json(%{error: message})
994 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
995 with %User{} = subscription_target <- User.get_cached_by_id(id),
996 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
998 |> put_view(AccountView)
999 |> render("relationship.json", %{user: user, target: subscription_target})
1001 {:error, message} ->
1003 |> put_status(:forbidden)
1004 |> json(%{error: message})
1008 def favourites(%{assigns: %{user: user}} = conn, params) do
1011 |> Map.put("type", "Create")
1012 |> Map.put("favorited_by", user.ap_id)
1013 |> Map.put("blocking_user", user)
1016 ActivityPub.fetch_activities([], params)
1020 |> add_link_headers(activities)
1021 |> put_view(StatusView)
1022 |> render("index.json", %{activities: activities, for: user, as: :activity})
1025 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1026 with %User{} = user <- User.get_by_id(id),
1027 false <- user.info.hide_favorites do
1030 |> Map.put("type", "Create")
1031 |> Map.put("favorited_by", user.ap_id)
1032 |> Map.put("blocking_user", for_user)
1036 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1038 [Pleroma.Constants.as_public()]
1043 |> ActivityPub.fetch_activities(params)
1047 |> add_link_headers(activities)
1048 |> put_view(StatusView)
1049 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1051 nil -> {:error, :not_found}
1052 true -> render_error(conn, :forbidden, "Can't get favorites")
1056 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1057 user = User.get_cached_by_id(user.id)
1060 Bookmark.for_user_query(user.id)
1061 |> Pagination.fetch_paginated(params)
1065 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1068 |> add_link_headers(bookmarks)
1069 |> put_view(StatusView)
1070 |> render("index.json", %{activities: activities, for: user, as: :activity})
1073 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1074 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1075 res = ListView.render("lists.json", lists: lists)
1079 def index(%{assigns: %{user: user}} = conn, _params) do
1080 token = get_session(conn, :oauth_token)
1083 mastodon_emoji = mastodonized_emoji()
1085 limit = Config.get([:instance, :limit])
1088 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1093 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1094 access_token: token,
1096 domain: Pleroma.Web.Endpoint.host(),
1099 unfollow_modal: false,
1102 auto_play_gif: false,
1103 display_sensitive_media: false,
1104 reduce_motion: false,
1105 max_toot_chars: limit,
1106 mascot: User.get_mascot(user)["url"]
1108 poll_limits: Config.get([:instance, :poll_limits]),
1110 delete_others_notice: present?(user.info.is_moderator),
1111 admin: present?(user.info.is_admin)
1115 default_privacy: user.info.default_scope,
1116 default_sensitive: false,
1117 allow_content_types: Config.get([:instance, :allowed_post_formats])
1119 media_attachments: %{
1120 accept_content_types: [
1136 user.info.settings ||
1166 push_subscription: nil,
1168 custom_emojis: mastodon_emoji,
1174 |> put_layout(false)
1175 |> put_view(MastodonView)
1176 |> render("index.html", %{initial_state: initial_state})
1179 |> put_session(:return_to, conn.request_path)
1180 |> redirect(to: "/web/login")
1184 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1185 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
1190 |> put_status(:internal_server_error)
1191 |> json(%{error: inspect(e)})
1195 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1196 redirect(conn, to: local_mastodon_root_path(conn))
1199 @doc "Local Mastodon FE login init action"
1200 def login(conn, %{"code" => auth_token}) do
1201 with {:ok, app} <- get_or_make_app(),
1202 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1203 {:ok, token} <- Token.exchange_token(app, auth) do
1205 |> put_session(:oauth_token, token.token)
1206 |> redirect(to: local_mastodon_root_path(conn))
1210 @doc "Local Mastodon FE callback action"
1211 def login(conn, _) do
1212 with {:ok, app} <- get_or_make_app() do
1217 response_type: "code",
1218 client_id: app.client_id,
1220 scope: Enum.join(app.scopes, " ")
1223 redirect(conn, to: path)
1227 defp local_mastodon_root_path(conn) do
1228 case get_session(conn, :return_to) do
1230 mastodon_api_path(conn, :index, ["getting-started"])
1233 delete_session(conn, :return_to)
1238 defp get_or_make_app do
1239 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1240 scopes = ["read", "write", "follow", "push"]
1242 with %App{} = app <- Repo.get_by(App, find_attrs) do
1244 if app.scopes == scopes do
1248 |> Changeset.change(%{scopes: scopes})
1256 App.register_changeset(
1258 Map.put(find_attrs, :scopes, scopes)
1265 def logout(conn, _) do
1268 |> redirect(to: "/")
1271 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1272 Logger.debug("Unimplemented, returning unmodified relationship")
1274 with %User{} = target <- User.get_cached_by_id(id) do
1276 |> put_view(AccountView)
1277 |> render("relationship.json", %{user: user, target: target})
1281 def empty_array(conn, _) do
1282 Logger.debug("Unimplemented, returning an empty array")
1286 def empty_object(conn, _) do
1287 Logger.debug("Unimplemented, returning an empty object")
1291 def get_filters(%{assigns: %{user: user}} = conn, _) do
1292 filters = Filter.get_filters(user)
1293 res = FilterView.render("filters.json", filters: filters)
1298 %{assigns: %{user: user}} = conn,
1299 %{"phrase" => phrase, "context" => context} = params
1305 hide: Map.get(params, "irreversible", false),
1306 whole_word: Map.get(params, "boolean", true)
1310 {:ok, response} = Filter.create(query)
1311 res = FilterView.render("filter.json", filter: response)
1315 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1316 filter = Filter.get(filter_id, user)
1317 res = FilterView.render("filter.json", filter: filter)
1322 %{assigns: %{user: user}} = conn,
1323 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1327 filter_id: filter_id,
1330 hide: Map.get(params, "irreversible", nil),
1331 whole_word: Map.get(params, "boolean", true)
1335 {:ok, response} = Filter.update(query)
1336 res = FilterView.render("filter.json", filter: response)
1340 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1343 filter_id: filter_id
1346 {:ok, _} = Filter.delete(query)
1350 def suggestions(%{assigns: %{user: user}} = conn, _) do
1351 suggestions = Config.get(:suggestions)
1353 if Keyword.get(suggestions, :enabled, false) do
1354 api = Keyword.get(suggestions, :third_party_engine, "")
1355 timeout = Keyword.get(suggestions, :timeout, 5000)
1356 limit = Keyword.get(suggestions, :limit, 23)
1358 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1360 user = user.nickname
1364 |> String.replace("{{host}}", host)
1365 |> String.replace("{{user}}", user)
1367 with {:ok, %{status: 200, body: body}} <-
1368 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1369 {:ok, data} <- Jason.decode(body) do
1372 |> Enum.slice(0, limit)
1375 |> Map.put("id", fetch_suggestion_id(x))
1376 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1377 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1383 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1390 defp fetch_suggestion_id(attrs) do
1391 case User.get_or_fetch(attrs["acct"]) do
1392 {:ok, %User{id: id}} -> id
1397 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1398 with %Activity{} = activity <- Activity.get_by_id(status_id),
1399 true <- Visibility.visible_for_user?(activity, user) do
1403 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1413 def reports(%{assigns: %{user: user}} = conn, params) do
1414 case CommonAPI.report(user, params) do
1417 |> put_view(ReportView)
1418 |> try_render("report.json", %{activity: activity})
1422 |> put_status(:bad_request)
1423 |> json(%{error: err})
1427 def account_register(
1428 %{assigns: %{app: app}} = conn,
1429 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1437 "captcha_answer_data",
1441 |> Map.put("nickname", nickname)
1442 |> Map.put("fullname", params["fullname"] || nickname)
1443 |> Map.put("bio", params["bio"] || "")
1444 |> Map.put("confirm", params["password"])
1446 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1447 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1449 token_type: "Bearer",
1450 access_token: token.token,
1452 created_at: Token.Utils.format_created_at(token)
1457 |> put_status(:bad_request)
1462 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1463 render_error(conn, :bad_request, "Missing parameters")
1466 def account_register(conn, _) do
1467 render_error(conn, :forbidden, "Invalid credentials")
1470 def conversations(%{assigns: %{user: user}} = conn, params) do
1471 participations = Participation.for_user_with_last_activity_id(user, params)
1474 Enum.map(participations, fn participation ->
1475 ConversationView.render("participation.json", %{participation: participation, for: user})
1479 |> add_link_headers(participations)
1480 |> json(conversations)
1483 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1484 with %Participation{} = participation <-
1485 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1486 {:ok, participation} <- Participation.mark_as_read(participation) do
1487 participation_view =
1488 ConversationView.render("participation.json", %{participation: participation, for: user})
1491 |> json(participation_view)
1495 def password_reset(conn, params) do
1496 nickname_or_email = params["email"] || params["nickname"]
1498 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1500 |> put_status(:no_content)
1503 {:error, "unknown user"} ->
1504 send_resp(conn, :not_found, "")
1507 send_resp(conn, :bad_request, "")
1511 def account_confirmation_resend(conn, params) do
1512 nickname_or_email = params["email"] || params["nickname"]
1514 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1515 {:ok, _} <- User.try_send_confirmation_email(user) do
1517 |> json_response(:no_content, "")
1521 def try_render(conn, target, params)
1522 when is_binary(target) do
1523 case render(conn, target, params) do
1524 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1529 def try_render(conn, _, _) do
1530 render_error(conn, :not_implemented, "Can't display this activity")
1533 defp present?(nil), do: false
1534 defp present?(false), do: false
1535 defp present?(_), do: true