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),
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"]) 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)
426 |> add_link_headers(:dm_timeline, activities)
427 |> put_view(StatusView)
428 |> render("index.json", %{activities: activities, for: user, as: :activity})
431 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
432 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
433 true <- Visibility.visible_for_user?(activity, user) do
435 |> put_view(StatusView)
436 |> try_render("status.json", %{activity: activity, for: user})
440 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
441 with %Activity{} = activity <- Activity.get_by_id(id),
443 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
444 "blocking_user" => user,
446 "exclude_id" => activity.id
448 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
454 activities: grouped_activities[true] || [],
458 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
463 activities: grouped_activities[false] || [],
467 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
474 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
475 with %Object{} = object <- Object.get_by_id(id),
476 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
477 true <- Visibility.visible_for_user?(activity, user) do
479 |> put_view(StatusView)
480 |> try_render("poll.json", %{object: object, for: user})
482 error when is_nil(error) or error == false ->
483 render_error(conn, :not_found, "Record not found")
487 defp get_cached_vote_or_vote(user, object, choices) do
488 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
491 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
492 case CommonAPI.vote(user, object, choices) do
493 {:error, _message} = res -> {:ignore, res}
494 res -> {:commit, res}
501 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
502 with %Object{} = object <- Object.get_by_id(id),
503 true <- object.data["type"] == "Question",
504 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
505 true <- Visibility.visible_for_user?(activity, user),
506 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
508 |> put_view(StatusView)
509 |> try_render("poll.json", %{object: object, for: user})
512 render_error(conn, :not_found, "Record not found")
515 render_error(conn, :not_found, "Record not found")
519 |> put_status(:unprocessable_entity)
520 |> json(%{error: message})
524 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
525 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
527 |> add_link_headers(:scheduled_statuses, scheduled_activities)
528 |> put_view(ScheduledActivityView)
529 |> render("index.json", %{scheduled_activities: scheduled_activities})
533 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
534 with %ScheduledActivity{} = scheduled_activity <-
535 ScheduledActivity.get(user, scheduled_activity_id) do
537 |> put_view(ScheduledActivityView)
538 |> render("show.json", %{scheduled_activity: scheduled_activity})
540 _ -> {:error, :not_found}
544 def update_scheduled_status(
545 %{assigns: %{user: user}} = conn,
546 %{"id" => scheduled_activity_id} = params
548 with %ScheduledActivity{} = scheduled_activity <-
549 ScheduledActivity.get(user, scheduled_activity_id),
550 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
552 |> put_view(ScheduledActivityView)
553 |> render("show.json", %{scheduled_activity: scheduled_activity})
555 nil -> {:error, :not_found}
560 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
561 with %ScheduledActivity{} = scheduled_activity <-
562 ScheduledActivity.get(user, scheduled_activity_id),
563 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
565 |> put_view(ScheduledActivityView)
566 |> render("show.json", %{scheduled_activity: scheduled_activity})
568 nil -> {:error, :not_found}
573 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
576 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
578 scheduled_at = params["scheduled_at"]
580 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
581 with {:ok, scheduled_activity} <-
582 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
584 |> put_view(ScheduledActivityView)
585 |> render("show.json", %{scheduled_activity: scheduled_activity})
588 params = Map.drop(params, ["scheduled_at"])
590 case CommonAPI.post(user, params) do
593 |> put_status(:unprocessable_entity)
594 |> json(%{error: message})
598 |> put_view(StatusView)
599 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
604 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
605 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
608 _e -> render_error(conn, :forbidden, "Can't delete this post")
612 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
613 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
614 %Activity{} = announce <- Activity.normalize(announce.data) do
616 |> put_view(StatusView)
617 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
621 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
622 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
623 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
625 |> put_view(StatusView)
626 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
630 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
631 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
632 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
634 |> put_view(StatusView)
635 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
639 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
640 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
641 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
643 |> put_view(StatusView)
644 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
648 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
649 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
651 |> put_view(StatusView)
652 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
656 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
657 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
659 |> put_view(StatusView)
660 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
664 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
665 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
666 %User{} = user <- User.get_cached_by_nickname(user.nickname),
667 true <- Visibility.visible_for_user?(activity, user),
668 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
670 |> put_view(StatusView)
671 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
675 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
676 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
677 %User{} = user <- User.get_cached_by_nickname(user.nickname),
678 true <- Visibility.visible_for_user?(activity, user),
679 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
681 |> put_view(StatusView)
682 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
686 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
687 activity = Activity.get_by_id(id)
689 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
691 |> put_view(StatusView)
692 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
696 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
697 activity = Activity.get_by_id(id)
699 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
701 |> put_view(StatusView)
702 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
706 def notifications(%{assigns: %{user: user}} = conn, params) do
707 notifications = MastodonAPI.get_notifications(user, params)
710 |> add_link_headers(:notifications, notifications)
711 |> put_view(NotificationView)
712 |> render("index.json", %{notifications: notifications, for: user})
715 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
716 with {:ok, notification} <- Notification.get(user, id) do
718 |> put_view(NotificationView)
719 |> render("show.json", %{notification: notification, for: user})
723 |> put_status(:forbidden)
724 |> json(%{"error" => reason})
728 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
729 Notification.clear(user)
733 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
734 with {:ok, _notif} <- Notification.dismiss(user, id) do
739 |> put_status(:forbidden)
740 |> json(%{"error" => reason})
744 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
745 Notification.destroy_multiple(user, ids)
749 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
751 q = from(u in User, where: u.id in ^id)
752 targets = Repo.all(q)
755 |> put_view(AccountView)
756 |> render("relationships.json", %{user: user, targets: targets})
759 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
760 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
762 def update_media(%{assigns: %{user: user}} = conn, data) do
763 with %Object{} = object <- Repo.get(Object, data["id"]),
764 true <- Object.authorize_mutation(object, user),
765 true <- is_binary(data["description"]),
766 description <- data["description"] do
767 new_data = %{object.data | "name" => description}
771 |> Object.change(%{data: new_data})
774 attachment_data = Map.put(new_data, "id", object.id)
777 |> put_view(StatusView)
778 |> render("attachment.json", %{attachment: attachment_data})
782 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
783 with {:ok, object} <-
786 actor: User.ap_id(user),
787 description: Map.get(data, "description")
789 attachment_data = Map.put(object.data, "id", object.id)
792 |> put_view(StatusView)
793 |> render("attachment.json", %{attachment: attachment_data})
797 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
798 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
799 %{} = attachment_data <- Map.put(object.data, "id", object.id),
800 %{type: type} = rendered <-
801 StatusView.render("attachment.json", %{attachment: attachment_data}) do
802 # Reject if not an image
803 if type == "image" do
805 # Save to the user's info
806 info_changeset = User.Info.mascot_update(user.info, rendered)
810 |> Changeset.change()
811 |> Changeset.put_embed(:info, info_changeset)
813 {:ok, _user} = User.update_and_set_cache(user_changeset)
818 render_error(conn, :unsupported_media_type, "mascots can only be images")
823 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
824 mascot = User.get_mascot(user)
830 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
831 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
832 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
833 q = from(u in User, where: u.ap_id in ^likes)
837 |> Enum.filter(&(not User.blocks?(user, &1)))
840 |> put_view(AccountView)
841 |> render("accounts.json", %{for: user, users: users, as: :user})
847 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
848 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
849 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
850 q = from(u in User, where: u.ap_id in ^announces)
854 |> Enum.filter(&(not User.blocks?(user, &1)))
857 |> put_view(AccountView)
858 |> render("accounts.json", %{for: user, users: users, as: :user})
864 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
865 local_only = params["local"] in [true, "True", "true", "1"]
868 [params["tag"], params["any"]]
872 |> Enum.map(&String.downcase(&1))
877 |> Enum.map(&String.downcase(&1))
882 |> Enum.map(&String.downcase(&1))
886 |> Map.put("type", "Create")
887 |> Map.put("local_only", local_only)
888 |> Map.put("blocking_user", user)
889 |> Map.put("muting_user", user)
890 |> Map.put("user", user)
891 |> Map.put("tag", tags)
892 |> Map.put("tag_all", tag_all)
893 |> Map.put("tag_reject", tag_reject)
894 |> ActivityPub.fetch_public_activities()
898 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
899 |> put_view(StatusView)
900 |> render("index.json", %{activities: activities, for: user, as: :activity})
903 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
904 with %User{} = user <- User.get_cached_by_id(id),
905 followers <- MastodonAPI.get_followers(user, params) do
908 for_user && user.id == for_user.id -> followers
909 user.info.hide_followers -> []
914 |> add_link_headers(:followers, followers, user)
915 |> put_view(AccountView)
916 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
920 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
921 with %User{} = user <- User.get_cached_by_id(id),
922 followers <- MastodonAPI.get_friends(user, params) do
925 for_user && user.id == for_user.id -> followers
926 user.info.hide_follows -> []
931 |> add_link_headers(:following, followers, user)
932 |> put_view(AccountView)
933 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
937 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
938 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
940 |> put_view(AccountView)
941 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
945 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
946 with %User{} = follower <- User.get_cached_by_id(id),
947 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
949 |> put_view(AccountView)
950 |> render("relationship.json", %{user: followed, target: follower})
954 |> put_status(:forbidden)
955 |> json(%{error: message})
959 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
960 with %User{} = follower <- User.get_cached_by_id(id),
961 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
963 |> put_view(AccountView)
964 |> render("relationship.json", %{user: followed, target: follower})
968 |> put_status(:forbidden)
969 |> json(%{error: message})
973 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
974 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
975 {_, true} <- {:followed, follower.id != followed.id},
976 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
978 |> put_view(AccountView)
979 |> render("relationship.json", %{user: follower, target: followed})
986 |> put_status(:forbidden)
987 |> json(%{error: message})
991 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
992 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
993 {_, true} <- {:followed, follower.id != followed.id},
994 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
996 |> put_view(AccountView)
997 |> render("account.json", %{user: followed, for: follower})
1000 {:error, :not_found}
1002 {:error, message} ->
1004 |> put_status(:forbidden)
1005 |> json(%{error: message})
1009 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1010 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1011 {_, true} <- {:followed, follower.id != followed.id},
1012 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1014 |> put_view(AccountView)
1015 |> render("relationship.json", %{user: follower, target: followed})
1018 {:error, :not_found}
1025 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1027 if Map.has_key?(params, "notifications"),
1028 do: params["notifications"] in [true, "True", "true", "1"],
1031 with %User{} = muted <- User.get_cached_by_id(id),
1032 {:ok, muter} <- User.mute(muter, muted, notifications) do
1034 |> put_view(AccountView)
1035 |> render("relationship.json", %{user: muter, target: muted})
1037 {:error, message} ->
1039 |> put_status(:forbidden)
1040 |> json(%{error: message})
1044 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1045 with %User{} = muted <- User.get_cached_by_id(id),
1046 {:ok, muter} <- User.unmute(muter, muted) do
1048 |> put_view(AccountView)
1049 |> render("relationship.json", %{user: muter, target: muted})
1051 {:error, message} ->
1053 |> put_status(:forbidden)
1054 |> json(%{error: message})
1058 def mutes(%{assigns: %{user: user}} = conn, _) do
1059 with muted_accounts <- User.muted_users(user) do
1060 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1065 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1066 with %User{} = blocked <- User.get_cached_by_id(id),
1067 {:ok, blocker} <- User.block(blocker, blocked),
1068 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1070 |> put_view(AccountView)
1071 |> render("relationship.json", %{user: blocker, target: blocked})
1073 {:error, message} ->
1075 |> put_status(:forbidden)
1076 |> json(%{error: message})
1080 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1081 with %User{} = blocked <- User.get_cached_by_id(id),
1082 {:ok, blocker} <- User.unblock(blocker, blocked),
1083 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1085 |> put_view(AccountView)
1086 |> render("relationship.json", %{user: blocker, target: blocked})
1088 {:error, message} ->
1090 |> put_status(:forbidden)
1091 |> json(%{error: message})
1095 def blocks(%{assigns: %{user: user}} = conn, _) do
1096 with blocked_accounts <- User.blocked_users(user) do
1097 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1102 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1103 json(conn, info.domain_blocks || [])
1106 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1107 User.block_domain(blocker, domain)
1111 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1112 User.unblock_domain(blocker, domain)
1116 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1117 with %User{} = subscription_target <- User.get_cached_by_id(id),
1118 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1120 |> put_view(AccountView)
1121 |> render("relationship.json", %{user: user, target: subscription_target})
1123 {:error, message} ->
1125 |> put_status(:forbidden)
1126 |> json(%{error: message})
1130 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1131 with %User{} = subscription_target <- User.get_cached_by_id(id),
1132 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1134 |> put_view(AccountView)
1135 |> render("relationship.json", %{user: user, target: subscription_target})
1137 {:error, message} ->
1139 |> put_status(:forbidden)
1140 |> json(%{error: message})
1144 def favourites(%{assigns: %{user: user}} = conn, params) do
1147 |> Map.put("type", "Create")
1148 |> Map.put("favorited_by", user.ap_id)
1149 |> Map.put("blocking_user", user)
1152 ActivityPub.fetch_activities([], params)
1156 |> add_link_headers(:favourites, activities)
1157 |> put_view(StatusView)
1158 |> render("index.json", %{activities: activities, for: user, as: :activity})
1161 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1162 with %User{} = user <- User.get_by_id(id),
1163 false <- user.info.hide_favorites do
1166 |> Map.put("type", "Create")
1167 |> Map.put("favorited_by", user.ap_id)
1168 |> Map.put("blocking_user", for_user)
1172 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1174 [Pleroma.Constants.as_public()]
1179 |> ActivityPub.fetch_activities(params)
1183 |> add_link_headers(:favourites, activities)
1184 |> put_view(StatusView)
1185 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1187 nil -> {:error, :not_found}
1188 true -> render_error(conn, :forbidden, "Can't get favorites")
1192 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1193 user = User.get_cached_by_id(user.id)
1196 Bookmark.for_user_query(user.id)
1197 |> Pagination.fetch_paginated(params)
1202 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1205 |> add_link_headers(:bookmarks, bookmarks)
1206 |> put_view(StatusView)
1207 |> render("index.json", %{activities: activities, for: user, as: :activity})
1210 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1211 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1212 res = ListView.render("lists.json", lists: lists)
1216 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1217 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1220 |> Map.put("type", "Create")
1221 |> Map.put("blocking_user", user)
1222 |> Map.put("user", user)
1223 |> Map.put("muting_user", user)
1225 # we must filter the following list for the user to avoid leaking statuses the user
1226 # does not actually have permission to see (for more info, peruse security issue #270).
1229 |> Enum.filter(fn x -> x in user.following end)
1230 |> ActivityPub.fetch_activities_bounded(following, params)
1234 |> put_view(StatusView)
1235 |> render("index.json", %{activities: activities, for: user, as: :activity})
1237 _e -> render_error(conn, :forbidden, "Error.")
1241 def index(%{assigns: %{user: user}} = conn, _params) do
1242 token = get_session(conn, :oauth_token)
1245 mastodon_emoji = mastodonized_emoji()
1247 limit = Config.get([:instance, :limit])
1250 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1255 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1256 access_token: token,
1258 domain: Pleroma.Web.Endpoint.host(),
1261 unfollow_modal: false,
1264 auto_play_gif: false,
1265 display_sensitive_media: false,
1266 reduce_motion: false,
1267 max_toot_chars: limit,
1268 mascot: User.get_mascot(user)["url"]
1270 poll_limits: Config.get([:instance, :poll_limits]),
1272 delete_others_notice: present?(user.info.is_moderator),
1273 admin: present?(user.info.is_admin)
1277 default_privacy: user.info.default_scope,
1278 default_sensitive: false,
1279 allow_content_types: Config.get([:instance, :allowed_post_formats])
1281 media_attachments: %{
1282 accept_content_types: [
1298 user.info.settings ||
1328 push_subscription: nil,
1330 custom_emojis: mastodon_emoji,
1336 |> put_layout(false)
1337 |> put_view(MastodonView)
1338 |> render("index.html", %{initial_state: initial_state})
1341 |> put_session(:return_to, conn.request_path)
1342 |> redirect(to: "/web/login")
1346 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1347 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1349 with changeset <- Changeset.change(user),
1350 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1351 {:ok, _user} <- User.update_and_set_cache(changeset) do
1356 |> put_status(:internal_server_error)
1357 |> json(%{error: inspect(e)})
1361 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1362 redirect(conn, to: local_mastodon_root_path(conn))
1365 @doc "Local Mastodon FE login init action"
1366 def login(conn, %{"code" => auth_token}) do
1367 with {:ok, app} <- get_or_make_app(),
1368 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1369 {:ok, token} <- Token.exchange_token(app, auth) do
1371 |> put_session(:oauth_token, token.token)
1372 |> redirect(to: local_mastodon_root_path(conn))
1376 @doc "Local Mastodon FE callback action"
1377 def login(conn, _) do
1378 with {:ok, app} <- get_or_make_app() do
1383 response_type: "code",
1384 client_id: app.client_id,
1386 scope: Enum.join(app.scopes, " ")
1389 redirect(conn, to: path)
1393 defp local_mastodon_root_path(conn) do
1394 case get_session(conn, :return_to) do
1396 mastodon_api_path(conn, :index, ["getting-started"])
1399 delete_session(conn, :return_to)
1404 defp get_or_make_app do
1405 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1406 scopes = ["read", "write", "follow", "push"]
1408 with %App{} = app <- Repo.get_by(App, find_attrs) do
1410 if app.scopes == scopes do
1414 |> Changeset.change(%{scopes: scopes})
1422 App.register_changeset(
1424 Map.put(find_attrs, :scopes, scopes)
1431 def logout(conn, _) do
1434 |> redirect(to: "/")
1437 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1438 Logger.debug("Unimplemented, returning unmodified relationship")
1440 with %User{} = target <- User.get_cached_by_id(id) do
1442 |> put_view(AccountView)
1443 |> render("relationship.json", %{user: user, target: target})
1447 def empty_array(conn, _) do
1448 Logger.debug("Unimplemented, returning an empty array")
1452 def empty_object(conn, _) do
1453 Logger.debug("Unimplemented, returning an empty object")
1457 def get_filters(%{assigns: %{user: user}} = conn, _) do
1458 filters = Filter.get_filters(user)
1459 res = FilterView.render("filters.json", filters: filters)
1464 %{assigns: %{user: user}} = conn,
1465 %{"phrase" => phrase, "context" => context} = params
1471 hide: Map.get(params, "irreversible", false),
1472 whole_word: Map.get(params, "boolean", true)
1476 {:ok, response} = Filter.create(query)
1477 res = FilterView.render("filter.json", filter: response)
1481 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1482 filter = Filter.get(filter_id, user)
1483 res = FilterView.render("filter.json", filter: filter)
1488 %{assigns: %{user: user}} = conn,
1489 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1493 filter_id: filter_id,
1496 hide: Map.get(params, "irreversible", nil),
1497 whole_word: Map.get(params, "boolean", true)
1501 {:ok, response} = Filter.update(query)
1502 res = FilterView.render("filter.json", filter: response)
1506 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1509 filter_id: filter_id
1512 {:ok, _} = Filter.delete(query)
1516 def suggestions(%{assigns: %{user: user}} = conn, _) do
1517 suggestions = Config.get(:suggestions)
1519 if Keyword.get(suggestions, :enabled, false) do
1520 api = Keyword.get(suggestions, :third_party_engine, "")
1521 timeout = Keyword.get(suggestions, :timeout, 5000)
1522 limit = Keyword.get(suggestions, :limit, 23)
1524 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1526 user = user.nickname
1530 |> String.replace("{{host}}", host)
1531 |> String.replace("{{user}}", user)
1533 with {:ok, %{status: 200, body: body}} <-
1534 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1535 {:ok, data} <- Jason.decode(body) do
1538 |> Enum.slice(0, limit)
1541 |> Map.put("id", fetch_suggestion_id(x))
1542 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1543 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1549 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1556 defp fetch_suggestion_id(attrs) do
1557 case User.get_or_fetch(attrs["acct"]) do
1558 {:ok, %User{id: id}} -> id
1563 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1564 with %Activity{} = activity <- Activity.get_by_id(status_id),
1565 true <- Visibility.visible_for_user?(activity, user) do
1569 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1579 def reports(%{assigns: %{user: user}} = conn, params) do
1580 case CommonAPI.report(user, params) do
1583 |> put_view(ReportView)
1584 |> try_render("report.json", %{activity: activity})
1588 |> put_status(:bad_request)
1589 |> json(%{error: err})
1593 def account_register(
1594 %{assigns: %{app: app}} = conn,
1595 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1603 "captcha_answer_data",
1607 |> Map.put("nickname", nickname)
1608 |> Map.put("fullname", params["fullname"] || nickname)
1609 |> Map.put("bio", params["bio"] || "")
1610 |> Map.put("confirm", params["password"])
1612 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1613 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1615 token_type: "Bearer",
1616 access_token: token.token,
1618 created_at: Token.Utils.format_created_at(token)
1623 |> put_status(:bad_request)
1628 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1629 render_error(conn, :bad_request, "Missing parameters")
1632 def account_register(conn, _) do
1633 render_error(conn, :forbidden, "Invalid credentials")
1636 def conversations(%{assigns: %{user: user}} = conn, params) do
1637 participations = Participation.for_user_with_last_activity_id(user, params)
1640 Enum.map(participations, fn participation ->
1641 ConversationView.render("participation.json", %{participation: participation, for: user})
1645 |> add_link_headers(:conversations, participations)
1646 |> json(conversations)
1649 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1650 with %Participation{} = participation <-
1651 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1652 {:ok, participation} <- Participation.mark_as_read(participation) do
1653 participation_view =
1654 ConversationView.render("participation.json", %{participation: participation, for: user})
1657 |> json(participation_view)
1661 def password_reset(conn, params) do
1662 nickname_or_email = params["email"] || params["nickname"]
1664 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1666 |> put_status(:no_content)
1669 {:error, "unknown user"} ->
1670 send_resp(conn, :not_found, "")
1673 send_resp(conn, :bad_request, "")
1677 def account_confirmation_resend(conn, params) do
1678 nickname_or_email = params["email"] || params["nickname"]
1680 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1681 {:ok, _} <- User.try_send_confirmation_email(user) do
1683 |> json_response(:no_content, "")
1687 def try_render(conn, target, params)
1688 when is_binary(target) do
1689 case render(conn, target, params) do
1690 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1695 def try_render(conn, _, _) do
1696 render_error(conn, :not_implemented, "Can't display this activity")
1699 defp present?(nil), do: false
1700 defp present?(false), do: false
1701 defp present?(_), do: true