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: 5, add_link_headers: 4, add_link_headers: 3]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
17 alias Pleroma.Formatter
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(Formatter.get_emoji_map(emojis_text))
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(:home_timeline, 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 |> Map.put("user", user)
383 |> ActivityPub.fetch_public_activities()
387 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
388 |> put_view(StatusView)
389 |> render("index.json", %{activities: activities, for: user, as: :activity})
392 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
393 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
396 |> Map.put("tag", params["tagged"])
398 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
401 |> add_link_headers(:user_statuses, activities, params["id"])
402 |> put_view(StatusView)
403 |> render("index.json", %{
404 activities: activities,
411 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
414 |> Map.put("type", "Create")
415 |> Map.put("blocking_user", user)
416 |> Map.put("user", user)
417 |> Map.put(:visibility, "direct")
421 |> ActivityPub.fetch_activities_query(params)
422 |> Pagination.fetch_paginated(params)
425 |> add_link_headers(:dm_timeline, activities)
426 |> put_view(StatusView)
427 |> render("index.json", %{activities: activities, for: user, as: :activity})
430 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
431 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
432 true <- Visibility.visible_for_user?(activity, user) do
434 |> put_view(StatusView)
435 |> try_render("status.json", %{activity: activity, for: user})
439 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
440 with %Activity{} = activity <- Activity.get_by_id(id),
442 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
443 "blocking_user" => user,
445 "exclude_id" => activity.id
447 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
450 StatusView.render("index.json",
452 activities: grouped_activities[true] || [],
456 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
458 StatusView.render("index.json",
460 activities: grouped_activities[false] || [],
464 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
471 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
472 with %Object{} = object <- Object.get_by_id(id),
473 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
474 true <- Visibility.visible_for_user?(activity, user) do
476 |> put_view(StatusView)
477 |> try_render("poll.json", %{object: object, for: user})
479 error when is_nil(error) or error == false ->
480 render_error(conn, :not_found, "Record not found")
484 defp get_cached_vote_or_vote(user, object, choices) do
485 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
488 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
489 case CommonAPI.vote(user, object, choices) do
490 {:error, _message} = res -> {:ignore, res}
491 res -> {:commit, res}
498 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
499 with %Object{} = object <- Object.get_by_id(id),
500 true <- object.data["type"] == "Question",
501 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
502 true <- Visibility.visible_for_user?(activity, user),
503 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
505 |> put_view(StatusView)
506 |> try_render("poll.json", %{object: object, for: user})
509 render_error(conn, :not_found, "Record not found")
512 render_error(conn, :not_found, "Record not found")
516 |> put_status(:unprocessable_entity)
517 |> json(%{error: message})
521 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
522 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
524 |> add_link_headers(:scheduled_statuses, scheduled_activities)
525 |> put_view(ScheduledActivityView)
526 |> render("index.json", %{scheduled_activities: scheduled_activities})
530 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
531 with %ScheduledActivity{} = scheduled_activity <-
532 ScheduledActivity.get(user, scheduled_activity_id) do
534 |> put_view(ScheduledActivityView)
535 |> render("show.json", %{scheduled_activity: scheduled_activity})
537 _ -> {:error, :not_found}
541 def update_scheduled_status(
542 %{assigns: %{user: user}} = conn,
543 %{"id" => scheduled_activity_id} = params
545 with %ScheduledActivity{} = scheduled_activity <-
546 ScheduledActivity.get(user, scheduled_activity_id),
547 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
549 |> put_view(ScheduledActivityView)
550 |> render("show.json", %{scheduled_activity: scheduled_activity})
552 nil -> {:error, :not_found}
557 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
558 with %ScheduledActivity{} = scheduled_activity <-
559 ScheduledActivity.get(user, scheduled_activity_id),
560 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
562 |> put_view(ScheduledActivityView)
563 |> render("show.json", %{scheduled_activity: scheduled_activity})
565 nil -> {:error, :not_found}
570 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
573 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
575 scheduled_at = params["scheduled_at"]
577 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
578 with {:ok, scheduled_activity} <-
579 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
581 |> put_view(ScheduledActivityView)
582 |> render("show.json", %{scheduled_activity: scheduled_activity})
585 params = Map.drop(params, ["scheduled_at"])
587 case CommonAPI.post(user, params) do
590 |> put_status(:unprocessable_entity)
591 |> json(%{error: message})
595 |> put_view(StatusView)
596 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
601 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
602 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
605 _e -> render_error(conn, :forbidden, "Can't delete this post")
609 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
610 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
611 %Activity{} = announce <- Activity.normalize(announce.data) do
613 |> put_view(StatusView)
614 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
618 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
619 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
620 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
622 |> put_view(StatusView)
623 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
627 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
628 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
629 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
631 |> put_view(StatusView)
632 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
636 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
637 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
638 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
640 |> put_view(StatusView)
641 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
645 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
646 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
648 |> put_view(StatusView)
649 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
653 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
654 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
656 |> put_view(StatusView)
657 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
661 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
662 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
663 %User{} = user <- User.get_cached_by_nickname(user.nickname),
664 true <- Visibility.visible_for_user?(activity, user),
665 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
667 |> put_view(StatusView)
668 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
672 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
673 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
674 %User{} = user <- User.get_cached_by_nickname(user.nickname),
675 true <- Visibility.visible_for_user?(activity, user),
676 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
678 |> put_view(StatusView)
679 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
683 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
684 activity = Activity.get_by_id(id)
686 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
688 |> put_view(StatusView)
689 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
693 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
694 activity = Activity.get_by_id(id)
696 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
698 |> put_view(StatusView)
699 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
703 def notifications(%{assigns: %{user: user}} = conn, params) do
704 notifications = MastodonAPI.get_notifications(user, params)
707 |> add_link_headers(:notifications, notifications)
708 |> put_view(NotificationView)
709 |> render("index.json", %{notifications: notifications, for: user})
712 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
713 with {:ok, notification} <- Notification.get(user, id) do
715 |> put_view(NotificationView)
716 |> render("show.json", %{notification: notification, for: user})
720 |> put_status(:forbidden)
721 |> json(%{"error" => reason})
725 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
726 Notification.clear(user)
730 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
731 with {:ok, _notif} <- Notification.dismiss(user, id) do
736 |> put_status(:forbidden)
737 |> json(%{"error" => reason})
741 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
742 Notification.destroy_multiple(user, ids)
746 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
747 targets = User.get_all_by_ids(List.wrap(id))
750 |> put_view(AccountView)
751 |> render("relationships.json", %{user: user, targets: targets})
754 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
755 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
758 %{assigns: %{user: user}} = conn,
759 %{"id" => id, "description" => description} = _
761 when is_binary(description) do
762 with %Object{} = object <- Repo.get(Object, id),
763 true <- Object.authorize_mutation(object, user),
764 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
765 attachment_data = Map.put(data, "id", object.id)
768 |> put_view(StatusView)
769 |> render("attachment.json", %{attachment: attachment_data})
773 def update_media(_conn, _data), do: {:error, :bad_request}
775 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
776 with {:ok, object} <-
779 actor: User.ap_id(user),
780 description: Map.get(data, "description")
782 attachment_data = Map.put(object.data, "id", object.id)
785 |> put_view(StatusView)
786 |> render("attachment.json", %{attachment: attachment_data})
790 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
791 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
792 %{} = attachment_data <- Map.put(object.data, "id", object.id),
793 %{type: "image"} = rendered <-
794 StatusView.render("attachment.json", %{attachment: attachment_data}),
795 {:ok, _user} = User.update_mascot(user, rendered) do
798 %{type: _type} = _ ->
799 render_error(conn, :unsupported_media_type, "mascots can only be images")
806 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
807 mascot = User.get_mascot(user)
812 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
813 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
814 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
815 q = from(u in User, where: u.ap_id in ^likes)
819 |> Enum.filter(&(not User.blocks?(user, &1)))
822 |> put_view(AccountView)
823 |> render("accounts.json", %{for: user, users: users, as: :user})
829 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
830 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
831 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
832 q = from(u in User, where: u.ap_id in ^announces)
836 |> Enum.filter(&(not User.blocks?(user, &1)))
839 |> put_view(AccountView)
840 |> render("accounts.json", %{for: user, users: users, as: :user})
846 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
847 local_only = params["local"] in [true, "True", "true", "1"]
850 [params["tag"], params["any"]]
854 |> Enum.map(&String.downcase(&1))
859 |> Enum.map(&String.downcase(&1))
864 |> Enum.map(&String.downcase(&1))
868 |> Map.put("type", "Create")
869 |> Map.put("local_only", local_only)
870 |> Map.put("blocking_user", user)
871 |> Map.put("muting_user", user)
872 |> Map.put("user", user)
873 |> Map.put("tag", tags)
874 |> Map.put("tag_all", tag_all)
875 |> Map.put("tag_reject", tag_reject)
876 |> ActivityPub.fetch_public_activities()
880 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
881 |> put_view(StatusView)
882 |> render("index.json", %{activities: activities, for: user, as: :activity})
885 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
886 with %User{} = user <- User.get_cached_by_id(id),
887 followers <- MastodonAPI.get_followers(user, params) do
890 for_user && user.id == for_user.id -> followers
891 user.info.hide_followers -> []
896 |> add_link_headers(:followers, followers, user)
897 |> put_view(AccountView)
898 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
902 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
903 with %User{} = user <- User.get_cached_by_id(id),
904 followers <- MastodonAPI.get_friends(user, params) do
907 for_user && user.id == for_user.id -> followers
908 user.info.hide_follows -> []
913 |> add_link_headers(:following, followers, user)
914 |> put_view(AccountView)
915 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
919 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
920 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
922 |> put_view(AccountView)
923 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
927 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
928 with %User{} = follower <- User.get_cached_by_id(id),
929 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
931 |> put_view(AccountView)
932 |> render("relationship.json", %{user: followed, target: follower})
936 |> put_status(:forbidden)
937 |> json(%{error: message})
941 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
942 with %User{} = follower <- User.get_cached_by_id(id),
943 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
945 |> put_view(AccountView)
946 |> render("relationship.json", %{user: followed, target: follower})
950 |> put_status(:forbidden)
951 |> json(%{error: message})
955 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
956 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
957 {_, true} <- {:followed, follower.id != followed.id},
958 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
960 |> put_view(AccountView)
961 |> render("relationship.json", %{user: follower, target: followed})
968 |> put_status(:forbidden)
969 |> json(%{error: message})
973 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
974 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
975 {_, true} <- {:followed, follower.id != followed.id},
976 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
978 |> put_view(AccountView)
979 |> render("account.json", %{user: followed, for: follower})
986 |> put_status(:forbidden)
987 |> json(%{error: message})
991 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
992 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
993 {_, true} <- {:followed, follower.id != followed.id},
994 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
996 |> put_view(AccountView)
997 |> render("relationship.json", %{user: follower, target: followed})
1000 {:error, :not_found}
1007 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1009 if Map.has_key?(params, "notifications"),
1010 do: params["notifications"] in [true, "True", "true", "1"],
1013 with %User{} = muted <- User.get_cached_by_id(id),
1014 {:ok, muter} <- User.mute(muter, muted, notifications) do
1016 |> put_view(AccountView)
1017 |> render("relationship.json", %{user: muter, target: muted})
1019 {:error, message} ->
1021 |> put_status(:forbidden)
1022 |> json(%{error: message})
1026 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1027 with %User{} = muted <- User.get_cached_by_id(id),
1028 {:ok, muter} <- User.unmute(muter, muted) do
1030 |> put_view(AccountView)
1031 |> render("relationship.json", %{user: muter, target: muted})
1033 {:error, message} ->
1035 |> put_status(:forbidden)
1036 |> json(%{error: message})
1040 def mutes(%{assigns: %{user: user}} = conn, _) do
1041 with muted_accounts <- User.muted_users(user) do
1042 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1047 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1048 with %User{} = blocked <- User.get_cached_by_id(id),
1049 {:ok, blocker} <- User.block(blocker, blocked),
1050 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1052 |> put_view(AccountView)
1053 |> render("relationship.json", %{user: blocker, target: blocked})
1055 {:error, message} ->
1057 |> put_status(:forbidden)
1058 |> json(%{error: message})
1062 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1063 with %User{} = blocked <- User.get_cached_by_id(id),
1064 {:ok, blocker} <- User.unblock(blocker, blocked),
1065 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1067 |> put_view(AccountView)
1068 |> render("relationship.json", %{user: blocker, target: blocked})
1070 {:error, message} ->
1072 |> put_status(:forbidden)
1073 |> json(%{error: message})
1077 def blocks(%{assigns: %{user: user}} = conn, _) do
1078 with blocked_accounts <- User.blocked_users(user) do
1079 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1084 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1085 json(conn, info.domain_blocks || [])
1088 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1089 User.block_domain(blocker, domain)
1093 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1094 User.unblock_domain(blocker, domain)
1098 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1099 with %User{} = subscription_target <- User.get_cached_by_id(id),
1100 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1102 |> put_view(AccountView)
1103 |> render("relationship.json", %{user: user, target: subscription_target})
1105 nil -> {:error, :not_found}
1110 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1111 with %User{} = subscription_target <- User.get_cached_by_id(id),
1112 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1114 |> put_view(AccountView)
1115 |> render("relationship.json", %{user: user, target: subscription_target})
1117 nil -> {:error, :not_found}
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(:favourites, 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(:favourites, 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, 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)
1191 |> put_view(ListView)
1192 |> render("index.json", %{lists: lists})
1195 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1196 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1199 |> Map.put("type", "Create")
1200 |> Map.put("blocking_user", user)
1201 |> Map.put("user", user)
1202 |> Map.put("muting_user", user)
1204 # we must filter the following list for the user to avoid leaking statuses the user
1205 # does not actually have permission to see (for more info, peruse security issue #270).
1208 |> Enum.filter(fn x -> x in user.following end)
1209 |> ActivityPub.fetch_activities_bounded(following, params)
1213 |> put_view(StatusView)
1214 |> render("index.json", %{activities: activities, for: user, as: :activity})
1216 _e -> render_error(conn, :forbidden, "Error.")
1220 def index(%{assigns: %{user: user}} = conn, _params) do
1221 token = get_session(conn, :oauth_token)
1224 mastodon_emoji = mastodonized_emoji()
1226 limit = Config.get([:instance, :limit])
1229 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1234 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1235 access_token: token,
1237 domain: Pleroma.Web.Endpoint.host(),
1240 unfollow_modal: false,
1243 auto_play_gif: false,
1244 display_sensitive_media: false,
1245 reduce_motion: false,
1246 max_toot_chars: limit,
1247 mascot: User.get_mascot(user)["url"]
1249 poll_limits: Config.get([:instance, :poll_limits]),
1251 delete_others_notice: present?(user.info.is_moderator),
1252 admin: present?(user.info.is_admin)
1256 default_privacy: user.info.default_scope,
1257 default_sensitive: false,
1258 allow_content_types: Config.get([:instance, :allowed_post_formats])
1260 media_attachments: %{
1261 accept_content_types: [
1277 user.info.settings ||
1307 push_subscription: nil,
1309 custom_emojis: mastodon_emoji,
1315 |> put_layout(false)
1316 |> put_view(MastodonView)
1317 |> render("index.html", %{initial_state: initial_state})
1320 |> put_session(:return_to, conn.request_path)
1321 |> redirect(to: "/web/login")
1325 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1326 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1328 with changeset <- Changeset.change(user),
1329 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1330 {:ok, _user} <- User.update_and_set_cache(changeset) do
1335 |> put_status(:internal_server_error)
1336 |> json(%{error: inspect(e)})
1340 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1341 redirect(conn, to: local_mastodon_root_path(conn))
1344 @doc "Local Mastodon FE login init action"
1345 def login(conn, %{"code" => auth_token}) do
1346 with {:ok, app} <- get_or_make_app(),
1347 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
1348 {:ok, token} <- Token.exchange_token(app, auth) do
1350 |> put_session(:oauth_token, token.token)
1351 |> redirect(to: local_mastodon_root_path(conn))
1355 @doc "Local Mastodon FE callback action"
1356 def login(conn, _) do
1357 with {:ok, app} <- get_or_make_app() do
1359 o_auth_path(conn, :authorize,
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 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
1382 defp get_or_make_app do
1384 %{client_name: @local_mastodon_name, redirect_uris: "."},
1385 ["read", "write", "follow", "push"]
1389 def logout(conn, _) do
1392 |> redirect(to: "/")
1395 # Stubs for unimplemented mastodon api
1397 def empty_array(conn, _) do
1398 Logger.debug("Unimplemented, returning an empty array")
1402 def get_filters(%{assigns: %{user: user}} = conn, _) do
1403 filters = Filter.get_filters(user)
1404 res = FilterView.render("filters.json", filters: filters)
1409 %{assigns: %{user: user}} = conn,
1410 %{"phrase" => phrase, "context" => context} = params
1416 hide: Map.get(params, "irreversible", false),
1417 whole_word: Map.get(params, "boolean", true)
1421 {:ok, response} = Filter.create(query)
1422 res = FilterView.render("filter.json", filter: response)
1426 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1427 filter = Filter.get(filter_id, user)
1428 res = FilterView.render("filter.json", filter: filter)
1433 %{assigns: %{user: user}} = conn,
1434 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1438 filter_id: filter_id,
1441 hide: Map.get(params, "irreversible", nil),
1442 whole_word: Map.get(params, "boolean", true)
1446 {:ok, response} = Filter.update(query)
1447 res = FilterView.render("filter.json", filter: response)
1451 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1454 filter_id: filter_id
1457 {:ok, _} = Filter.delete(query)
1461 def suggestions(%{assigns: %{user: user}} = conn, _) do
1462 suggestions = Config.get(:suggestions)
1464 if Keyword.get(suggestions, :enabled, false) do
1465 api = Keyword.get(suggestions, :third_party_engine, "")
1466 timeout = Keyword.get(suggestions, :timeout, 5000)
1467 limit = Keyword.get(suggestions, :limit, 23)
1469 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1471 user = user.nickname
1475 |> String.replace("{{host}}", host)
1476 |> String.replace("{{user}}", user)
1478 with {:ok, %{status: 200, body: body}} <-
1479 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1480 {:ok, data} <- Jason.decode(body) do
1483 |> Enum.slice(0, limit)
1486 |> Map.put("id", fetch_suggestion_id(x))
1487 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1488 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1494 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1501 defp fetch_suggestion_id(attrs) do
1502 case User.get_or_fetch(attrs["acct"]) do
1503 {:ok, %User{id: id}} -> id
1508 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1509 with %Activity{} = activity <- Activity.get_by_id(status_id),
1510 true <- Visibility.visible_for_user?(activity, user) do
1514 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1524 def reports(%{assigns: %{user: user}} = conn, params) do
1525 case CommonAPI.report(user, params) do
1528 |> put_view(ReportView)
1529 |> try_render("report.json", %{activity: activity})
1533 |> put_status(:bad_request)
1534 |> json(%{error: err})
1538 def account_register(
1539 %{assigns: %{app: app}} = conn,
1540 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1548 "captcha_answer_data",
1552 |> Map.put("nickname", nickname)
1553 |> Map.put("fullname", params["fullname"] || nickname)
1554 |> Map.put("bio", params["bio"] || "")
1555 |> Map.put("confirm", params["password"])
1557 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1558 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1560 token_type: "Bearer",
1561 access_token: token.token,
1563 created_at: Token.Utils.format_created_at(token)
1568 |> put_status(:bad_request)
1573 def account_register(%{assigns: %{app: _app}} = conn, _) do
1574 render_error(conn, :bad_request, "Missing parameters")
1577 def account_register(conn, _) do
1578 render_error(conn, :forbidden, "Invalid credentials")
1581 def conversations(%{assigns: %{user: user}} = conn, params) do
1582 participations = Participation.for_user_with_last_activity_id(user, params)
1585 Enum.map(participations, fn participation ->
1586 ConversationView.render("participation.json", %{participation: participation, for: user})
1590 |> add_link_headers(:conversations, participations)
1591 |> json(conversations)
1594 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1595 with %Participation{} = participation <-
1596 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1597 {:ok, participation} <- Participation.mark_as_read(participation) do
1598 participation_view =
1599 ConversationView.render("participation.json", %{participation: participation, for: user})
1602 |> json(participation_view)
1606 def password_reset(conn, params) do
1607 nickname_or_email = params["email"] || params["nickname"]
1609 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1611 |> put_status(:no_content)
1614 {:error, "unknown user"} ->
1615 send_resp(conn, :not_found, "")
1618 send_resp(conn, :bad_request, "")
1622 def account_confirmation_resend(conn, params) do
1623 nickname_or_email = params["email"] || params["nickname"]
1625 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1626 {:ok, _} <- User.try_send_confirmation_email(user) do
1628 |> json_response(:no_content, "")
1632 defp try_render(conn, target, params)
1633 when is_binary(target) do
1634 case render(conn, target, params) do
1635 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1640 defp try_render(conn, _, _) do
1641 render_error(conn, :not_implemented, "Can't display this activity")
1644 defp present?(nil), do: false
1645 defp present?(false), do: false
1646 defp present?(_), do: true