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
19 alias Pleroma.Notification
21 alias Pleroma.Pagination
22 alias Pleroma.Plugs.RateLimiter
24 alias Pleroma.ScheduledActivity
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Visibility
30 alias Pleroma.Web.CommonAPI
31 alias Pleroma.Web.MastodonAPI.AccountView
32 alias Pleroma.Web.MastodonAPI.AppView
33 alias Pleroma.Web.MastodonAPI.ConversationView
34 alias Pleroma.Web.MastodonAPI.FilterView
35 alias Pleroma.Web.MastodonAPI.ListView
36 alias Pleroma.Web.MastodonAPI.MastodonAPI
37 alias Pleroma.Web.MastodonAPI.MastodonView
38 alias Pleroma.Web.MastodonAPI.NotificationView
39 alias Pleroma.Web.MastodonAPI.ReportView
40 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
41 alias Pleroma.Web.MastodonAPI.StatusView
42 alias Pleroma.Web.MediaProxy
43 alias Pleroma.Web.OAuth.App
44 alias Pleroma.Web.OAuth.Authorization
45 alias Pleroma.Web.OAuth.Scopes
46 alias Pleroma.Web.OAuth.Token
47 alias Pleroma.Web.TwitterAPI.TwitterAPI
49 alias Pleroma.Web.ControllerHelper
53 require Pleroma.Constants
55 @rate_limited_relations_actions ~w(follow unfollow)a
57 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
58 post_status delete_status)a
62 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
63 when action in ~w(reblog_status unreblog_status)a
68 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
69 when action in ~w(fav_status unfav_status)a
74 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
77 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
78 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
79 plug(RateLimiter, :app_account_creation when action == :account_register)
80 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
81 plug(RateLimiter, :password_reset when action == :password_reset)
82 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
84 @local_mastodon_name "Mastodon-Local"
86 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
88 def create_app(conn, params) do
89 scopes = Scopes.fetch_scopes(params, ["read"])
93 |> Map.drop(["scope", "scopes"])
94 |> Map.put("scopes", scopes)
96 with cs <- App.register_changeset(%App{}, app_attrs),
97 false <- cs.changes[:client_name] == @local_mastodon_name,
98 {:ok, app} <- Repo.insert(cs) do
101 |> render("show.json", %{app: app})
110 value_function \\ fn x -> {:ok, x} end
112 if Map.has_key?(params, params_field) do
113 case value_function.(params[params_field]) do
114 {:ok, new_value} -> Map.put(map, map_field, new_value)
122 def update_credentials(%{assigns: %{user: user}} = conn, params) do
127 |> add_if_present(params, "display_name", :name)
128 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
129 |> add_if_present(params, "avatar", :avatar, fn value ->
130 with %Plug.Upload{} <- value,
131 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
138 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
142 |> Map.get(:emoji, [])
143 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
150 :hide_followers_count,
156 :skip_thread_containment,
159 |> Enum.reduce(%{}, fn key, acc ->
160 add_if_present(acc, params, to_string(key), key, fn value ->
161 {:ok, ControllerHelper.truthy_param?(value)}
164 |> add_if_present(params, "default_scope", :default_scope)
165 |> add_if_present(params, "fields", :fields, fn fields ->
166 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
170 |> add_if_present(params, "fields", :raw_fields)
171 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
172 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
174 |> add_if_present(params, "header", :banner, fn value ->
175 with %Plug.Upload{} <- value,
176 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
182 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
183 with %Plug.Upload{} <- value,
184 {:ok, object} <- ActivityPub.upload(value, type: :background) do
190 |> Map.put(:emoji, user_info_emojis)
192 info_cng = User.Info.profile_update(user.info, info_params)
194 with changeset <- User.update_changeset(user, user_params),
195 changeset <- Changeset.put_embed(changeset, :info, info_cng),
196 {:ok, user} <- User.update_and_set_cache(changeset) do
197 if original_user != user do
198 CommonAPI.update(user)
203 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
206 _e -> render_error(conn, :forbidden, "Invalid request")
210 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
211 change = Changeset.change(user, %{avatar: nil})
212 {:ok, user} = User.update_and_set_cache(change)
213 CommonAPI.update(user)
215 json(conn, %{url: nil})
218 def update_avatar(%{assigns: %{user: user}} = conn, params) do
219 {:ok, object} = ActivityPub.upload(params, type: :avatar)
220 change = Changeset.change(user, %{avatar: object.data})
221 {:ok, user} = User.update_and_set_cache(change)
222 CommonAPI.update(user)
223 %{"url" => [%{"href" => href} | _]} = object.data
225 json(conn, %{url: href})
228 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
229 with new_info <- %{"banner" => %{}},
230 info_cng <- User.Info.profile_update(user.info, new_info),
231 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
232 {:ok, user} <- User.update_and_set_cache(changeset) do
233 CommonAPI.update(user)
235 json(conn, %{url: nil})
239 def update_banner(%{assigns: %{user: user}} = conn, params) do
240 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
241 new_info <- %{"banner" => object.data},
242 info_cng <- User.Info.profile_update(user.info, new_info),
243 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
244 {:ok, user} <- User.update_and_set_cache(changeset) do
245 CommonAPI.update(user)
246 %{"url" => [%{"href" => href} | _]} = object.data
248 json(conn, %{url: href})
252 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
253 with new_info <- %{"background" => %{}},
254 info_cng <- User.Info.profile_update(user.info, new_info),
255 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
256 {:ok, _user} <- User.update_and_set_cache(changeset) do
257 json(conn, %{url: nil})
261 def update_background(%{assigns: %{user: user}} = conn, params) do
262 with {:ok, object} <- ActivityPub.upload(params, type: :background),
263 new_info <- %{"background" => object.data},
264 info_cng <- User.Info.profile_update(user.info, new_info),
265 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
266 {:ok, _user} <- User.update_and_set_cache(changeset) do
267 %{"url" => [%{"href" => href} | _]} = object.data
269 json(conn, %{url: href})
273 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
274 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
277 AccountView.render("account.json", %{
280 with_pleroma_settings: true,
281 with_chat_token: chat_token
287 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
288 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
291 |> render("short.json", %{app: app})
295 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
296 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
297 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
298 account = AccountView.render("account.json", %{user: user, for: for_user})
301 _e -> render_error(conn, :not_found, "Can't find user")
305 @mastodon_api_level "2.7.2"
307 def masto_instance(conn, _params) do
308 instance = Config.get(:instance)
312 title: Keyword.get(instance, :name),
313 description: Keyword.get(instance, :description),
314 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
315 email: Keyword.get(instance, :email),
317 streaming_api: Pleroma.Web.Endpoint.websocket_url()
319 stats: Stats.get_stats(),
320 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
322 registrations: Pleroma.Config.get([:instance, :registrations_open]),
323 # Extra (not present in Mastodon):
324 max_toot_chars: Keyword.get(instance, :limit),
325 poll_limits: Keyword.get(instance, :poll_limits)
331 def peers(conn, _params) do
332 json(conn, Stats.get_peers())
335 defp mastodonized_emoji do
336 Pleroma.Emoji.get_all()
337 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
338 url = to_string(URI.merge(Web.base_url(), relative_url))
341 "shortcode" => shortcode,
343 "visible_in_picker" => true,
346 # Assuming that a comma is authorized in the category name
347 "category" => (tags -- ["Custom"]) |> Enum.join(",")
352 def custom_emojis(conn, _params) do
353 mastodon_emoji = mastodonized_emoji()
354 json(conn, mastodon_emoji)
357 def home_timeline(%{assigns: %{user: user}} = conn, params) do
360 |> Map.put("type", ["Create", "Announce"])
361 |> Map.put("blocking_user", user)
362 |> Map.put("muting_user", user)
363 |> Map.put("user", user)
366 [user.ap_id | user.following]
367 |> ActivityPub.fetch_activities(params)
371 |> add_link_headers(activities)
372 |> put_view(StatusView)
373 |> render("index.json", %{activities: activities, for: user, as: :activity})
376 def public_timeline(%{assigns: %{user: user}} = conn, params) do
377 local_only = params["local"] in [true, "True", "true", "1"]
381 |> Map.put("type", ["Create", "Announce"])
382 |> Map.put("local_only", local_only)
383 |> Map.put("blocking_user", user)
384 |> Map.put("muting_user", user)
385 |> ActivityPub.fetch_public_activities()
389 |> add_link_headers(activities, %{"local" => local_only})
390 |> put_view(StatusView)
391 |> render("index.json", %{activities: activities, for: user, as: :activity})
394 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
395 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
398 |> Map.put("tag", params["tagged"])
400 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
403 |> add_link_headers(activities)
404 |> put_view(StatusView)
405 |> render("index.json", %{
406 activities: activities,
413 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
416 |> Map.put("type", "Create")
417 |> Map.put("blocking_user", user)
418 |> Map.put("user", user)
419 |> Map.put(:visibility, "direct")
423 |> ActivityPub.fetch_activities_query(params)
424 |> Pagination.fetch_paginated(params)
427 |> add_link_headers(activities)
428 |> put_view(StatusView)
429 |> render("index.json", %{activities: activities, for: user, as: :activity})
432 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
438 |> Activity.all_by_ids_with_object()
439 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
442 |> put_view(StatusView)
443 |> render("index.json", activities: activities, for: user, as: :activity)
446 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
447 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
448 true <- Visibility.visible_for_user?(activity, user) do
450 |> put_view(StatusView)
451 |> try_render("status.json", %{activity: activity, for: user})
455 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
456 with %Activity{} = activity <- Activity.get_by_id(id),
458 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
459 "blocking_user" => user,
461 "exclude_id" => activity.id
463 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
469 activities: grouped_activities[true] || [],
473 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
478 activities: grouped_activities[false] || [],
482 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
489 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
490 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
491 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
492 true <- Visibility.visible_for_user?(activity, user) do
494 |> put_view(StatusView)
495 |> try_render("poll.json", %{object: object, for: user})
497 error when is_nil(error) or error == false ->
498 render_error(conn, :not_found, "Record not found")
502 defp get_cached_vote_or_vote(user, object, choices) do
503 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
506 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
507 case CommonAPI.vote(user, object, choices) do
508 {:error, _message} = res -> {:ignore, res}
509 res -> {:commit, res}
516 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
517 with %Object{} = object <- Object.get_by_id(id),
518 true <- object.data["type"] == "Question",
519 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
520 true <- Visibility.visible_for_user?(activity, user),
521 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
523 |> put_view(StatusView)
524 |> try_render("poll.json", %{object: object, for: user})
527 render_error(conn, :not_found, "Record not found")
530 render_error(conn, :not_found, "Record not found")
534 |> put_status(:unprocessable_entity)
535 |> json(%{error: message})
539 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
540 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
542 |> add_link_headers(scheduled_activities)
543 |> put_view(ScheduledActivityView)
544 |> render("index.json", %{scheduled_activities: scheduled_activities})
548 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
549 with %ScheduledActivity{} = scheduled_activity <-
550 ScheduledActivity.get(user, scheduled_activity_id) do
552 |> put_view(ScheduledActivityView)
553 |> render("show.json", %{scheduled_activity: scheduled_activity})
555 _ -> {:error, :not_found}
559 def update_scheduled_status(
560 %{assigns: %{user: user}} = conn,
561 %{"id" => scheduled_activity_id} = params
563 with %ScheduledActivity{} = scheduled_activity <-
564 ScheduledActivity.get(user, scheduled_activity_id),
565 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
567 |> put_view(ScheduledActivityView)
568 |> render("show.json", %{scheduled_activity: scheduled_activity})
570 nil -> {:error, :not_found}
575 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
576 with %ScheduledActivity{} = scheduled_activity <-
577 ScheduledActivity.get(user, scheduled_activity_id),
578 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
580 |> put_view(ScheduledActivityView)
581 |> render("show.json", %{scheduled_activity: scheduled_activity})
583 nil -> {:error, :not_found}
588 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
591 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
593 scheduled_at = params["scheduled_at"]
595 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
596 with {:ok, scheduled_activity} <-
597 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
599 |> put_view(ScheduledActivityView)
600 |> render("show.json", %{scheduled_activity: scheduled_activity})
603 params = Map.drop(params, ["scheduled_at"])
605 case CommonAPI.post(user, params) do
608 |> put_status(:unprocessable_entity)
609 |> json(%{error: message})
613 |> put_view(StatusView)
614 |> try_render("status.json", %{
618 with_direct_conversation_id: true
624 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
625 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
628 _e -> render_error(conn, :forbidden, "Can't delete this post")
632 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
633 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
634 %Activity{} = announce <- Activity.normalize(announce.data) do
636 |> put_view(StatusView)
637 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
641 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
642 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
643 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
645 |> put_view(StatusView)
646 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
650 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
651 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
652 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
654 |> put_view(StatusView)
655 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
659 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
660 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
661 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
663 |> put_view(StatusView)
664 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
668 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
669 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
671 |> put_view(StatusView)
672 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
676 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
677 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
679 |> put_view(StatusView)
680 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
684 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
685 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
686 %User{} = user <- User.get_cached_by_nickname(user.nickname),
687 true <- Visibility.visible_for_user?(activity, user),
688 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
695 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
697 %User{} = user <- User.get_cached_by_nickname(user.nickname),
698 true <- Visibility.visible_for_user?(activity, user),
699 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
701 |> put_view(StatusView)
702 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
706 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
707 activity = Activity.get_by_id(id)
709 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
711 |> put_view(StatusView)
712 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
716 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
717 activity = Activity.get_by_id(id)
719 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
721 |> put_view(StatusView)
722 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
726 def notifications(%{assigns: %{user: user}} = conn, params) do
727 notifications = MastodonAPI.get_notifications(user, params)
730 |> add_link_headers(notifications)
731 |> put_view(NotificationView)
732 |> render("index.json", %{notifications: notifications, for: user})
735 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
736 with {:ok, notification} <- Notification.get(user, id) do
738 |> put_view(NotificationView)
739 |> render("show.json", %{notification: notification, for: user})
743 |> put_status(:forbidden)
744 |> json(%{"error" => reason})
748 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
749 Notification.clear(user)
753 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
754 with {:ok, _notif} <- Notification.dismiss(user, id) do
759 |> put_status(:forbidden)
760 |> json(%{"error" => reason})
764 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
765 Notification.destroy_multiple(user, ids)
769 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
771 q = from(u in User, where: u.id in ^id)
772 targets = Repo.all(q)
775 |> put_view(AccountView)
776 |> render("relationships.json", %{user: user, targets: targets})
779 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
780 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
782 def update_media(%{assigns: %{user: user}} = conn, data) do
783 with %Object{} = object <- Repo.get(Object, data["id"]),
784 true <- Object.authorize_mutation(object, user),
785 true <- is_binary(data["description"]),
786 description <- data["description"] do
787 new_data = %{object.data | "name" => description}
791 |> Object.change(%{data: new_data})
794 attachment_data = Map.put(new_data, "id", object.id)
797 |> put_view(StatusView)
798 |> render("attachment.json", %{attachment: attachment_data})
802 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
803 with {:ok, object} <-
806 actor: User.ap_id(user),
807 description: Map.get(data, "description")
809 attachment_data = Map.put(object.data, "id", object.id)
812 |> put_view(StatusView)
813 |> render("attachment.json", %{attachment: attachment_data})
817 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
818 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
819 %{} = attachment_data <- Map.put(object.data, "id", object.id),
820 %{type: type} = rendered <-
821 StatusView.render("attachment.json", %{attachment: attachment_data}) do
822 # Reject if not an image
823 if type == "image" do
825 # Save to the user's info
826 info_changeset = User.Info.mascot_update(user.info, rendered)
830 |> Changeset.change()
831 |> Changeset.put_embed(:info, info_changeset)
833 {:ok, _user} = User.update_and_set_cache(user_changeset)
838 render_error(conn, :unsupported_media_type, "mascots can only be images")
843 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
844 mascot = User.get_mascot(user)
850 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
851 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
852 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
853 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
854 q = from(u in User, where: u.ap_id in ^likes)
858 |> Enum.filter(&(not User.blocks?(user, &1)))
861 |> put_view(AccountView)
862 |> render("accounts.json", %{for: user, users: users, as: :user})
864 {:visible, false} -> {:error, :not_found}
869 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
870 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
871 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
872 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
873 q = from(u in User, where: u.ap_id in ^announces)
877 |> Enum.filter(&(not User.blocks?(user, &1)))
880 |> put_view(AccountView)
881 |> render("accounts.json", %{for: user, users: users, as: :user})
883 {:visible, false} -> {:error, :not_found}
888 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
889 local_only = params["local"] in [true, "True", "true", "1"]
892 [params["tag"], params["any"]]
896 |> Enum.map(&String.downcase(&1))
901 |> Enum.map(&String.downcase(&1))
906 |> Enum.map(&String.downcase(&1))
910 |> Map.put("type", "Create")
911 |> Map.put("local_only", local_only)
912 |> Map.put("blocking_user", user)
913 |> Map.put("muting_user", user)
914 |> Map.put("user", user)
915 |> Map.put("tag", tags)
916 |> Map.put("tag_all", tag_all)
917 |> Map.put("tag_reject", tag_reject)
918 |> ActivityPub.fetch_public_activities()
922 |> add_link_headers(activities, %{"local" => local_only})
923 |> put_view(StatusView)
924 |> render("index.json", %{activities: activities, for: user, as: :activity})
927 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
928 with %User{} = user <- User.get_cached_by_id(id),
929 followers <- MastodonAPI.get_followers(user, params) do
932 for_user && user.id == for_user.id -> followers
933 user.info.hide_followers -> []
938 |> add_link_headers(followers)
939 |> put_view(AccountView)
940 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
944 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
945 with %User{} = user <- User.get_cached_by_id(id),
946 followers <- MastodonAPI.get_friends(user, params) do
949 for_user && user.id == for_user.id -> followers
950 user.info.hide_follows -> []
955 |> add_link_headers(followers)
956 |> put_view(AccountView)
957 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
961 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
962 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
964 |> put_view(AccountView)
965 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
969 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
970 with %User{} = follower <- User.get_cached_by_id(id),
971 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
973 |> put_view(AccountView)
974 |> render("relationship.json", %{user: followed, target: follower})
978 |> put_status(:forbidden)
979 |> json(%{error: message})
983 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
984 with %User{} = follower <- User.get_cached_by_id(id),
985 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
987 |> put_view(AccountView)
988 |> render("relationship.json", %{user: followed, target: follower})
992 |> put_status(:forbidden)
993 |> json(%{error: message})
997 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
998 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
999 {_, true} <- {:followed, follower.id != followed.id},
1000 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1002 |> put_view(AccountView)
1003 |> render("relationship.json", %{user: follower, target: followed})
1006 {:error, :not_found}
1008 {:error, message} ->
1010 |> put_status(:forbidden)
1011 |> json(%{error: message})
1015 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1016 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1017 {_, true} <- {:followed, follower.id != followed.id},
1018 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1020 |> put_view(AccountView)
1021 |> render("account.json", %{user: followed, for: follower})
1024 {:error, :not_found}
1026 {:error, message} ->
1028 |> put_status(:forbidden)
1029 |> json(%{error: message})
1033 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1034 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1035 {_, true} <- {:followed, follower.id != followed.id},
1036 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1038 |> put_view(AccountView)
1039 |> render("relationship.json", %{user: follower, target: followed})
1042 {:error, :not_found}
1049 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1051 if Map.has_key?(params, "notifications"),
1052 do: params["notifications"] in [true, "True", "true", "1"],
1055 with %User{} = muted <- User.get_cached_by_id(id),
1056 {:ok, muter} <- User.mute(muter, muted, notifications) do
1058 |> put_view(AccountView)
1059 |> render("relationship.json", %{user: muter, target: muted})
1061 {:error, message} ->
1063 |> put_status(:forbidden)
1064 |> json(%{error: message})
1068 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1069 with %User{} = muted <- User.get_cached_by_id(id),
1070 {:ok, muter} <- User.unmute(muter, muted) do
1072 |> put_view(AccountView)
1073 |> render("relationship.json", %{user: muter, target: muted})
1075 {:error, message} ->
1077 |> put_status(:forbidden)
1078 |> json(%{error: message})
1082 def mutes(%{assigns: %{user: user}} = conn, _) do
1083 with muted_accounts <- User.muted_users(user) do
1084 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1089 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1090 with %User{} = blocked <- User.get_cached_by_id(id),
1091 {:ok, blocker} <- User.block(blocker, blocked),
1092 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1094 |> put_view(AccountView)
1095 |> render("relationship.json", %{user: blocker, target: blocked})
1097 {:error, message} ->
1099 |> put_status(:forbidden)
1100 |> json(%{error: message})
1104 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1105 with %User{} = blocked <- User.get_cached_by_id(id),
1106 {:ok, blocker} <- User.unblock(blocker, blocked),
1107 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1109 |> put_view(AccountView)
1110 |> render("relationship.json", %{user: blocker, target: blocked})
1112 {:error, message} ->
1114 |> put_status(:forbidden)
1115 |> json(%{error: message})
1119 def blocks(%{assigns: %{user: user}} = conn, _) do
1120 with blocked_accounts <- User.blocked_users(user) do
1121 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1126 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1127 json(conn, info.domain_blocks || [])
1130 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1131 User.block_domain(blocker, domain)
1135 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1136 User.unblock_domain(blocker, domain)
1140 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1141 with %User{} = subscription_target <- User.get_cached_by_id(id),
1142 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1144 |> put_view(AccountView)
1145 |> render("relationship.json", %{user: user, target: subscription_target})
1147 {:error, message} ->
1149 |> put_status(:forbidden)
1150 |> json(%{error: message})
1154 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1155 with %User{} = subscription_target <- User.get_cached_by_id(id),
1156 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1158 |> put_view(AccountView)
1159 |> render("relationship.json", %{user: user, target: subscription_target})
1161 {:error, message} ->
1163 |> put_status(:forbidden)
1164 |> json(%{error: message})
1168 def favourites(%{assigns: %{user: user}} = conn, params) do
1171 |> Map.put("type", "Create")
1172 |> Map.put("favorited_by", user.ap_id)
1173 |> Map.put("blocking_user", user)
1176 ActivityPub.fetch_activities([], params)
1180 |> add_link_headers(activities)
1181 |> put_view(StatusView)
1182 |> render("index.json", %{activities: activities, for: user, as: :activity})
1185 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1186 with %User{} = user <- User.get_by_id(id),
1187 false <- user.info.hide_favorites do
1190 |> Map.put("type", "Create")
1191 |> Map.put("favorited_by", user.ap_id)
1192 |> Map.put("blocking_user", for_user)
1196 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1198 [Pleroma.Constants.as_public()]
1203 |> ActivityPub.fetch_activities(params)
1207 |> add_link_headers(activities)
1208 |> put_view(StatusView)
1209 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1211 nil -> {:error, :not_found}
1212 true -> render_error(conn, :forbidden, "Can't get favorites")
1216 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1217 user = User.get_cached_by_id(user.id)
1220 Bookmark.for_user_query(user.id)
1221 |> Pagination.fetch_paginated(params)
1225 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1228 |> add_link_headers(bookmarks)
1229 |> put_view(StatusView)
1230 |> render("index.json", %{activities: activities, for: user, as: :activity})
1233 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1234 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1235 res = ListView.render("lists.json", lists: lists)
1239 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1240 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1243 |> Map.put("type", "Create")
1244 |> Map.put("blocking_user", user)
1245 |> Map.put("user", user)
1246 |> Map.put("muting_user", user)
1248 # we must filter the following list for the user to avoid leaking statuses the user
1249 # does not actually have permission to see (for more info, peruse security issue #270).
1252 |> Enum.filter(fn x -> x in user.following end)
1253 |> ActivityPub.fetch_activities_bounded(following, params)
1257 |> put_view(StatusView)
1258 |> render("index.json", %{activities: activities, for: user, as: :activity})
1260 _e -> render_error(conn, :forbidden, "Error.")
1264 def index(%{assigns: %{user: user}} = conn, _params) do
1265 token = get_session(conn, :oauth_token)
1268 mastodon_emoji = mastodonized_emoji()
1270 limit = Config.get([:instance, :limit])
1273 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1278 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1279 access_token: token,
1281 domain: Pleroma.Web.Endpoint.host(),
1284 unfollow_modal: false,
1287 auto_play_gif: false,
1288 display_sensitive_media: false,
1289 reduce_motion: false,
1290 max_toot_chars: limit,
1291 mascot: User.get_mascot(user)["url"]
1293 poll_limits: Config.get([:instance, :poll_limits]),
1295 delete_others_notice: present?(user.info.is_moderator),
1296 admin: present?(user.info.is_admin)
1300 default_privacy: user.info.default_scope,
1301 default_sensitive: false,
1302 allow_content_types: Config.get([:instance, :allowed_post_formats])
1304 media_attachments: %{
1305 accept_content_types: [
1321 user.info.settings ||
1351 push_subscription: nil,
1353 custom_emojis: mastodon_emoji,
1359 |> put_layout(false)
1360 |> put_view(MastodonView)
1361 |> render("index.html", %{initial_state: initial_state})
1364 |> put_session(:return_to, conn.request_path)
1365 |> redirect(to: "/web/login")
1369 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1370 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1372 with changeset <- Changeset.change(user),
1373 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1374 {:ok, _user} <- User.update_and_set_cache(changeset) do
1379 |> put_status(:internal_server_error)
1380 |> json(%{error: inspect(e)})
1384 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1385 redirect(conn, to: local_mastodon_root_path(conn))
1388 @doc "Local Mastodon FE login init action"
1389 def login(conn, %{"code" => auth_token}) do
1390 with {:ok, app} <- get_or_make_app(),
1391 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1392 {:ok, token} <- Token.exchange_token(app, auth) do
1394 |> put_session(:oauth_token, token.token)
1395 |> redirect(to: local_mastodon_root_path(conn))
1399 @doc "Local Mastodon FE callback action"
1400 def login(conn, _) do
1401 with {:ok, app} <- get_or_make_app() do
1406 response_type: "code",
1407 client_id: app.client_id,
1409 scope: Enum.join(app.scopes, " ")
1412 redirect(conn, to: path)
1416 defp local_mastodon_root_path(conn) do
1417 case get_session(conn, :return_to) do
1419 mastodon_api_path(conn, :index, ["getting-started"])
1422 delete_session(conn, :return_to)
1427 defp get_or_make_app do
1428 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1429 scopes = ["read", "write", "follow", "push"]
1431 with %App{} = app <- Repo.get_by(App, find_attrs) do
1433 if app.scopes == scopes do
1437 |> Changeset.change(%{scopes: scopes})
1445 App.register_changeset(
1447 Map.put(find_attrs, :scopes, scopes)
1454 def logout(conn, _) do
1457 |> redirect(to: "/")
1460 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1461 Logger.debug("Unimplemented, returning unmodified relationship")
1463 with %User{} = target <- User.get_cached_by_id(id) do
1465 |> put_view(AccountView)
1466 |> render("relationship.json", %{user: user, target: target})
1470 def empty_array(conn, _) do
1471 Logger.debug("Unimplemented, returning an empty array")
1475 def empty_object(conn, _) do
1476 Logger.debug("Unimplemented, returning an empty object")
1480 def get_filters(%{assigns: %{user: user}} = conn, _) do
1481 filters = Filter.get_filters(user)
1482 res = FilterView.render("filters.json", filters: filters)
1487 %{assigns: %{user: user}} = conn,
1488 %{"phrase" => phrase, "context" => context} = params
1494 hide: Map.get(params, "irreversible", false),
1495 whole_word: Map.get(params, "boolean", true)
1499 {:ok, response} = Filter.create(query)
1500 res = FilterView.render("filter.json", filter: response)
1504 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1505 filter = Filter.get(filter_id, user)
1506 res = FilterView.render("filter.json", filter: filter)
1511 %{assigns: %{user: user}} = conn,
1512 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1516 filter_id: filter_id,
1519 hide: Map.get(params, "irreversible", nil),
1520 whole_word: Map.get(params, "boolean", true)
1524 {:ok, response} = Filter.update(query)
1525 res = FilterView.render("filter.json", filter: response)
1529 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1532 filter_id: filter_id
1535 {:ok, _} = Filter.delete(query)
1539 def suggestions(%{assigns: %{user: user}} = conn, _) do
1540 suggestions = Config.get(:suggestions)
1542 if Keyword.get(suggestions, :enabled, false) do
1543 api = Keyword.get(suggestions, :third_party_engine, "")
1544 timeout = Keyword.get(suggestions, :timeout, 5000)
1545 limit = Keyword.get(suggestions, :limit, 23)
1547 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1549 user = user.nickname
1553 |> String.replace("{{host}}", host)
1554 |> String.replace("{{user}}", user)
1556 with {:ok, %{status: 200, body: body}} <-
1557 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1558 {:ok, data} <- Jason.decode(body) do
1561 |> Enum.slice(0, limit)
1564 |> Map.put("id", fetch_suggestion_id(x))
1565 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1566 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1572 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1579 defp fetch_suggestion_id(attrs) do
1580 case User.get_or_fetch(attrs["acct"]) do
1581 {:ok, %User{id: id}} -> id
1586 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1587 with %Activity{} = activity <- Activity.get_by_id(status_id),
1588 true <- Visibility.visible_for_user?(activity, user) do
1592 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1602 def reports(%{assigns: %{user: user}} = conn, params) do
1603 case CommonAPI.report(user, params) do
1606 |> put_view(ReportView)
1607 |> try_render("report.json", %{activity: activity})
1611 |> put_status(:bad_request)
1612 |> json(%{error: err})
1616 def account_register(
1617 %{assigns: %{app: app}} = conn,
1618 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1626 "captcha_answer_data",
1630 |> Map.put("nickname", nickname)
1631 |> Map.put("fullname", params["fullname"] || nickname)
1632 |> Map.put("bio", params["bio"] || "")
1633 |> Map.put("confirm", params["password"])
1635 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1636 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1638 token_type: "Bearer",
1639 access_token: token.token,
1641 created_at: Token.Utils.format_created_at(token)
1646 |> put_status(:bad_request)
1651 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1652 render_error(conn, :bad_request, "Missing parameters")
1655 def account_register(conn, _) do
1656 render_error(conn, :forbidden, "Invalid credentials")
1659 def conversations(%{assigns: %{user: user}} = conn, params) do
1660 participations = Participation.for_user_with_last_activity_id(user, params)
1663 Enum.map(participations, fn participation ->
1664 ConversationView.render("participation.json", %{participation: participation, for: user})
1668 |> add_link_headers(participations)
1669 |> json(conversations)
1672 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1673 with %Participation{} = participation <-
1674 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1675 {:ok, participation} <- Participation.mark_as_read(participation) do
1676 participation_view =
1677 ConversationView.render("participation.json", %{participation: participation, for: user})
1680 |> json(participation_view)
1684 def password_reset(conn, params) do
1685 nickname_or_email = params["email"] || params["nickname"]
1687 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1689 |> put_status(:no_content)
1692 {:error, "unknown user"} ->
1693 send_resp(conn, :not_found, "")
1696 send_resp(conn, :bad_request, "")
1700 def account_confirmation_resend(conn, params) do
1701 nickname_or_email = params["email"] || params["nickname"]
1703 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1704 {:ok, _} <- User.try_send_confirmation_email(user) do
1706 |> json_response(:no_content, "")
1710 def try_render(conn, target, params)
1711 when is_binary(target) do
1712 case render(conn, target, params) do
1713 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1718 def try_render(conn, _, _) do
1719 render_error(conn, :not_implemented, "Can't display this activity")
1722 defp present?(nil), do: false
1723 defp present?(false), do: false
1724 defp present?(_), do: true