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
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(Emoji.Formatter.get_emoji_map(emojis_text))
148 :hide_followers_count,
154 :skip_thread_containment,
157 |> Enum.reduce(%{}, fn key, acc ->
158 add_if_present(acc, params, to_string(key), key, fn value ->
159 {:ok, ControllerHelper.truthy_param?(value)}
162 |> add_if_present(params, "default_scope", :default_scope)
163 |> add_if_present(params, "fields", :fields, fn fields ->
164 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
168 |> add_if_present(params, "fields", :raw_fields)
169 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
170 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
172 |> add_if_present(params, "header", :banner, fn value ->
173 with %Plug.Upload{} <- value,
174 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
180 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
181 with %Plug.Upload{} <- value,
182 {:ok, object} <- ActivityPub.upload(value, type: :background) do
188 |> Map.put(:emoji, user_info_emojis)
192 |> User.update_changeset(user_params)
193 |> User.change_info(&User.Info.profile_update(&1, info_params))
195 with {:ok, user} <- User.update_and_set_cache(changeset) do
196 if original_user != user, do: 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 new_info = %{"banner" => %{}}
228 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
229 CommonAPI.update(user)
230 json(conn, %{url: nil})
234 def update_banner(%{assigns: %{user: user}} = conn, params) do
235 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
236 new_info <- %{"banner" => object.data},
237 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
238 CommonAPI.update(user)
239 %{"url" => [%{"href" => href} | _]} = object.data
241 json(conn, %{url: href})
245 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
246 new_info = %{"background" => %{}}
248 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
249 json(conn, %{url: nil})
253 def update_background(%{assigns: %{user: user}} = conn, params) do
254 with {:ok, object} <- ActivityPub.upload(params, type: :background),
255 new_info <- %{"background" => object.data},
256 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
257 %{"url" => [%{"href" => href} | _]} = object.data
259 json(conn, %{url: href})
263 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
264 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
267 AccountView.render("account.json", %{
270 with_pleroma_settings: true,
271 with_chat_token: chat_token
277 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
278 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
281 |> render("short.json", %{app: app})
285 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
286 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
287 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
288 account = AccountView.render("account.json", %{user: user, for: for_user})
291 _e -> render_error(conn, :not_found, "Can't find user")
295 @mastodon_api_level "2.7.2"
297 def masto_instance(conn, _params) do
298 instance = Config.get(:instance)
302 title: Keyword.get(instance, :name),
303 description: Keyword.get(instance, :description),
304 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
305 email: Keyword.get(instance, :email),
307 streaming_api: Pleroma.Web.Endpoint.websocket_url()
309 stats: Stats.get_stats(),
310 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
312 registrations: Pleroma.Config.get([:instance, :registrations_open]),
313 # Extra (not present in Mastodon):
314 max_toot_chars: Keyword.get(instance, :limit),
315 poll_limits: Keyword.get(instance, :poll_limits)
321 def peers(conn, _params) do
322 json(conn, Stats.get_peers())
325 defp mastodonized_emoji do
326 Pleroma.Emoji.get_all()
327 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
328 url = to_string(URI.merge(Web.base_url(), relative_url))
331 "shortcode" => shortcode,
333 "visible_in_picker" => true,
336 # Assuming that a comma is authorized in the category name
337 "category" => (tags -- ["Custom"]) |> Enum.join(",")
342 def custom_emojis(conn, _params) do
343 mastodon_emoji = mastodonized_emoji()
344 json(conn, mastodon_emoji)
347 def home_timeline(%{assigns: %{user: user}} = conn, params) do
350 |> Map.put("type", ["Create", "Announce"])
351 |> Map.put("blocking_user", user)
352 |> Map.put("muting_user", user)
353 |> Map.put("user", user)
356 [user.ap_id | user.following]
357 |> ActivityPub.fetch_activities(params)
361 |> add_link_headers(activities)
362 |> put_view(StatusView)
363 |> render("index.json", %{activities: activities, for: user, as: :activity})
366 def public_timeline(%{assigns: %{user: user}} = conn, params) do
367 local_only = params["local"] in [true, "True", "true", "1"]
371 |> Map.put("type", ["Create", "Announce"])
372 |> Map.put("local_only", local_only)
373 |> Map.put("blocking_user", user)
374 |> Map.put("muting_user", user)
375 |> ActivityPub.fetch_public_activities()
379 |> add_link_headers(activities, %{"local" => local_only})
380 |> put_view(StatusView)
381 |> render("index.json", %{activities: activities, for: user, as: :activity})
384 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
385 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
388 |> Map.put("tag", params["tagged"])
390 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
393 |> add_link_headers(activities)
394 |> put_view(StatusView)
395 |> render("index.json", %{
396 activities: activities,
403 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
406 |> Map.put("type", "Create")
407 |> Map.put("blocking_user", user)
408 |> Map.put("user", user)
409 |> Map.put(:visibility, "direct")
413 |> ActivityPub.fetch_activities_query(params)
414 |> Pagination.fetch_paginated(params)
417 |> add_link_headers(activities)
418 |> put_view(StatusView)
419 |> render("index.json", %{activities: activities, for: user, as: :activity})
422 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
428 |> Activity.all_by_ids_with_object()
429 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
432 |> put_view(StatusView)
433 |> render("index.json", activities: activities, for: user, as: :activity)
436 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
437 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
438 true <- Visibility.visible_for_user?(activity, user) do
440 |> put_view(StatusView)
441 |> try_render("status.json", %{activity: activity, for: user})
445 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
446 with %Activity{} = activity <- Activity.get_by_id(id),
448 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
449 "blocking_user" => user,
451 "exclude_id" => activity.id
453 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
459 activities: grouped_activities[true] || [],
463 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
468 activities: grouped_activities[false] || [],
472 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
479 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
480 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
481 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
482 true <- Visibility.visible_for_user?(activity, user) do
484 |> put_view(StatusView)
485 |> try_render("poll.json", %{object: object, for: user})
487 error when is_nil(error) or error == false ->
488 render_error(conn, :not_found, "Record not found")
492 defp get_cached_vote_or_vote(user, object, choices) do
493 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
496 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
497 case CommonAPI.vote(user, object, choices) do
498 {:error, _message} = res -> {:ignore, res}
499 res -> {:commit, res}
506 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
507 with %Object{} = object <- Object.get_by_id(id),
508 true <- object.data["type"] == "Question",
509 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
510 true <- Visibility.visible_for_user?(activity, user),
511 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
513 |> put_view(StatusView)
514 |> try_render("poll.json", %{object: object, for: user})
517 render_error(conn, :not_found, "Record not found")
520 render_error(conn, :not_found, "Record not found")
524 |> put_status(:unprocessable_entity)
525 |> json(%{error: message})
529 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
530 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
532 |> add_link_headers(scheduled_activities)
533 |> put_view(ScheduledActivityView)
534 |> render("index.json", %{scheduled_activities: scheduled_activities})
538 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
539 with %ScheduledActivity{} = scheduled_activity <-
540 ScheduledActivity.get(user, scheduled_activity_id) do
542 |> put_view(ScheduledActivityView)
543 |> render("show.json", %{scheduled_activity: scheduled_activity})
545 _ -> {:error, :not_found}
549 def update_scheduled_status(
550 %{assigns: %{user: user}} = conn,
551 %{"id" => scheduled_activity_id} = params
553 with %ScheduledActivity{} = scheduled_activity <-
554 ScheduledActivity.get(user, scheduled_activity_id),
555 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
557 |> put_view(ScheduledActivityView)
558 |> render("show.json", %{scheduled_activity: scheduled_activity})
560 nil -> {:error, :not_found}
565 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
566 with %ScheduledActivity{} = scheduled_activity <-
567 ScheduledActivity.get(user, scheduled_activity_id),
568 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
570 |> put_view(ScheduledActivityView)
571 |> render("show.json", %{scheduled_activity: scheduled_activity})
573 nil -> {:error, :not_found}
578 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
581 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
583 scheduled_at = params["scheduled_at"]
585 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
586 with {:ok, scheduled_activity} <-
587 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
589 |> put_view(ScheduledActivityView)
590 |> render("show.json", %{scheduled_activity: scheduled_activity})
593 params = Map.drop(params, ["scheduled_at"])
595 case CommonAPI.post(user, params) do
598 |> put_status(:unprocessable_entity)
599 |> json(%{error: message})
603 |> put_view(StatusView)
604 |> try_render("status.json", %{
608 with_direct_conversation_id: true
614 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
615 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
618 _e -> render_error(conn, :forbidden, "Can't delete this post")
622 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
623 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
624 %Activity{} = announce <- Activity.normalize(announce.data) do
626 |> put_view(StatusView)
627 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
631 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
632 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
633 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
635 |> put_view(StatusView)
636 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
640 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
641 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
642 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
644 |> put_view(StatusView)
645 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
649 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
650 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
651 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
653 |> put_view(StatusView)
654 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
658 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
659 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
661 |> put_view(StatusView)
662 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
666 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
667 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
675 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
676 %User{} = user <- User.get_cached_by_nickname(user.nickname),
677 true <- Visibility.visible_for_user?(activity, user),
678 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
680 |> put_view(StatusView)
681 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
685 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
686 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
687 %User{} = user <- User.get_cached_by_nickname(user.nickname),
688 true <- Visibility.visible_for_user?(activity, user),
689 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
691 |> put_view(StatusView)
692 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
696 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
697 activity = Activity.get_by_id(id)
699 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
701 |> put_view(StatusView)
702 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
706 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
707 activity = Activity.get_by_id(id)
709 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
711 |> put_view(StatusView)
712 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
716 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
718 q = from(u in User, where: u.id in ^id)
719 targets = Repo.all(q)
722 |> put_view(AccountView)
723 |> render("relationships.json", %{user: user, targets: targets})
726 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
727 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
729 def update_media(%{assigns: %{user: user}} = conn, data) do
730 with %Object{} = object <- Repo.get(Object, data["id"]),
731 true <- Object.authorize_mutation(object, user),
732 true <- is_binary(data["description"]),
733 description <- data["description"] do
734 new_data = %{object.data | "name" => description}
738 |> Object.change(%{data: new_data})
741 attachment_data = Map.put(new_data, "id", object.id)
744 |> put_view(StatusView)
745 |> render("attachment.json", %{attachment: attachment_data})
749 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
750 with {:ok, object} <-
753 actor: User.ap_id(user),
754 description: Map.get(data, "description")
756 attachment_data = Map.put(object.data, "id", object.id)
759 |> put_view(StatusView)
760 |> render("attachment.json", %{attachment: attachment_data})
764 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
765 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
766 %{} = attachment_data <- Map.put(object.data, "id", object.id),
767 # Reject if not an image
768 %{type: "image"} = rendered <-
769 StatusView.render("attachment.json", %{attachment: attachment_data}) do
771 # Save to the user's info
772 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
776 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
780 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
781 mascot = User.get_mascot(user)
787 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
788 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
789 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
790 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
791 q = from(u in User, where: u.ap_id in ^likes)
795 |> Enum.filter(&(not User.blocks?(user, &1)))
798 |> put_view(AccountView)
799 |> render("accounts.json", %{for: user, users: users, as: :user})
801 {:visible, false} -> {:error, :not_found}
806 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
807 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
808 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
809 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
810 q = from(u in User, where: u.ap_id in ^announces)
814 |> Enum.filter(&(not User.blocks?(user, &1)))
817 |> put_view(AccountView)
818 |> render("accounts.json", %{for: user, users: users, as: :user})
820 {:visible, false} -> {:error, :not_found}
825 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
826 local_only = params["local"] in [true, "True", "true", "1"]
829 [params["tag"], params["any"]]
833 |> Enum.map(&String.downcase(&1))
838 |> Enum.map(&String.downcase(&1))
843 |> Enum.map(&String.downcase(&1))
847 |> Map.put("type", "Create")
848 |> Map.put("local_only", local_only)
849 |> Map.put("blocking_user", user)
850 |> Map.put("muting_user", user)
851 |> Map.put("user", user)
852 |> Map.put("tag", tags)
853 |> Map.put("tag_all", tag_all)
854 |> Map.put("tag_reject", tag_reject)
855 |> ActivityPub.fetch_public_activities()
859 |> add_link_headers(activities, %{"local" => local_only})
860 |> put_view(StatusView)
861 |> render("index.json", %{activities: activities, for: user, as: :activity})
864 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
865 with %User{} = user <- User.get_cached_by_id(id),
866 followers <- MastodonAPI.get_followers(user, params) do
869 for_user && user.id == for_user.id -> followers
870 user.info.hide_followers -> []
875 |> add_link_headers(followers)
876 |> put_view(AccountView)
877 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
881 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
882 with %User{} = user <- User.get_cached_by_id(id),
883 followers <- MastodonAPI.get_friends(user, params) do
886 for_user && user.id == for_user.id -> followers
887 user.info.hide_follows -> []
892 |> add_link_headers(followers)
893 |> put_view(AccountView)
894 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
898 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
899 follow_requests = User.get_follow_requests(followed)
902 |> put_view(AccountView)
903 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
906 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
907 with %User{} = follower <- User.get_cached_by_id(id),
908 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
910 |> put_view(AccountView)
911 |> render("relationship.json", %{user: followed, target: follower})
915 |> put_status(:forbidden)
916 |> json(%{error: message})
920 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
921 with %User{} = follower <- User.get_cached_by_id(id),
922 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
924 |> put_view(AccountView)
925 |> render("relationship.json", %{user: followed, target: follower})
929 |> put_status(:forbidden)
930 |> json(%{error: message})
934 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
935 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
936 {_, true} <- {:followed, follower.id != followed.id},
937 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
939 |> put_view(AccountView)
940 |> render("relationship.json", %{user: follower, target: followed})
947 |> put_status(:forbidden)
948 |> json(%{error: message})
952 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
953 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
954 {_, true} <- {:followed, follower.id != followed.id},
955 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
957 |> put_view(AccountView)
958 |> render("account.json", %{user: followed, for: follower})
965 |> put_status(:forbidden)
966 |> json(%{error: message})
970 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
971 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
972 {_, true} <- {:followed, follower.id != followed.id},
973 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
975 |> put_view(AccountView)
976 |> render("relationship.json", %{user: follower, target: followed})
986 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
988 if Map.has_key?(params, "notifications"),
989 do: params["notifications"] in [true, "True", "true", "1"],
992 with %User{} = muted <- User.get_cached_by_id(id),
993 {:ok, muter} <- User.mute(muter, muted, notifications) do
995 |> put_view(AccountView)
996 |> render("relationship.json", %{user: muter, target: muted})
1000 |> put_status(:forbidden)
1001 |> json(%{error: message})
1005 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1006 with %User{} = muted <- User.get_cached_by_id(id),
1007 {:ok, muter} <- User.unmute(muter, muted) do
1009 |> put_view(AccountView)
1010 |> render("relationship.json", %{user: muter, target: muted})
1012 {:error, message} ->
1014 |> put_status(:forbidden)
1015 |> json(%{error: message})
1019 def mutes(%{assigns: %{user: user}} = conn, _) do
1020 with muted_accounts <- User.muted_users(user) do
1021 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1026 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1027 with %User{} = blocked <- User.get_cached_by_id(id),
1028 {:ok, blocker} <- User.block(blocker, blocked),
1029 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1031 |> put_view(AccountView)
1032 |> render("relationship.json", %{user: blocker, target: blocked})
1034 {:error, message} ->
1036 |> put_status(:forbidden)
1037 |> json(%{error: message})
1041 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1042 with %User{} = blocked <- User.get_cached_by_id(id),
1043 {:ok, blocker} <- User.unblock(blocker, blocked),
1044 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1046 |> put_view(AccountView)
1047 |> render("relationship.json", %{user: blocker, target: blocked})
1049 {:error, message} ->
1051 |> put_status(:forbidden)
1052 |> json(%{error: message})
1056 def blocks(%{assigns: %{user: user}} = conn, _) do
1057 with blocked_accounts <- User.blocked_users(user) do
1058 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1063 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1064 json(conn, info.domain_blocks || [])
1067 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1068 User.block_domain(blocker, domain)
1072 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1073 User.unblock_domain(blocker, domain)
1077 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1078 with %User{} = subscription_target <- User.get_cached_by_id(id),
1079 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1081 |> put_view(AccountView)
1082 |> render("relationship.json", %{user: user, target: subscription_target})
1084 {:error, message} ->
1086 |> put_status(:forbidden)
1087 |> json(%{error: message})
1091 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1092 with %User{} = subscription_target <- User.get_cached_by_id(id),
1093 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1095 |> put_view(AccountView)
1096 |> render("relationship.json", %{user: user, target: subscription_target})
1098 {:error, message} ->
1100 |> put_status(:forbidden)
1101 |> json(%{error: message})
1105 def favourites(%{assigns: %{user: user}} = conn, params) do
1108 |> Map.put("type", "Create")
1109 |> Map.put("favorited_by", user.ap_id)
1110 |> Map.put("blocking_user", user)
1113 ActivityPub.fetch_activities([], params)
1117 |> add_link_headers(activities)
1118 |> put_view(StatusView)
1119 |> render("index.json", %{activities: activities, for: user, as: :activity})
1122 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1123 with %User{} = user <- User.get_by_id(id),
1124 false <- user.info.hide_favorites do
1127 |> Map.put("type", "Create")
1128 |> Map.put("favorited_by", user.ap_id)
1129 |> Map.put("blocking_user", for_user)
1133 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1135 [Pleroma.Constants.as_public()]
1140 |> ActivityPub.fetch_activities(params)
1144 |> add_link_headers(activities)
1145 |> put_view(StatusView)
1146 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1148 nil -> {:error, :not_found}
1149 true -> render_error(conn, :forbidden, "Can't get favorites")
1153 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1154 user = User.get_cached_by_id(user.id)
1157 Bookmark.for_user_query(user.id)
1158 |> Pagination.fetch_paginated(params)
1162 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1165 |> add_link_headers(bookmarks)
1166 |> put_view(StatusView)
1167 |> render("index.json", %{activities: activities, for: user, as: :activity})
1170 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1171 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1172 res = ListView.render("lists.json", lists: lists)
1176 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1177 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1180 |> Map.put("type", "Create")
1181 |> Map.put("blocking_user", user)
1182 |> Map.put("user", user)
1183 |> Map.put("muting_user", user)
1185 # we must filter the following list for the user to avoid leaking statuses the user
1186 # does not actually have permission to see (for more info, peruse security issue #270).
1189 |> Enum.filter(fn x -> x in user.following end)
1190 |> ActivityPub.fetch_activities_bounded(following, params)
1194 |> put_view(StatusView)
1195 |> render("index.json", %{activities: activities, for: user, as: :activity})
1197 _e -> render_error(conn, :forbidden, "Error.")
1201 def index(%{assigns: %{user: user}} = conn, _params) do
1202 token = get_session(conn, :oauth_token)
1205 mastodon_emoji = mastodonized_emoji()
1207 limit = Config.get([:instance, :limit])
1210 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1215 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1216 access_token: token,
1218 domain: Pleroma.Web.Endpoint.host(),
1221 unfollow_modal: false,
1224 auto_play_gif: false,
1225 display_sensitive_media: false,
1226 reduce_motion: false,
1227 max_toot_chars: limit,
1228 mascot: User.get_mascot(user)["url"]
1230 poll_limits: Config.get([:instance, :poll_limits]),
1232 delete_others_notice: present?(user.info.is_moderator),
1233 admin: present?(user.info.is_admin)
1237 default_privacy: user.info.default_scope,
1238 default_sensitive: false,
1239 allow_content_types: Config.get([:instance, :allowed_post_formats])
1241 media_attachments: %{
1242 accept_content_types: [
1258 user.info.settings ||
1288 push_subscription: nil,
1290 custom_emojis: mastodon_emoji,
1296 |> put_layout(false)
1297 |> put_view(MastodonView)
1298 |> render("index.html", %{initial_state: initial_state})
1301 |> put_session(:return_to, conn.request_path)
1302 |> redirect(to: "/web/login")
1306 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1307 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
1312 |> put_status(:internal_server_error)
1313 |> json(%{error: inspect(e)})
1317 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1318 redirect(conn, to: local_mastodon_root_path(conn))
1321 @doc "Local Mastodon FE login init action"
1322 def login(conn, %{"code" => auth_token}) do
1323 with {:ok, app} <- get_or_make_app(),
1324 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1325 {:ok, token} <- Token.exchange_token(app, auth) do
1327 |> put_session(:oauth_token, token.token)
1328 |> redirect(to: local_mastodon_root_path(conn))
1332 @doc "Local Mastodon FE callback action"
1333 def login(conn, _) do
1334 with {:ok, app} <- get_or_make_app() do
1339 response_type: "code",
1340 client_id: app.client_id,
1342 scope: Enum.join(app.scopes, " ")
1345 redirect(conn, to: path)
1349 defp local_mastodon_root_path(conn) do
1350 case get_session(conn, :return_to) do
1352 mastodon_api_path(conn, :index, ["getting-started"])
1355 delete_session(conn, :return_to)
1360 defp get_or_make_app do
1361 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1362 scopes = ["read", "write", "follow", "push"]
1364 with %App{} = app <- Repo.get_by(App, find_attrs) do
1366 if app.scopes == scopes do
1370 |> Changeset.change(%{scopes: scopes})
1378 App.register_changeset(
1380 Map.put(find_attrs, :scopes, scopes)
1387 def logout(conn, _) do
1390 |> redirect(to: "/")
1393 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1394 Logger.debug("Unimplemented, returning unmodified relationship")
1396 with %User{} = target <- User.get_cached_by_id(id) do
1398 |> put_view(AccountView)
1399 |> render("relationship.json", %{user: user, target: target})
1403 def empty_array(conn, _) do
1404 Logger.debug("Unimplemented, returning an empty array")
1408 def empty_object(conn, _) do
1409 Logger.debug("Unimplemented, returning an empty object")
1413 def get_filters(%{assigns: %{user: user}} = conn, _) do
1414 filters = Filter.get_filters(user)
1415 res = FilterView.render("filters.json", filters: filters)
1420 %{assigns: %{user: user}} = conn,
1421 %{"phrase" => phrase, "context" => context} = params
1427 hide: Map.get(params, "irreversible", false),
1428 whole_word: Map.get(params, "boolean", true)
1432 {:ok, response} = Filter.create(query)
1433 res = FilterView.render("filter.json", filter: response)
1437 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1438 filter = Filter.get(filter_id, user)
1439 res = FilterView.render("filter.json", filter: filter)
1444 %{assigns: %{user: user}} = conn,
1445 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1449 filter_id: filter_id,
1452 hide: Map.get(params, "irreversible", nil),
1453 whole_word: Map.get(params, "boolean", true)
1457 {:ok, response} = Filter.update(query)
1458 res = FilterView.render("filter.json", filter: response)
1462 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1465 filter_id: filter_id
1468 {:ok, _} = Filter.delete(query)
1472 def suggestions(%{assigns: %{user: user}} = conn, _) do
1473 suggestions = Config.get(:suggestions)
1475 if Keyword.get(suggestions, :enabled, false) do
1476 api = Keyword.get(suggestions, :third_party_engine, "")
1477 timeout = Keyword.get(suggestions, :timeout, 5000)
1478 limit = Keyword.get(suggestions, :limit, 23)
1480 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1482 user = user.nickname
1486 |> String.replace("{{host}}", host)
1487 |> String.replace("{{user}}", user)
1489 with {:ok, %{status: 200, body: body}} <-
1490 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1491 {:ok, data} <- Jason.decode(body) do
1494 |> Enum.slice(0, limit)
1497 |> Map.put("id", fetch_suggestion_id(x))
1498 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1499 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1505 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1512 defp fetch_suggestion_id(attrs) do
1513 case User.get_or_fetch(attrs["acct"]) do
1514 {:ok, %User{id: id}} -> id
1519 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1520 with %Activity{} = activity <- Activity.get_by_id(status_id),
1521 true <- Visibility.visible_for_user?(activity, user) do
1525 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1535 def reports(%{assigns: %{user: user}} = conn, params) do
1536 case CommonAPI.report(user, params) do
1539 |> put_view(ReportView)
1540 |> try_render("report.json", %{activity: activity})
1544 |> put_status(:bad_request)
1545 |> json(%{error: err})
1549 def account_register(
1550 %{assigns: %{app: app}} = conn,
1551 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1559 "captcha_answer_data",
1563 |> Map.put("nickname", nickname)
1564 |> Map.put("fullname", params["fullname"] || nickname)
1565 |> Map.put("bio", params["bio"] || "")
1566 |> Map.put("confirm", params["password"])
1568 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1569 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1571 token_type: "Bearer",
1572 access_token: token.token,
1574 created_at: Token.Utils.format_created_at(token)
1579 |> put_status(:bad_request)
1584 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1585 render_error(conn, :bad_request, "Missing parameters")
1588 def account_register(conn, _) do
1589 render_error(conn, :forbidden, "Invalid credentials")
1592 def conversations(%{assigns: %{user: user}} = conn, params) do
1593 participations = Participation.for_user_with_last_activity_id(user, params)
1596 Enum.map(participations, fn participation ->
1597 ConversationView.render("participation.json", %{participation: participation, for: user})
1601 |> add_link_headers(participations)
1602 |> json(conversations)
1605 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1606 with %Participation{} = participation <-
1607 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1608 {:ok, participation} <- Participation.mark_as_read(participation) do
1609 participation_view =
1610 ConversationView.render("participation.json", %{participation: participation, for: user})
1613 |> json(participation_view)
1617 def password_reset(conn, params) do
1618 nickname_or_email = params["email"] || params["nickname"]
1620 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1622 |> put_status(:no_content)
1625 {:error, "unknown user"} ->
1626 send_resp(conn, :not_found, "")
1629 send_resp(conn, :bad_request, "")
1633 def account_confirmation_resend(conn, params) do
1634 nickname_or_email = params["email"] || params["nickname"]
1636 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1637 {:ok, _} <- User.try_send_confirmation_email(user) do
1639 |> json_response(:no_content, "")
1643 def try_render(conn, target, params)
1644 when is_binary(target) do
1645 case render(conn, target, params) do
1646 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1651 def try_render(conn, _, _) do
1652 render_error(conn, :not_implemented, "Can't display this activity")
1655 defp present?(nil), do: false
1656 defp present?(false), do: false
1657 defp present?(_), do: true