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
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
47 alias Pleroma.Web.ControllerHelper
51 require Pleroma.Constants
53 @rate_limited_relations_actions ~w(follow unfollow)a
55 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
56 post_status delete_status)a
60 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
61 when action in ~w(reblog_status unreblog_status)a
66 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
67 when action in ~w(fav_status unfav_status)a
72 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
75 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
76 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
77 plug(RateLimiter, :app_account_creation when action == :account_register)
78 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
79 plug(RateLimiter, :password_reset when action == :password_reset)
80 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
82 @local_mastodon_name "Mastodon-Local"
84 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
86 def create_app(conn, params) do
87 scopes = Scopes.fetch_scopes(params, ["read"])
91 |> Map.drop(["scope", "scopes"])
92 |> Map.put("scopes", scopes)
94 with cs <- App.register_changeset(%App{}, app_attrs),
95 false <- cs.changes[:client_name] == @local_mastodon_name,
96 {:ok, app} <- Repo.insert(cs) do
99 |> render("show.json", %{app: app})
108 value_function \\ fn x -> {:ok, x} end
110 if Map.has_key?(params, params_field) do
111 case value_function.(params[params_field]) do
112 {:ok, new_value} -> Map.put(map, map_field, new_value)
120 def update_credentials(%{assigns: %{user: user}} = conn, params) do
125 |> add_if_present(params, "display_name", :name)
126 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
127 |> add_if_present(params, "avatar", :avatar, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
136 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
140 |> Map.get(:emoji, [])
141 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
148 :hide_followers_count,
154 :skip_thread_containment
156 |> Enum.reduce(%{}, fn key, acc ->
157 add_if_present(acc, params, to_string(key), key, fn value ->
158 {:ok, ControllerHelper.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)
189 info_cng = User.Info.profile_update(user.info, info_params)
191 with changeset <- User.update_changeset(user, user_params),
192 changeset <- Changeset.put_embed(changeset, :info, info_cng),
193 {:ok, user} <- User.update_and_set_cache(changeset) do
194 if original_user != user do
195 CommonAPI.update(user)
200 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
203 _e -> render_error(conn, :forbidden, "Invalid request")
207 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
208 change = Changeset.change(user, %{avatar: nil})
209 {:ok, user} = User.update_and_set_cache(change)
210 CommonAPI.update(user)
212 json(conn, %{url: nil})
215 def update_avatar(%{assigns: %{user: user}} = conn, params) do
216 {:ok, object} = ActivityPub.upload(params, type: :avatar)
217 change = Changeset.change(user, %{avatar: object.data})
218 {:ok, user} = User.update_and_set_cache(change)
219 CommonAPI.update(user)
220 %{"url" => [%{"href" => href} | _]} = object.data
222 json(conn, %{url: href})
225 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
226 with new_info <- %{"banner" => %{}},
227 info_cng <- User.Info.profile_update(user.info, new_info),
228 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
229 {:ok, user} <- User.update_and_set_cache(changeset) do
230 CommonAPI.update(user)
232 json(conn, %{url: nil})
236 def update_banner(%{assigns: %{user: user}} = conn, params) do
237 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
238 new_info <- %{"banner" => object.data},
239 info_cng <- User.Info.profile_update(user.info, new_info),
240 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
241 {:ok, user} <- User.update_and_set_cache(changeset) do
242 CommonAPI.update(user)
243 %{"url" => [%{"href" => href} | _]} = object.data
245 json(conn, %{url: href})
249 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
250 with new_info <- %{"background" => %{}},
251 info_cng <- User.Info.profile_update(user.info, new_info),
252 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
253 {:ok, _user} <- User.update_and_set_cache(changeset) do
254 json(conn, %{url: nil})
258 def update_background(%{assigns: %{user: user}} = conn, params) do
259 with {:ok, object} <- ActivityPub.upload(params, type: :background),
260 new_info <- %{"background" => object.data},
261 info_cng <- User.Info.profile_update(user.info, new_info),
262 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
263 {:ok, _user} <- User.update_and_set_cache(changeset) do
264 %{"url" => [%{"href" => href} | _]} = object.data
266 json(conn, %{url: href})
270 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
271 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
274 AccountView.render("account.json", %{
277 with_pleroma_settings: true,
278 with_chat_token: chat_token
284 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
285 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
288 |> render("short.json", %{app: app})
292 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
293 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
294 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
295 account = AccountView.render("account.json", %{user: user, for: for_user})
298 _e -> render_error(conn, :not_found, "Can't find user")
302 @mastodon_api_level "2.7.2"
304 def masto_instance(conn, _params) do
305 instance = Config.get(:instance)
309 title: Keyword.get(instance, :name),
310 description: Keyword.get(instance, :description),
311 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
312 email: Keyword.get(instance, :email),
314 streaming_api: Pleroma.Web.Endpoint.websocket_url()
316 stats: Stats.get_stats(),
317 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
319 registrations: Pleroma.Config.get([:instance, :registrations_open]),
320 # Extra (not present in Mastodon):
321 max_toot_chars: Keyword.get(instance, :limit),
322 poll_limits: Keyword.get(instance, :poll_limits)
328 def peers(conn, _params) do
329 json(conn, Stats.get_peers())
332 defp mastodonized_emoji do
333 Pleroma.Emoji.get_all()
334 |> Enum.map(fn {shortcode, relative_url, tags} ->
335 url = to_string(URI.merge(Web.base_url(), relative_url))
338 "shortcode" => shortcode,
340 "visible_in_picker" => true,
343 # Assuming that a comma is authorized in the category name
344 "category" => (tags -- ["Custom"]) |> Enum.join(",")
349 def custom_emojis(conn, _params) do
350 mastodon_emoji = mastodonized_emoji()
351 json(conn, mastodon_emoji)
354 def home_timeline(%{assigns: %{user: user}} = conn, params) do
357 |> Map.put("type", ["Create", "Announce"])
358 |> Map.put("blocking_user", user)
359 |> Map.put("muting_user", user)
360 |> Map.put("user", user)
363 [user.ap_id | user.following]
364 |> ActivityPub.fetch_activities(params)
368 |> add_link_headers(activities)
369 |> put_view(StatusView)
370 |> render("index.json", %{activities: activities, for: user, as: :activity})
373 def public_timeline(%{assigns: %{user: user}} = conn, params) do
374 local_only = params["local"] in [true, "True", "true", "1"]
378 |> Map.put("type", ["Create", "Announce"])
379 |> Map.put("local_only", local_only)
380 |> Map.put("blocking_user", user)
381 |> Map.put("muting_user", user)
382 |> ActivityPub.fetch_public_activities()
386 |> add_link_headers(activities, %{"local" => local_only})
387 |> put_view(StatusView)
388 |> render("index.json", %{activities: activities, for: user, as: :activity})
391 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
392 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
395 |> Map.put("tag", params["tagged"])
397 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
400 |> add_link_headers(activities)
401 |> put_view(StatusView)
402 |> render("index.json", %{
403 activities: activities,
410 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
413 |> Map.put("type", "Create")
414 |> Map.put("blocking_user", user)
415 |> Map.put("user", user)
416 |> Map.put(:visibility, "direct")
420 |> ActivityPub.fetch_activities_query(params)
421 |> Pagination.fetch_paginated(params)
424 |> add_link_headers(activities)
425 |> put_view(StatusView)
426 |> render("index.json", %{activities: activities, for: user, as: :activity})
429 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
435 |> Activity.all_by_ids_with_object()
436 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
439 |> put_view(StatusView)
440 |> render("index.json", activities: activities, for: user, as: :activity)
443 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
444 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
445 true <- Visibility.visible_for_user?(activity, user) do
447 |> put_view(StatusView)
448 |> try_render("status.json", %{activity: activity, for: user})
452 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
453 with %Activity{} = activity <- Activity.get_by_id(id),
455 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
456 "blocking_user" => user,
458 "exclude_id" => activity.id
460 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
466 activities: grouped_activities[true] || [],
470 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
475 activities: grouped_activities[false] || [],
479 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
486 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
487 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
488 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
489 true <- Visibility.visible_for_user?(activity, user) do
491 |> put_view(StatusView)
492 |> try_render("poll.json", %{object: object, for: user})
494 error when is_nil(error) or error == false ->
495 render_error(conn, :not_found, "Record not found")
499 defp get_cached_vote_or_vote(user, object, choices) do
500 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
503 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
504 case CommonAPI.vote(user, object, choices) do
505 {:error, _message} = res -> {:ignore, res}
506 res -> {:commit, res}
513 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
514 with %Object{} = object <- Object.get_by_id(id),
515 true <- object.data["type"] == "Question",
516 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
517 true <- Visibility.visible_for_user?(activity, user),
518 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
520 |> put_view(StatusView)
521 |> try_render("poll.json", %{object: object, for: user})
524 render_error(conn, :not_found, "Record not found")
527 render_error(conn, :not_found, "Record not found")
531 |> put_status(:unprocessable_entity)
532 |> json(%{error: message})
536 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
537 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
539 |> add_link_headers(scheduled_activities)
540 |> put_view(ScheduledActivityView)
541 |> render("index.json", %{scheduled_activities: scheduled_activities})
545 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
546 with %ScheduledActivity{} = scheduled_activity <-
547 ScheduledActivity.get(user, scheduled_activity_id) do
549 |> put_view(ScheduledActivityView)
550 |> render("show.json", %{scheduled_activity: scheduled_activity})
552 _ -> {:error, :not_found}
556 def update_scheduled_status(
557 %{assigns: %{user: user}} = conn,
558 %{"id" => scheduled_activity_id} = params
560 with %ScheduledActivity{} = scheduled_activity <-
561 ScheduledActivity.get(user, scheduled_activity_id),
562 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
564 |> put_view(ScheduledActivityView)
565 |> render("show.json", %{scheduled_activity: scheduled_activity})
567 nil -> {:error, :not_found}
572 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
573 with %ScheduledActivity{} = scheduled_activity <-
574 ScheduledActivity.get(user, scheduled_activity_id),
575 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
577 |> put_view(ScheduledActivityView)
578 |> render("show.json", %{scheduled_activity: scheduled_activity})
580 nil -> {:error, :not_found}
585 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
588 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
590 scheduled_at = params["scheduled_at"]
592 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
593 with {:ok, scheduled_activity} <-
594 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
596 |> put_view(ScheduledActivityView)
597 |> render("show.json", %{scheduled_activity: scheduled_activity})
600 params = Map.drop(params, ["scheduled_at"])
602 case CommonAPI.post(user, params) do
605 |> put_status(:unprocessable_entity)
606 |> json(%{error: message})
610 |> put_view(StatusView)
611 |> try_render("status.json", %{
615 with_direct_conversation_id: true
621 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
622 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
625 _e -> render_error(conn, :forbidden, "Can't delete this post")
629 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
630 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
631 %Activity{} = announce <- Activity.normalize(announce.data) do
633 |> put_view(StatusView)
634 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
638 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
639 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
640 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
647 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
648 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
649 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
651 |> put_view(StatusView)
652 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
656 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
657 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
658 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
665 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
666 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
668 |> put_view(StatusView)
669 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
673 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
674 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
676 |> put_view(StatusView)
677 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
681 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
682 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
683 %User{} = user <- User.get_cached_by_nickname(user.nickname),
684 true <- Visibility.visible_for_user?(activity, user),
685 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
687 |> put_view(StatusView)
688 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
692 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
693 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
694 %User{} = user <- User.get_cached_by_nickname(user.nickname),
695 true <- Visibility.visible_for_user?(activity, user),
696 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
698 |> put_view(StatusView)
699 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
703 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
704 activity = Activity.get_by_id(id)
706 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
708 |> put_view(StatusView)
709 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
713 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
714 activity = Activity.get_by_id(id)
716 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
718 |> put_view(StatusView)
719 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
723 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
725 q = from(u in User, where: u.id in ^id)
726 targets = Repo.all(q)
729 |> put_view(AccountView)
730 |> render("relationships.json", %{user: user, targets: targets})
733 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
734 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
736 def update_media(%{assigns: %{user: user}} = conn, data) do
737 with %Object{} = object <- Repo.get(Object, data["id"]),
738 true <- Object.authorize_mutation(object, user),
739 true <- is_binary(data["description"]),
740 description <- data["description"] do
741 new_data = %{object.data | "name" => description}
745 |> Object.change(%{data: new_data})
748 attachment_data = Map.put(new_data, "id", object.id)
751 |> put_view(StatusView)
752 |> render("attachment.json", %{attachment: attachment_data})
756 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
757 with {:ok, object} <-
760 actor: User.ap_id(user),
761 description: Map.get(data, "description")
763 attachment_data = Map.put(object.data, "id", object.id)
766 |> put_view(StatusView)
767 |> render("attachment.json", %{attachment: attachment_data})
771 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
772 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
773 %{} = attachment_data <- Map.put(object.data, "id", object.id),
774 %{type: type} = rendered <-
775 StatusView.render("attachment.json", %{attachment: attachment_data}) do
776 # Reject if not an image
777 if type == "image" do
779 # Save to the user's info
780 info_changeset = User.Info.mascot_update(user.info, rendered)
784 |> Changeset.change()
785 |> Changeset.put_embed(:info, info_changeset)
787 {:ok, _user} = User.update_and_set_cache(user_changeset)
792 render_error(conn, :unsupported_media_type, "mascots can only be images")
797 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
798 mascot = User.get_mascot(user)
804 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
805 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
806 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
807 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
808 q = from(u in User, where: u.ap_id in ^likes)
812 |> Enum.filter(&(not User.blocks?(user, &1)))
815 |> put_view(AccountView)
816 |> render("accounts.json", %{for: user, users: users, as: :user})
818 {:visible, false} -> {:error, :not_found}
823 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
824 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
825 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
826 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
827 q = from(u in User, where: u.ap_id in ^announces)
831 |> Enum.filter(&(not User.blocks?(user, &1)))
834 |> put_view(AccountView)
835 |> render("accounts.json", %{for: user, users: users, as: :user})
837 {:visible, false} -> {:error, :not_found}
842 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
843 local_only = params["local"] in [true, "True", "true", "1"]
846 [params["tag"], params["any"]]
850 |> Enum.map(&String.downcase(&1))
855 |> Enum.map(&String.downcase(&1))
860 |> Enum.map(&String.downcase(&1))
864 |> Map.put("type", "Create")
865 |> Map.put("local_only", local_only)
866 |> Map.put("blocking_user", user)
867 |> Map.put("muting_user", user)
868 |> Map.put("user", user)
869 |> Map.put("tag", tags)
870 |> Map.put("tag_all", tag_all)
871 |> Map.put("tag_reject", tag_reject)
872 |> ActivityPub.fetch_public_activities()
876 |> add_link_headers(activities, %{"local" => local_only})
877 |> put_view(StatusView)
878 |> render("index.json", %{activities: activities, for: user, as: :activity})
881 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
882 with %User{} = user <- User.get_cached_by_id(id),
883 followers <- MastodonAPI.get_followers(user, params) do
886 for_user && user.id == for_user.id -> followers
887 user.info.hide_followers -> []
892 |> add_link_headers(followers)
893 |> put_view(AccountView)
894 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
898 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
899 with %User{} = user <- User.get_cached_by_id(id),
900 followers <- MastodonAPI.get_friends(user, params) do
903 for_user && user.id == for_user.id -> followers
904 user.info.hide_follows -> []
909 |> add_link_headers(followers)
910 |> put_view(AccountView)
911 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
915 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
916 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
918 |> put_view(AccountView)
919 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
923 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
924 with %User{} = follower <- User.get_cached_by_id(id),
925 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
927 |> put_view(AccountView)
928 |> render("relationship.json", %{user: followed, target: follower})
932 |> put_status(:forbidden)
933 |> json(%{error: message})
937 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
938 with %User{} = follower <- User.get_cached_by_id(id),
939 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
941 |> put_view(AccountView)
942 |> render("relationship.json", %{user: followed, target: follower})
946 |> put_status(:forbidden)
947 |> json(%{error: message})
951 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
952 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
953 {_, true} <- {:followed, follower.id != followed.id},
954 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
956 |> put_view(AccountView)
957 |> render("relationship.json", %{user: follower, target: followed})
964 |> put_status(:forbidden)
965 |> json(%{error: message})
969 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
970 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
971 {_, true} <- {:followed, follower.id != followed.id},
972 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
974 |> put_view(AccountView)
975 |> render("account.json", %{user: followed, for: follower})
982 |> put_status(:forbidden)
983 |> json(%{error: message})
987 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
988 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
989 {_, true} <- {:followed, follower.id != followed.id},
990 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
992 |> put_view(AccountView)
993 |> render("relationship.json", %{user: follower, target: followed})
1003 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1005 if Map.has_key?(params, "notifications"),
1006 do: params["notifications"] in [true, "True", "true", "1"],
1009 with %User{} = muted <- User.get_cached_by_id(id),
1010 {:ok, muter} <- User.mute(muter, muted, notifications) do
1012 |> put_view(AccountView)
1013 |> render("relationship.json", %{user: muter, target: muted})
1015 {:error, message} ->
1017 |> put_status(:forbidden)
1018 |> json(%{error: message})
1022 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1023 with %User{} = muted <- User.get_cached_by_id(id),
1024 {:ok, muter} <- User.unmute(muter, muted) do
1026 |> put_view(AccountView)
1027 |> render("relationship.json", %{user: muter, target: muted})
1029 {:error, message} ->
1031 |> put_status(:forbidden)
1032 |> json(%{error: message})
1036 def mutes(%{assigns: %{user: user}} = conn, _) do
1037 with muted_accounts <- User.muted_users(user) do
1038 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1043 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1044 with %User{} = blocked <- User.get_cached_by_id(id),
1045 {:ok, blocker} <- User.block(blocker, blocked),
1046 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1048 |> put_view(AccountView)
1049 |> render("relationship.json", %{user: blocker, target: blocked})
1051 {:error, message} ->
1053 |> put_status(:forbidden)
1054 |> json(%{error: message})
1058 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1059 with %User{} = blocked <- User.get_cached_by_id(id),
1060 {:ok, blocker} <- User.unblock(blocker, blocked),
1061 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1063 |> put_view(AccountView)
1064 |> render("relationship.json", %{user: blocker, target: blocked})
1066 {:error, message} ->
1068 |> put_status(:forbidden)
1069 |> json(%{error: message})
1073 def blocks(%{assigns: %{user: user}} = conn, _) do
1074 with blocked_accounts <- User.blocked_users(user) do
1075 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1080 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1081 json(conn, info.domain_blocks || [])
1084 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1085 User.block_domain(blocker, domain)
1089 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1090 User.unblock_domain(blocker, domain)
1094 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1095 with %User{} = subscription_target <- User.get_cached_by_id(id),
1096 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1098 |> put_view(AccountView)
1099 |> render("relationship.json", %{user: user, target: subscription_target})
1101 {:error, message} ->
1103 |> put_status(:forbidden)
1104 |> json(%{error: message})
1108 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1109 with %User{} = subscription_target <- User.get_cached_by_id(id),
1110 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1112 |> put_view(AccountView)
1113 |> render("relationship.json", %{user: user, target: subscription_target})
1115 {:error, message} ->
1117 |> put_status(:forbidden)
1118 |> json(%{error: message})
1122 def favourites(%{assigns: %{user: user}} = conn, params) do
1125 |> Map.put("type", "Create")
1126 |> Map.put("favorited_by", user.ap_id)
1127 |> Map.put("blocking_user", user)
1130 ActivityPub.fetch_activities([], params)
1134 |> add_link_headers(activities)
1135 |> put_view(StatusView)
1136 |> render("index.json", %{activities: activities, for: user, as: :activity})
1139 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1140 with %User{} = user <- User.get_by_id(id),
1141 false <- user.info.hide_favorites do
1144 |> Map.put("type", "Create")
1145 |> Map.put("favorited_by", user.ap_id)
1146 |> Map.put("blocking_user", for_user)
1150 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1152 [Pleroma.Constants.as_public()]
1157 |> ActivityPub.fetch_activities(params)
1161 |> add_link_headers(activities)
1162 |> put_view(StatusView)
1163 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1165 nil -> {:error, :not_found}
1166 true -> render_error(conn, :forbidden, "Can't get favorites")
1170 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1171 user = User.get_cached_by_id(user.id)
1174 Bookmark.for_user_query(user.id)
1175 |> Pagination.fetch_paginated(params)
1179 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1182 |> add_link_headers(bookmarks)
1183 |> put_view(StatusView)
1184 |> render("index.json", %{activities: activities, for: user, as: :activity})
1187 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1188 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1189 res = ListView.render("lists.json", lists: lists)
1193 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1194 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1197 |> Map.put("type", "Create")
1198 |> Map.put("blocking_user", user)
1199 |> Map.put("user", user)
1200 |> Map.put("muting_user", user)
1202 # we must filter the following list for the user to avoid leaking statuses the user
1203 # does not actually have permission to see (for more info, peruse security issue #270).
1206 |> Enum.filter(fn x -> x in user.following end)
1207 |> ActivityPub.fetch_activities_bounded(following, params)
1211 |> put_view(StatusView)
1212 |> render("index.json", %{activities: activities, for: user, as: :activity})
1214 _e -> render_error(conn, :forbidden, "Error.")
1218 def index(%{assigns: %{user: user}} = conn, _params) do
1219 token = get_session(conn, :oauth_token)
1222 mastodon_emoji = mastodonized_emoji()
1224 limit = Config.get([:instance, :limit])
1227 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1232 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1233 access_token: token,
1235 domain: Pleroma.Web.Endpoint.host(),
1238 unfollow_modal: false,
1241 auto_play_gif: false,
1242 display_sensitive_media: false,
1243 reduce_motion: false,
1244 max_toot_chars: limit,
1245 mascot: User.get_mascot(user)["url"]
1247 poll_limits: Config.get([:instance, :poll_limits]),
1249 delete_others_notice: present?(user.info.is_moderator),
1250 admin: present?(user.info.is_admin)
1254 default_privacy: user.info.default_scope,
1255 default_sensitive: false,
1256 allow_content_types: Config.get([:instance, :allowed_post_formats])
1258 media_attachments: %{
1259 accept_content_types: [
1275 user.info.settings ||
1305 push_subscription: nil,
1307 custom_emojis: mastodon_emoji,
1313 |> put_layout(false)
1314 |> put_view(MastodonView)
1315 |> render("index.html", %{initial_state: initial_state})
1318 |> put_session(:return_to, conn.request_path)
1319 |> redirect(to: "/web/login")
1323 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1324 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1326 with changeset <- Changeset.change(user),
1327 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1328 {:ok, _user} <- User.update_and_set_cache(changeset) do
1333 |> put_status(:internal_server_error)
1334 |> json(%{error: inspect(e)})
1338 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1339 redirect(conn, to: local_mastodon_root_path(conn))
1342 @doc "Local Mastodon FE login init action"
1343 def login(conn, %{"code" => auth_token}) do
1344 with {:ok, app} <- get_or_make_app(),
1345 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1346 {:ok, token} <- Token.exchange_token(app, auth) do
1348 |> put_session(:oauth_token, token.token)
1349 |> redirect(to: local_mastodon_root_path(conn))
1353 @doc "Local Mastodon FE callback action"
1354 def login(conn, _) do
1355 with {:ok, app} <- get_or_make_app() do
1360 response_type: "code",
1361 client_id: app.client_id,
1363 scope: Enum.join(app.scopes, " ")
1366 redirect(conn, to: path)
1370 defp local_mastodon_root_path(conn) do
1371 case get_session(conn, :return_to) do
1373 mastodon_api_path(conn, :index, ["getting-started"])
1376 delete_session(conn, :return_to)
1381 defp get_or_make_app do
1382 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1383 scopes = ["read", "write", "follow", "push"]
1385 with %App{} = app <- Repo.get_by(App, find_attrs) do
1387 if app.scopes == scopes do
1391 |> Changeset.change(%{scopes: scopes})
1399 App.register_changeset(
1401 Map.put(find_attrs, :scopes, scopes)
1408 def logout(conn, _) do
1411 |> redirect(to: "/")
1414 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1415 Logger.debug("Unimplemented, returning unmodified relationship")
1417 with %User{} = target <- User.get_cached_by_id(id) do
1419 |> put_view(AccountView)
1420 |> render("relationship.json", %{user: user, target: target})
1424 def empty_array(conn, _) do
1425 Logger.debug("Unimplemented, returning an empty array")
1429 def empty_object(conn, _) do
1430 Logger.debug("Unimplemented, returning an empty object")
1434 def get_filters(%{assigns: %{user: user}} = conn, _) do
1435 filters = Filter.get_filters(user)
1436 res = FilterView.render("filters.json", filters: filters)
1441 %{assigns: %{user: user}} = conn,
1442 %{"phrase" => phrase, "context" => context} = params
1448 hide: Map.get(params, "irreversible", false),
1449 whole_word: Map.get(params, "boolean", true)
1453 {:ok, response} = Filter.create(query)
1454 res = FilterView.render("filter.json", filter: response)
1458 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1459 filter = Filter.get(filter_id, user)
1460 res = FilterView.render("filter.json", filter: filter)
1465 %{assigns: %{user: user}} = conn,
1466 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1470 filter_id: filter_id,
1473 hide: Map.get(params, "irreversible", nil),
1474 whole_word: Map.get(params, "boolean", true)
1478 {:ok, response} = Filter.update(query)
1479 res = FilterView.render("filter.json", filter: response)
1483 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1486 filter_id: filter_id
1489 {:ok, _} = Filter.delete(query)
1493 def suggestions(%{assigns: %{user: user}} = conn, _) do
1494 suggestions = Config.get(:suggestions)
1496 if Keyword.get(suggestions, :enabled, false) do
1497 api = Keyword.get(suggestions, :third_party_engine, "")
1498 timeout = Keyword.get(suggestions, :timeout, 5000)
1499 limit = Keyword.get(suggestions, :limit, 23)
1501 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1503 user = user.nickname
1507 |> String.replace("{{host}}", host)
1508 |> String.replace("{{user}}", user)
1510 with {:ok, %{status: 200, body: body}} <-
1511 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1512 {:ok, data} <- Jason.decode(body) do
1515 |> Enum.slice(0, limit)
1518 |> Map.put("id", fetch_suggestion_id(x))
1519 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1520 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1526 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1533 defp fetch_suggestion_id(attrs) do
1534 case User.get_or_fetch(attrs["acct"]) do
1535 {:ok, %User{id: id}} -> id
1540 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1541 with %Activity{} = activity <- Activity.get_by_id(status_id),
1542 true <- Visibility.visible_for_user?(activity, user) do
1546 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1556 def reports(%{assigns: %{user: user}} = conn, params) do
1557 case CommonAPI.report(user, params) do
1560 |> put_view(ReportView)
1561 |> try_render("report.json", %{activity: activity})
1565 |> put_status(:bad_request)
1566 |> json(%{error: err})
1570 def account_register(
1571 %{assigns: %{app: app}} = conn,
1572 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1580 "captcha_answer_data",
1584 |> Map.put("nickname", nickname)
1585 |> Map.put("fullname", params["fullname"] || nickname)
1586 |> Map.put("bio", params["bio"] || "")
1587 |> Map.put("confirm", params["password"])
1589 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1590 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1592 token_type: "Bearer",
1593 access_token: token.token,
1595 created_at: Token.Utils.format_created_at(token)
1600 |> put_status(:bad_request)
1605 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1606 render_error(conn, :bad_request, "Missing parameters")
1609 def account_register(conn, _) do
1610 render_error(conn, :forbidden, "Invalid credentials")
1613 def conversations(%{assigns: %{user: user}} = conn, params) do
1614 participations = Participation.for_user_with_last_activity_id(user, params)
1617 Enum.map(participations, fn participation ->
1618 ConversationView.render("participation.json", %{participation: participation, for: user})
1622 |> add_link_headers(participations)
1623 |> json(conversations)
1626 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1627 with %Participation{} = participation <-
1628 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1629 {:ok, participation} <- Participation.mark_as_read(participation) do
1630 participation_view =
1631 ConversationView.render("participation.json", %{participation: participation, for: user})
1634 |> json(participation_view)
1638 def password_reset(conn, params) do
1639 nickname_or_email = params["email"] || params["nickname"]
1641 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1643 |> put_status(:no_content)
1646 {:error, "unknown user"} ->
1647 send_resp(conn, :not_found, "")
1650 send_resp(conn, :bad_request, "")
1654 def account_confirmation_resend(conn, params) do
1655 nickname_or_email = params["email"] || params["nickname"]
1657 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1658 {:ok, _} <- User.try_send_confirmation_email(user) do
1660 |> json_response(:no_content, "")
1664 def try_render(conn, target, params)
1665 when is_binary(target) do
1666 case render(conn, target, params) do
1667 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1672 def try_render(conn, _, _) do
1673 render_error(conn, :not_implemented, "Can't display this activity")
1676 defp present?(nil), do: false
1677 defp present?(false), do: false
1678 defp present?(_), do: true