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
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))
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)
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
453 activities: grouped_activities[true] || [],
457 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
462 activities: grouped_activities[false] || [],
466 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
473 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
474 with %Object{} = object <- Object.get_by_id(id),
475 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
476 true <- Visibility.visible_for_user?(activity, user) do
478 |> put_view(StatusView)
479 |> try_render("poll.json", %{object: object, for: user})
481 error when is_nil(error) or error == false ->
482 render_error(conn, :not_found, "Record not found")
486 defp get_cached_vote_or_vote(user, object, choices) do
487 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
490 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
491 case CommonAPI.vote(user, object, choices) do
492 {:error, _message} = res -> {:ignore, res}
493 res -> {:commit, res}
500 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
501 with %Object{} = object <- Object.get_by_id(id),
502 true <- object.data["type"] == "Question",
503 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
504 true <- Visibility.visible_for_user?(activity, user),
505 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
507 |> put_view(StatusView)
508 |> try_render("poll.json", %{object: object, for: user})
511 render_error(conn, :not_found, "Record not found")
514 render_error(conn, :not_found, "Record not found")
518 |> put_status(:unprocessable_entity)
519 |> json(%{error: message})
523 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
524 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
526 |> add_link_headers(:scheduled_statuses, scheduled_activities)
527 |> put_view(ScheduledActivityView)
528 |> render("index.json", %{scheduled_activities: scheduled_activities})
532 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
533 with %ScheduledActivity{} = scheduled_activity <-
534 ScheduledActivity.get(user, scheduled_activity_id) do
536 |> put_view(ScheduledActivityView)
537 |> render("show.json", %{scheduled_activity: scheduled_activity})
539 _ -> {:error, :not_found}
543 def update_scheduled_status(
544 %{assigns: %{user: user}} = conn,
545 %{"id" => scheduled_activity_id} = params
547 with %ScheduledActivity{} = scheduled_activity <-
548 ScheduledActivity.get(user, scheduled_activity_id),
549 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
551 |> put_view(ScheduledActivityView)
552 |> render("show.json", %{scheduled_activity: scheduled_activity})
554 nil -> {:error, :not_found}
559 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
560 with %ScheduledActivity{} = scheduled_activity <-
561 ScheduledActivity.get(user, scheduled_activity_id),
562 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
564 |> put_view(ScheduledActivityView)
565 |> render("show.json", %{scheduled_activity: scheduled_activity})
567 nil -> {:error, :not_found}
572 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
575 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
577 scheduled_at = params["scheduled_at"]
579 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
580 with {:ok, scheduled_activity} <-
581 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
583 |> put_view(ScheduledActivityView)
584 |> render("show.json", %{scheduled_activity: scheduled_activity})
587 params = Map.drop(params, ["scheduled_at"])
589 case CommonAPI.post(user, params) do
592 |> put_status(:unprocessable_entity)
593 |> json(%{error: message})
597 |> put_view(StatusView)
598 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
603 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
604 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
607 _e -> render_error(conn, :forbidden, "Can't delete this post")
611 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
612 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
613 %Activity{} = announce <- Activity.normalize(announce.data) do
615 |> put_view(StatusView)
616 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
620 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
621 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
622 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
624 |> put_view(StatusView)
625 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
629 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
630 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
631 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
633 |> put_view(StatusView)
634 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
638 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
639 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
640 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
647 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
648 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
650 |> put_view(StatusView)
651 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
655 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
656 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
658 |> put_view(StatusView)
659 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
663 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
664 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
665 %User{} = user <- User.get_cached_by_nickname(user.nickname),
666 true <- Visibility.visible_for_user?(activity, user),
667 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
675 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
676 %User{} = user <- User.get_cached_by_nickname(user.nickname),
677 true <- Visibility.visible_for_user?(activity, user),
678 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
680 |> put_view(StatusView)
681 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
685 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
686 activity = Activity.get_by_id(id)
688 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
695 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 activity = Activity.get_by_id(id)
698 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
700 |> put_view(StatusView)
701 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
705 def notifications(%{assigns: %{user: user}} = conn, params) do
706 notifications = MastodonAPI.get_notifications(user, params)
709 |> add_link_headers(:notifications, notifications)
710 |> put_view(NotificationView)
711 |> render("index.json", %{notifications: notifications, for: user})
714 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
715 with {:ok, notification} <- Notification.get(user, id) do
717 |> put_view(NotificationView)
718 |> render("show.json", %{notification: notification, for: user})
722 |> put_status(:forbidden)
723 |> json(%{"error" => reason})
727 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
728 Notification.clear(user)
732 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
733 with {:ok, _notif} <- Notification.dismiss(user, id) do
738 |> put_status(:forbidden)
739 |> json(%{"error" => reason})
743 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
744 Notification.destroy_multiple(user, ids)
748 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
750 q = from(u in User, where: u.id in ^id)
751 targets = Repo.all(q)
754 |> put_view(AccountView)
755 |> render("relationships.json", %{user: user, targets: targets})
758 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
759 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
761 def update_media(%{assigns: %{user: user}} = conn, data) do
762 with %Object{} = object <- Repo.get(Object, data["id"]),
763 true <- Object.authorize_mutation(object, user),
764 true <- is_binary(data["description"]),
765 description <- data["description"] do
766 new_data = %{object.data | "name" => description}
770 |> Object.change(%{data: new_data})
773 attachment_data = Map.put(new_data, "id", object.id)
776 |> put_view(StatusView)
777 |> render("attachment.json", %{attachment: attachment_data})
781 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
782 with {:ok, object} <-
785 actor: User.ap_id(user),
786 description: Map.get(data, "description")
788 attachment_data = Map.put(object.data, "id", object.id)
791 |> put_view(StatusView)
792 |> render("attachment.json", %{attachment: attachment_data})
796 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
797 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
798 %{} = attachment_data <- Map.put(object.data, "id", object.id),
799 %{type: type} = rendered <-
800 StatusView.render("attachment.json", %{attachment: attachment_data}) do
801 # Reject if not an image
802 if type == "image" do
804 # Save to the user's info
805 info_changeset = User.Info.mascot_update(user.info, rendered)
809 |> Changeset.change()
810 |> Changeset.put_embed(:info, info_changeset)
812 {:ok, _user} = User.update_and_set_cache(user_changeset)
817 render_error(conn, :unsupported_media_type, "mascots can only be images")
822 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
823 mascot = User.get_mascot(user)
829 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
830 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
831 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
832 q = from(u in User, where: u.ap_id in ^likes)
836 |> Enum.filter(&(not User.blocks?(user, &1)))
839 |> put_view(AccountView)
840 |> render("accounts.json", %{for: user, users: users, as: :user})
846 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
847 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
848 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
849 q = from(u in User, where: u.ap_id in ^announces)
853 |> Enum.filter(&(not User.blocks?(user, &1)))
856 |> put_view(AccountView)
857 |> render("accounts.json", %{for: user, users: users, as: :user})
863 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
864 local_only = params["local"] in [true, "True", "true", "1"]
867 [params["tag"], params["any"]]
871 |> Enum.map(&String.downcase(&1))
876 |> Enum.map(&String.downcase(&1))
881 |> Enum.map(&String.downcase(&1))
885 |> Map.put("type", "Create")
886 |> Map.put("local_only", local_only)
887 |> Map.put("blocking_user", user)
888 |> Map.put("muting_user", user)
889 |> Map.put("user", user)
890 |> Map.put("tag", tags)
891 |> Map.put("tag_all", tag_all)
892 |> Map.put("tag_reject", tag_reject)
893 |> ActivityPub.fetch_public_activities()
897 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
898 |> put_view(StatusView)
899 |> render("index.json", %{activities: activities, for: user, as: :activity})
902 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
903 with %User{} = user <- User.get_cached_by_id(id),
904 followers <- MastodonAPI.get_followers(user, params) do
907 for_user && user.id == for_user.id -> followers
908 user.info.hide_followers -> []
913 |> add_link_headers(:followers, followers, user)
914 |> put_view(AccountView)
915 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
919 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
920 with %User{} = user <- User.get_cached_by_id(id),
921 followers <- MastodonAPI.get_friends(user, params) do
924 for_user && user.id == for_user.id -> followers
925 user.info.hide_follows -> []
930 |> add_link_headers(:following, followers, user)
931 |> put_view(AccountView)
932 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
936 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
937 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
939 |> put_view(AccountView)
940 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
944 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
945 with %User{} = follower <- User.get_cached_by_id(id),
946 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
948 |> put_view(AccountView)
949 |> render("relationship.json", %{user: followed, target: follower})
953 |> put_status(:forbidden)
954 |> json(%{error: message})
958 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
959 with %User{} = follower <- User.get_cached_by_id(id),
960 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
962 |> put_view(AccountView)
963 |> render("relationship.json", %{user: followed, target: follower})
967 |> put_status(:forbidden)
968 |> json(%{error: message})
972 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
973 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
974 {_, true} <- {:followed, follower.id != followed.id},
975 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
977 |> put_view(AccountView)
978 |> render("relationship.json", %{user: follower, target: followed})
985 |> put_status(:forbidden)
986 |> json(%{error: message})
990 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
991 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
992 {_, true} <- {:followed, follower.id != followed.id},
993 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
995 |> put_view(AccountView)
996 |> render("account.json", %{user: followed, for: follower})
1001 {:error, message} ->
1003 |> put_status(:forbidden)
1004 |> json(%{error: message})
1008 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1009 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1010 {_, true} <- {:followed, follower.id != followed.id},
1011 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1013 |> put_view(AccountView)
1014 |> render("relationship.json", %{user: follower, target: followed})
1017 {:error, :not_found}
1024 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1026 if Map.has_key?(params, "notifications"),
1027 do: params["notifications"] in [true, "True", "true", "1"],
1030 with %User{} = muted <- User.get_cached_by_id(id),
1031 {:ok, muter} <- User.mute(muter, muted, notifications) do
1033 |> put_view(AccountView)
1034 |> render("relationship.json", %{user: muter, target: muted})
1036 {:error, message} ->
1038 |> put_status(:forbidden)
1039 |> json(%{error: message})
1043 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1044 with %User{} = muted <- User.get_cached_by_id(id),
1045 {:ok, muter} <- User.unmute(muter, muted) do
1047 |> put_view(AccountView)
1048 |> render("relationship.json", %{user: muter, target: muted})
1050 {:error, message} ->
1052 |> put_status(:forbidden)
1053 |> json(%{error: message})
1057 def mutes(%{assigns: %{user: user}} = conn, _) do
1058 with muted_accounts <- User.muted_users(user) do
1059 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1064 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1065 with %User{} = blocked <- User.get_cached_by_id(id),
1066 {:ok, blocker} <- User.block(blocker, blocked),
1067 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1069 |> put_view(AccountView)
1070 |> render("relationship.json", %{user: blocker, target: blocked})
1072 {:error, message} ->
1074 |> put_status(:forbidden)
1075 |> json(%{error: message})
1079 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1080 with %User{} = blocked <- User.get_cached_by_id(id),
1081 {:ok, blocker} <- User.unblock(blocker, blocked),
1082 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1084 |> put_view(AccountView)
1085 |> render("relationship.json", %{user: blocker, target: blocked})
1087 {:error, message} ->
1089 |> put_status(:forbidden)
1090 |> json(%{error: message})
1094 def blocks(%{assigns: %{user: user}} = conn, _) do
1095 with blocked_accounts <- User.blocked_users(user) do
1096 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1101 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1102 json(conn, info.domain_blocks || [])
1105 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1106 User.block_domain(blocker, domain)
1110 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1111 User.unblock_domain(blocker, domain)
1115 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1116 with %User{} = subscription_target <- User.get_cached_by_id(id),
1117 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1119 |> put_view(AccountView)
1120 |> render("relationship.json", %{user: user, target: subscription_target})
1122 {:error, message} ->
1124 |> put_status(:forbidden)
1125 |> json(%{error: message})
1129 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1130 with %User{} = subscription_target <- User.get_cached_by_id(id),
1131 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1133 |> put_view(AccountView)
1134 |> render("relationship.json", %{user: user, target: subscription_target})
1136 {:error, message} ->
1138 |> put_status(:forbidden)
1139 |> json(%{error: message})
1143 def favourites(%{assigns: %{user: user}} = conn, params) do
1146 |> Map.put("type", "Create")
1147 |> Map.put("favorited_by", user.ap_id)
1148 |> Map.put("blocking_user", user)
1151 ActivityPub.fetch_activities([], params)
1155 |> add_link_headers(:favourites, activities)
1156 |> put_view(StatusView)
1157 |> render("index.json", %{activities: activities, for: user, as: :activity})
1160 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1161 with %User{} = user <- User.get_by_id(id),
1162 false <- user.info.hide_favorites do
1165 |> Map.put("type", "Create")
1166 |> Map.put("favorited_by", user.ap_id)
1167 |> Map.put("blocking_user", for_user)
1171 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1173 [Pleroma.Constants.as_public()]
1178 |> ActivityPub.fetch_activities(params)
1182 |> add_link_headers(:favourites, activities)
1183 |> put_view(StatusView)
1184 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1186 nil -> {:error, :not_found}
1187 true -> render_error(conn, :forbidden, "Can't get favorites")
1191 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1192 user = User.get_cached_by_id(user.id)
1195 Bookmark.for_user_query(user.id)
1196 |> Pagination.fetch_paginated(params)
1200 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1203 |> add_link_headers(:bookmarks, bookmarks)
1204 |> put_view(StatusView)
1205 |> render("index.json", %{activities: activities, for: user, as: :activity})
1208 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1209 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1210 res = ListView.render("lists.json", lists: lists)
1214 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1215 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1218 |> Map.put("type", "Create")
1219 |> Map.put("blocking_user", user)
1220 |> Map.put("user", user)
1221 |> Map.put("muting_user", user)
1223 # we must filter the following list for the user to avoid leaking statuses the user
1224 # does not actually have permission to see (for more info, peruse security issue #270).
1227 |> Enum.filter(fn x -> x in user.following end)
1228 |> ActivityPub.fetch_activities_bounded(following, params)
1232 |> put_view(StatusView)
1233 |> render("index.json", %{activities: activities, for: user, as: :activity})
1235 _e -> render_error(conn, :forbidden, "Error.")
1239 def index(%{assigns: %{user: user}} = conn, _params) do
1240 token = get_session(conn, :oauth_token)
1243 mastodon_emoji = mastodonized_emoji()
1245 limit = Config.get([:instance, :limit])
1248 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1253 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1254 access_token: token,
1256 domain: Pleroma.Web.Endpoint.host(),
1259 unfollow_modal: false,
1262 auto_play_gif: false,
1263 display_sensitive_media: false,
1264 reduce_motion: false,
1265 max_toot_chars: limit,
1266 mascot: User.get_mascot(user)["url"]
1268 poll_limits: Config.get([:instance, :poll_limits]),
1270 delete_others_notice: present?(user.info.is_moderator),
1271 admin: present?(user.info.is_admin)
1275 default_privacy: user.info.default_scope,
1276 default_sensitive: false,
1277 allow_content_types: Config.get([:instance, :allowed_post_formats])
1279 media_attachments: %{
1280 accept_content_types: [
1296 user.info.settings ||
1326 push_subscription: nil,
1328 custom_emojis: mastodon_emoji,
1334 |> put_layout(false)
1335 |> put_view(MastodonView)
1336 |> render("index.html", %{initial_state: initial_state})
1339 |> put_session(:return_to, conn.request_path)
1340 |> redirect(to: "/web/login")
1344 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1345 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1347 with changeset <- Changeset.change(user),
1348 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1349 {:ok, _user} <- User.update_and_set_cache(changeset) do
1354 |> put_status(:internal_server_error)
1355 |> json(%{error: inspect(e)})
1359 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1360 redirect(conn, to: local_mastodon_root_path(conn))
1363 @doc "Local Mastodon FE login init action"
1364 def login(conn, %{"code" => auth_token}) do
1365 with {:ok, app} <- get_or_make_app(),
1366 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1367 {:ok, token} <- Token.exchange_token(app, auth) do
1369 |> put_session(:oauth_token, token.token)
1370 |> redirect(to: local_mastodon_root_path(conn))
1374 @doc "Local Mastodon FE callback action"
1375 def login(conn, _) do
1376 with {:ok, app} <- get_or_make_app() do
1381 response_type: "code",
1382 client_id: app.client_id,
1384 scope: Enum.join(app.scopes, " ")
1387 redirect(conn, to: path)
1391 defp local_mastodon_root_path(conn) do
1392 case get_session(conn, :return_to) do
1394 mastodon_api_path(conn, :index, ["getting-started"])
1397 delete_session(conn, :return_to)
1402 defp get_or_make_app do
1403 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1404 scopes = ["read", "write", "follow", "push"]
1406 with %App{} = app <- Repo.get_by(App, find_attrs) do
1408 if app.scopes == scopes do
1412 |> Changeset.change(%{scopes: scopes})
1420 App.register_changeset(
1422 Map.put(find_attrs, :scopes, scopes)
1429 def logout(conn, _) do
1432 |> redirect(to: "/")
1435 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1436 Logger.debug("Unimplemented, returning unmodified relationship")
1438 with %User{} = target <- User.get_cached_by_id(id) do
1440 |> put_view(AccountView)
1441 |> render("relationship.json", %{user: user, target: target})
1445 def empty_array(conn, _) do
1446 Logger.debug("Unimplemented, returning an empty array")
1450 def empty_object(conn, _) do
1451 Logger.debug("Unimplemented, returning an empty object")
1455 def get_filters(%{assigns: %{user: user}} = conn, _) do
1456 filters = Filter.get_filters(user)
1457 res = FilterView.render("filters.json", filters: filters)
1462 %{assigns: %{user: user}} = conn,
1463 %{"phrase" => phrase, "context" => context} = params
1469 hide: Map.get(params, "irreversible", false),
1470 whole_word: Map.get(params, "boolean", true)
1474 {:ok, response} = Filter.create(query)
1475 res = FilterView.render("filter.json", filter: response)
1479 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1480 filter = Filter.get(filter_id, user)
1481 res = FilterView.render("filter.json", filter: filter)
1486 %{assigns: %{user: user}} = conn,
1487 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1491 filter_id: filter_id,
1494 hide: Map.get(params, "irreversible", nil),
1495 whole_word: Map.get(params, "boolean", true)
1499 {:ok, response} = Filter.update(query)
1500 res = FilterView.render("filter.json", filter: response)
1504 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1507 filter_id: filter_id
1510 {:ok, _} = Filter.delete(query)
1514 def suggestions(%{assigns: %{user: user}} = conn, _) do
1515 suggestions = Config.get(:suggestions)
1517 if Keyword.get(suggestions, :enabled, false) do
1518 api = Keyword.get(suggestions, :third_party_engine, "")
1519 timeout = Keyword.get(suggestions, :timeout, 5000)
1520 limit = Keyword.get(suggestions, :limit, 23)
1522 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1524 user = user.nickname
1528 |> String.replace("{{host}}", host)
1529 |> String.replace("{{user}}", user)
1531 with {:ok, %{status: 200, body: body}} <-
1532 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1533 {:ok, data} <- Jason.decode(body) do
1536 |> Enum.slice(0, limit)
1539 |> Map.put("id", fetch_suggestion_id(x))
1540 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1541 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1547 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1554 defp fetch_suggestion_id(attrs) do
1555 case User.get_or_fetch(attrs["acct"]) do
1556 {:ok, %User{id: id}} -> id
1561 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1562 with %Activity{} = activity <- Activity.get_by_id(status_id),
1563 true <- Visibility.visible_for_user?(activity, user) do
1567 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1577 def reports(%{assigns: %{user: user}} = conn, params) do
1578 case CommonAPI.report(user, params) do
1581 |> put_view(ReportView)
1582 |> try_render("report.json", %{activity: activity})
1586 |> put_status(:bad_request)
1587 |> json(%{error: err})
1591 def account_register(
1592 %{assigns: %{app: app}} = conn,
1593 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1601 "captcha_answer_data",
1605 |> Map.put("nickname", nickname)
1606 |> Map.put("fullname", params["fullname"] || nickname)
1607 |> Map.put("bio", params["bio"] || "")
1608 |> Map.put("confirm", params["password"])
1610 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1611 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1613 token_type: "Bearer",
1614 access_token: token.token,
1616 created_at: Token.Utils.format_created_at(token)
1621 |> put_status(:bad_request)
1626 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1627 render_error(conn, :bad_request, "Missing parameters")
1630 def account_register(conn, _) do
1631 render_error(conn, :forbidden, "Invalid credentials")
1634 def conversations(%{assigns: %{user: user}} = conn, params) do
1635 participations = Participation.for_user_with_last_activity_id(user, params)
1638 Enum.map(participations, fn participation ->
1639 ConversationView.render("participation.json", %{participation: participation, for: user})
1643 |> add_link_headers(:conversations, participations)
1644 |> json(conversations)
1647 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1648 with %Participation{} = participation <-
1649 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1650 {:ok, participation} <- Participation.mark_as_read(participation) do
1651 participation_view =
1652 ConversationView.render("participation.json", %{participation: participation, for: user})
1655 |> json(participation_view)
1659 def password_reset(conn, params) do
1660 nickname_or_email = params["email"] || params["nickname"]
1662 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1664 |> put_status(:no_content)
1667 {:error, "unknown user"} ->
1668 send_resp(conn, :not_found, "")
1671 send_resp(conn, :bad_request, "")
1675 def account_confirmation_resend(conn, params) do
1676 nickname_or_email = params["email"] || params["nickname"]
1678 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1679 {:ok, _} <- User.try_send_confirmation_email(user) do
1681 |> json_response(:no_content, "")
1685 def try_render(conn, target, params)
1686 when is_binary(target) do
1687 case render(conn, target, params) do
1688 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1693 def try_render(conn, _, _) do
1694 render_error(conn, :not_implemented, "Can't display this activity")
1697 defp present?(nil), do: false
1698 defp present?(false), do: false
1699 defp present?(_), do: true