1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
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(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(activities, %{"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(activities)
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(activities)
426 |> put_view(StatusView)
427 |> render("index.json", %{activities: activities, for: user, as: :activity})
430 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
436 |> Activity.all_by_ids_with_object()
437 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
440 |> put_view(StatusView)
441 |> render("index.json", activities: activities, for: user, as: :activity)
444 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
445 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
446 true <- Visibility.visible_for_user?(activity, user) do
448 |> put_view(StatusView)
449 |> try_render("status.json", %{activity: activity, for: user})
453 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
454 with %Activity{} = activity <- Activity.get_by_id(id),
456 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
457 "blocking_user" => user,
459 "exclude_id" => activity.id
461 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
467 activities: grouped_activities[true] || [],
471 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
476 activities: grouped_activities[false] || [],
480 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
487 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
488 with %Object{} = object <- Object.get_by_id(id),
489 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
490 true <- Visibility.visible_for_user?(activity, user) do
492 |> put_view(StatusView)
493 |> try_render("poll.json", %{object: object, for: user})
495 error when is_nil(error) or error == false ->
496 render_error(conn, :not_found, "Record not found")
500 defp get_cached_vote_or_vote(user, object, choices) do
501 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
504 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
505 case CommonAPI.vote(user, object, choices) do
506 {:error, _message} = res -> {:ignore, res}
507 res -> {:commit, res}
514 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
515 with %Object{} = object <- Object.get_by_id(id),
516 true <- object.data["type"] == "Question",
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
518 true <- Visibility.visible_for_user?(activity, user),
519 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
521 |> put_view(StatusView)
522 |> try_render("poll.json", %{object: object, for: user})
525 render_error(conn, :not_found, "Record not found")
528 render_error(conn, :not_found, "Record not found")
532 |> put_status(:unprocessable_entity)
533 |> json(%{error: message})
537 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
538 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
540 |> add_link_headers(scheduled_activities)
541 |> put_view(ScheduledActivityView)
542 |> render("index.json", %{scheduled_activities: scheduled_activities})
546 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
547 with %ScheduledActivity{} = scheduled_activity <-
548 ScheduledActivity.get(user, scheduled_activity_id) do
550 |> put_view(ScheduledActivityView)
551 |> render("show.json", %{scheduled_activity: scheduled_activity})
553 _ -> {:error, :not_found}
557 def update_scheduled_status(
558 %{assigns: %{user: user}} = conn,
559 %{"id" => scheduled_activity_id} = params
561 with %ScheduledActivity{} = scheduled_activity <-
562 ScheduledActivity.get(user, scheduled_activity_id),
563 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
565 |> put_view(ScheduledActivityView)
566 |> render("show.json", %{scheduled_activity: scheduled_activity})
568 nil -> {:error, :not_found}
573 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
574 with %ScheduledActivity{} = scheduled_activity <-
575 ScheduledActivity.get(user, scheduled_activity_id),
576 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
578 |> put_view(ScheduledActivityView)
579 |> render("show.json", %{scheduled_activity: scheduled_activity})
581 nil -> {:error, :not_found}
586 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
589 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
591 scheduled_at = params["scheduled_at"]
593 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
594 with {:ok, scheduled_activity} <-
595 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
597 |> put_view(ScheduledActivityView)
598 |> render("show.json", %{scheduled_activity: scheduled_activity})
601 params = Map.drop(params, ["scheduled_at"])
603 case CommonAPI.post(user, params) do
606 |> put_status(:unprocessable_entity)
607 |> json(%{error: message})
611 |> put_view(StatusView)
612 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
617 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
618 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
621 _e -> render_error(conn, :forbidden, "Can't delete this post")
625 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
626 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
627 %Activity{} = announce <- Activity.normalize(announce.data) do
629 |> put_view(StatusView)
630 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
634 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
635 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
636 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
638 |> put_view(StatusView)
639 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
643 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
644 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
645 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
647 |> put_view(StatusView)
648 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
652 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
653 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
654 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
656 |> put_view(StatusView)
657 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
661 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
662 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
664 |> put_view(StatusView)
665 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
669 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
670 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
672 |> put_view(StatusView)
673 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
677 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
678 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
679 %User{} = user <- User.get_cached_by_nickname(user.nickname),
680 true <- Visibility.visible_for_user?(activity, user),
681 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
683 |> put_view(StatusView)
684 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
688 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
689 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
690 %User{} = user <- User.get_cached_by_nickname(user.nickname),
691 true <- Visibility.visible_for_user?(activity, user),
692 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
694 |> put_view(StatusView)
695 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
699 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
700 activity = Activity.get_by_id(id)
702 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
704 |> put_view(StatusView)
705 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
709 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
710 activity = Activity.get_by_id(id)
712 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
714 |> put_view(StatusView)
715 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
719 def notifications(%{assigns: %{user: user}} = conn, params) do
720 notifications = MastodonAPI.get_notifications(user, params)
723 |> add_link_headers(notifications)
724 |> put_view(NotificationView)
725 |> render("index.json", %{notifications: notifications, for: user})
728 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
729 with {:ok, notification} <- Notification.get(user, id) do
731 |> put_view(NotificationView)
732 |> render("show.json", %{notification: notification, for: user})
736 |> put_status(:forbidden)
737 |> json(%{"error" => reason})
741 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
742 Notification.clear(user)
746 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
747 with {:ok, _notif} <- Notification.dismiss(user, id) do
752 |> put_status(:forbidden)
753 |> json(%{"error" => reason})
757 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
758 Notification.destroy_multiple(user, ids)
762 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
764 q = from(u in User, where: u.id in ^id)
765 targets = Repo.all(q)
768 |> put_view(AccountView)
769 |> render("relationships.json", %{user: user, targets: targets})
772 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
773 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
775 def update_media(%{assigns: %{user: user}} = conn, data) do
776 with %Object{} = object <- Repo.get(Object, data["id"]),
777 true <- Object.authorize_mutation(object, user),
778 true <- is_binary(data["description"]),
779 description <- data["description"] do
780 new_data = %{object.data | "name" => description}
784 |> Object.change(%{data: new_data})
787 attachment_data = Map.put(new_data, "id", object.id)
790 |> put_view(StatusView)
791 |> render("attachment.json", %{attachment: attachment_data})
795 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
796 with {:ok, object} <-
799 actor: User.ap_id(user),
800 description: Map.get(data, "description")
802 attachment_data = Map.put(object.data, "id", object.id)
805 |> put_view(StatusView)
806 |> render("attachment.json", %{attachment: attachment_data})
810 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
811 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
812 %{} = attachment_data <- Map.put(object.data, "id", object.id),
813 %{type: type} = rendered <-
814 StatusView.render("attachment.json", %{attachment: attachment_data}) do
815 # Reject if not an image
816 if type == "image" do
818 # Save to the user's info
819 info_changeset = User.Info.mascot_update(user.info, rendered)
823 |> Changeset.change()
824 |> Changeset.put_embed(:info, info_changeset)
826 {:ok, _user} = User.update_and_set_cache(user_changeset)
831 render_error(conn, :unsupported_media_type, "mascots can only be images")
836 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
837 mascot = User.get_mascot(user)
843 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
844 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
845 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
846 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
847 q = from(u in User, where: u.ap_id in ^likes)
851 |> Enum.filter(&(not User.blocks?(user, &1)))
854 |> put_view(AccountView)
855 |> render("accounts.json", %{for: user, users: users, as: :user})
857 {:visible, false} -> {:error, :not_found}
862 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
863 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
864 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
865 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
866 q = from(u in User, where: u.ap_id in ^announces)
870 |> Enum.filter(&(not User.blocks?(user, &1)))
873 |> put_view(AccountView)
874 |> render("accounts.json", %{for: user, users: users, as: :user})
876 {:visible, false} -> {:error, :not_found}
881 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
882 local_only = params["local"] in [true, "True", "true", "1"]
885 [params["tag"], params["any"]]
889 |> Enum.map(&String.downcase(&1))
894 |> Enum.map(&String.downcase(&1))
899 |> Enum.map(&String.downcase(&1))
903 |> Map.put("type", "Create")
904 |> Map.put("local_only", local_only)
905 |> Map.put("blocking_user", user)
906 |> Map.put("muting_user", user)
907 |> Map.put("user", user)
908 |> Map.put("tag", tags)
909 |> Map.put("tag_all", tag_all)
910 |> Map.put("tag_reject", tag_reject)
911 |> ActivityPub.fetch_public_activities()
915 |> add_link_headers(activities, %{"local" => local_only})
916 |> put_view(StatusView)
917 |> render("index.json", %{activities: activities, for: user, as: :activity})
920 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
921 with %User{} = user <- User.get_cached_by_id(id),
922 followers <- MastodonAPI.get_followers(user, params) do
925 for_user && user.id == for_user.id -> followers
926 user.info.hide_followers -> []
931 |> add_link_headers(followers)
932 |> put_view(AccountView)
933 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
937 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
938 with %User{} = user <- User.get_cached_by_id(id),
939 followers <- MastodonAPI.get_friends(user, params) do
942 for_user && user.id == for_user.id -> followers
943 user.info.hide_follows -> []
948 |> add_link_headers(followers)
949 |> put_view(AccountView)
950 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
954 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
955 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
957 |> put_view(AccountView)
958 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
962 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
963 with %User{} = follower <- User.get_cached_by_id(id),
964 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
966 |> put_view(AccountView)
967 |> render("relationship.json", %{user: followed, target: follower})
971 |> put_status(:forbidden)
972 |> json(%{error: message})
976 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
977 with %User{} = follower <- User.get_cached_by_id(id),
978 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
980 |> put_view(AccountView)
981 |> render("relationship.json", %{user: followed, target: follower})
985 |> put_status(:forbidden)
986 |> json(%{error: message})
990 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
991 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
992 {_, true} <- {:followed, follower.id != followed.id},
993 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
995 |> put_view(AccountView)
996 |> render("relationship.json", %{user: follower, target: followed})
1001 {:error, message} ->
1003 |> put_status(:forbidden)
1004 |> json(%{error: message})
1008 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1009 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1010 {_, true} <- {:followed, follower.id != followed.id},
1011 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1013 |> put_view(AccountView)
1014 |> render("account.json", %{user: followed, for: follower})
1017 {:error, :not_found}
1019 {:error, message} ->
1021 |> put_status(:forbidden)
1022 |> json(%{error: message})
1026 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1027 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1028 {_, true} <- {:followed, follower.id != followed.id},
1029 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1031 |> put_view(AccountView)
1032 |> render("relationship.json", %{user: follower, target: followed})
1035 {:error, :not_found}
1042 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1044 if Map.has_key?(params, "notifications"),
1045 do: params["notifications"] in [true, "True", "true", "1"],
1048 with %User{} = muted <- User.get_cached_by_id(id),
1049 {:ok, muter} <- User.mute(muter, muted, notifications) do
1051 |> put_view(AccountView)
1052 |> render("relationship.json", %{user: muter, target: muted})
1054 {:error, message} ->
1056 |> put_status(:forbidden)
1057 |> json(%{error: message})
1061 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1062 with %User{} = muted <- User.get_cached_by_id(id),
1063 {:ok, muter} <- User.unmute(muter, muted) do
1065 |> put_view(AccountView)
1066 |> render("relationship.json", %{user: muter, target: muted})
1068 {:error, message} ->
1070 |> put_status(:forbidden)
1071 |> json(%{error: message})
1075 def mutes(%{assigns: %{user: user}} = conn, _) do
1076 with muted_accounts <- User.muted_users(user) do
1077 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1082 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1083 with %User{} = blocked <- User.get_cached_by_id(id),
1084 {:ok, blocker} <- User.block(blocker, blocked),
1085 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1087 |> put_view(AccountView)
1088 |> render("relationship.json", %{user: blocker, target: blocked})
1090 {:error, message} ->
1092 |> put_status(:forbidden)
1093 |> json(%{error: message})
1097 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1098 with %User{} = blocked <- User.get_cached_by_id(id),
1099 {:ok, blocker} <- User.unblock(blocker, blocked),
1100 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1102 |> put_view(AccountView)
1103 |> render("relationship.json", %{user: blocker, target: blocked})
1105 {:error, message} ->
1107 |> put_status(:forbidden)
1108 |> json(%{error: message})
1112 def blocks(%{assigns: %{user: user}} = conn, _) do
1113 with blocked_accounts <- User.blocked_users(user) do
1114 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1119 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1120 json(conn, info.domain_blocks || [])
1123 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1124 User.block_domain(blocker, domain)
1128 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1129 User.unblock_domain(blocker, domain)
1133 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1134 with %User{} = subscription_target <- User.get_cached_by_id(id),
1135 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1137 |> put_view(AccountView)
1138 |> render("relationship.json", %{user: user, target: subscription_target})
1140 {:error, message} ->
1142 |> put_status(:forbidden)
1143 |> json(%{error: message})
1147 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1148 with %User{} = subscription_target <- User.get_cached_by_id(id),
1149 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1151 |> put_view(AccountView)
1152 |> render("relationship.json", %{user: user, target: subscription_target})
1154 {:error, message} ->
1156 |> put_status(:forbidden)
1157 |> json(%{error: message})
1161 def favourites(%{assigns: %{user: user}} = conn, params) do
1164 |> Map.put("type", "Create")
1165 |> Map.put("favorited_by", user.ap_id)
1166 |> Map.put("blocking_user", user)
1169 ActivityPub.fetch_activities([], params)
1173 |> add_link_headers(activities)
1174 |> put_view(StatusView)
1175 |> render("index.json", %{activities: activities, for: user, as: :activity})
1178 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1179 with %User{} = user <- User.get_by_id(id),
1180 false <- user.info.hide_favorites do
1183 |> Map.put("type", "Create")
1184 |> Map.put("favorited_by", user.ap_id)
1185 |> Map.put("blocking_user", for_user)
1189 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1191 [Pleroma.Constants.as_public()]
1196 |> ActivityPub.fetch_activities(params)
1200 |> add_link_headers(activities)
1201 |> put_view(StatusView)
1202 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1204 nil -> {:error, :not_found}
1205 true -> render_error(conn, :forbidden, "Can't get favorites")
1209 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1210 user = User.get_cached_by_id(user.id)
1213 Bookmark.for_user_query(user.id)
1214 |> Pagination.fetch_paginated(params)
1218 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1221 |> add_link_headers(bookmarks)
1222 |> put_view(StatusView)
1223 |> render("index.json", %{activities: activities, for: user, as: :activity})
1226 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1227 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1228 res = ListView.render("lists.json", lists: lists)
1232 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1233 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1236 |> Map.put("type", "Create")
1237 |> Map.put("blocking_user", user)
1238 |> Map.put("user", user)
1239 |> Map.put("muting_user", user)
1241 # we must filter the following list for the user to avoid leaking statuses the user
1242 # does not actually have permission to see (for more info, peruse security issue #270).
1245 |> Enum.filter(fn x -> x in user.following end)
1246 |> ActivityPub.fetch_activities_bounded(following, params)
1250 |> put_view(StatusView)
1251 |> render("index.json", %{activities: activities, for: user, as: :activity})
1253 _e -> render_error(conn, :forbidden, "Error.")
1257 def index(%{assigns: %{user: user}} = conn, _params) do
1258 token = get_session(conn, :oauth_token)
1261 mastodon_emoji = mastodonized_emoji()
1263 limit = Config.get([:instance, :limit])
1266 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1271 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1272 access_token: token,
1274 domain: Pleroma.Web.Endpoint.host(),
1277 unfollow_modal: false,
1280 auto_play_gif: false,
1281 display_sensitive_media: false,
1282 reduce_motion: false,
1283 max_toot_chars: limit,
1284 mascot: User.get_mascot(user)["url"]
1286 poll_limits: Config.get([:instance, :poll_limits]),
1288 delete_others_notice: present?(user.info.is_moderator),
1289 admin: present?(user.info.is_admin)
1293 default_privacy: user.info.default_scope,
1294 default_sensitive: false,
1295 allow_content_types: Config.get([:instance, :allowed_post_formats])
1297 media_attachments: %{
1298 accept_content_types: [
1314 user.info.settings ||
1344 push_subscription: nil,
1346 custom_emojis: mastodon_emoji,
1352 |> put_layout(false)
1353 |> put_view(MastodonView)
1354 |> render("index.html", %{initial_state: initial_state})
1357 |> put_session(:return_to, conn.request_path)
1358 |> redirect(to: "/web/login")
1362 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1363 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1365 with changeset <- Changeset.change(user),
1366 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1367 {:ok, _user} <- User.update_and_set_cache(changeset) do
1372 |> put_status(:internal_server_error)
1373 |> json(%{error: inspect(e)})
1377 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1378 redirect(conn, to: local_mastodon_root_path(conn))
1381 @doc "Local Mastodon FE login init action"
1382 def login(conn, %{"code" => auth_token}) do
1383 with {:ok, app} <- get_or_make_app(),
1384 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1385 {:ok, token} <- Token.exchange_token(app, auth) do
1387 |> put_session(:oauth_token, token.token)
1388 |> redirect(to: local_mastodon_root_path(conn))
1392 @doc "Local Mastodon FE callback action"
1393 def login(conn, _) do
1394 with {:ok, app} <- get_or_make_app() do
1399 response_type: "code",
1400 client_id: app.client_id,
1402 scope: Enum.join(app.scopes, " ")
1405 redirect(conn, to: path)
1409 defp local_mastodon_root_path(conn) do
1410 case get_session(conn, :return_to) do
1412 mastodon_api_path(conn, :index, ["getting-started"])
1415 delete_session(conn, :return_to)
1420 defp get_or_make_app do
1421 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1422 scopes = ["read", "write", "follow", "push"]
1424 with %App{} = app <- Repo.get_by(App, find_attrs) do
1426 if app.scopes == scopes do
1430 |> Changeset.change(%{scopes: scopes})
1438 App.register_changeset(
1440 Map.put(find_attrs, :scopes, scopes)
1447 def logout(conn, _) do
1450 |> redirect(to: "/")
1453 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1454 Logger.debug("Unimplemented, returning unmodified relationship")
1456 with %User{} = target <- User.get_cached_by_id(id) do
1458 |> put_view(AccountView)
1459 |> render("relationship.json", %{user: user, target: target})
1463 def empty_array(conn, _) do
1464 Logger.debug("Unimplemented, returning an empty array")
1468 def empty_object(conn, _) do
1469 Logger.debug("Unimplemented, returning an empty object")
1473 def get_filters(%{assigns: %{user: user}} = conn, _) do
1474 filters = Filter.get_filters(user)
1475 res = FilterView.render("filters.json", filters: filters)
1480 %{assigns: %{user: user}} = conn,
1481 %{"phrase" => phrase, "context" => context} = params
1487 hide: Map.get(params, "irreversible", false),
1488 whole_word: Map.get(params, "boolean", true)
1492 {:ok, response} = Filter.create(query)
1493 res = FilterView.render("filter.json", filter: response)
1497 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1498 filter = Filter.get(filter_id, user)
1499 res = FilterView.render("filter.json", filter: filter)
1504 %{assigns: %{user: user}} = conn,
1505 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1509 filter_id: filter_id,
1512 hide: Map.get(params, "irreversible", nil),
1513 whole_word: Map.get(params, "boolean", true)
1517 {:ok, response} = Filter.update(query)
1518 res = FilterView.render("filter.json", filter: response)
1522 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1525 filter_id: filter_id
1528 {:ok, _} = Filter.delete(query)
1532 def suggestions(%{assigns: %{user: user}} = conn, _) do
1533 suggestions = Config.get(:suggestions)
1535 if Keyword.get(suggestions, :enabled, false) do
1536 api = Keyword.get(suggestions, :third_party_engine, "")
1537 timeout = Keyword.get(suggestions, :timeout, 5000)
1538 limit = Keyword.get(suggestions, :limit, 23)
1540 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1542 user = user.nickname
1546 |> String.replace("{{host}}", host)
1547 |> String.replace("{{user}}", user)
1549 with {:ok, %{status: 200, body: body}} <-
1550 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1551 {:ok, data} <- Jason.decode(body) do
1554 |> Enum.slice(0, limit)
1557 |> Map.put("id", fetch_suggestion_id(x))
1558 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1559 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1565 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1572 defp fetch_suggestion_id(attrs) do
1573 case User.get_or_fetch(attrs["acct"]) do
1574 {:ok, %User{id: id}} -> id
1579 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1580 with %Activity{} = activity <- Activity.get_by_id(status_id),
1581 true <- Visibility.visible_for_user?(activity, user) do
1585 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1595 def reports(%{assigns: %{user: user}} = conn, params) do
1596 case CommonAPI.report(user, params) do
1599 |> put_view(ReportView)
1600 |> try_render("report.json", %{activity: activity})
1604 |> put_status(:bad_request)
1605 |> json(%{error: err})
1609 def account_register(
1610 %{assigns: %{app: app}} = conn,
1611 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1619 "captcha_answer_data",
1623 |> Map.put("nickname", nickname)
1624 |> Map.put("fullname", params["fullname"] || nickname)
1625 |> Map.put("bio", params["bio"] || "")
1626 |> Map.put("confirm", params["password"])
1628 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1629 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1631 token_type: "Bearer",
1632 access_token: token.token,
1634 created_at: Token.Utils.format_created_at(token)
1639 |> put_status(:bad_request)
1644 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1645 render_error(conn, :bad_request, "Missing parameters")
1648 def account_register(conn, _) do
1649 render_error(conn, :forbidden, "Invalid credentials")
1652 def conversations(%{assigns: %{user: user}} = conn, params) do
1653 participations = Participation.for_user_with_last_activity_id(user, params)
1656 Enum.map(participations, fn participation ->
1657 ConversationView.render("participation.json", %{participation: participation, for: user})
1661 |> add_link_headers(participations)
1662 |> json(conversations)
1665 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1666 with %Participation{} = participation <-
1667 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1668 {:ok, participation} <- Participation.mark_as_read(participation) do
1669 participation_view =
1670 ConversationView.render("participation.json", %{participation: participation, for: user})
1673 |> json(participation_view)
1677 def password_reset(conn, params) do
1678 nickname_or_email = params["email"] || params["nickname"]
1680 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1682 |> put_status(:no_content)
1685 {:error, "unknown user"} ->
1686 send_resp(conn, :not_found, "")
1689 send_resp(conn, :bad_request, "")
1693 def account_confirmation_resend(conn, params) do
1694 nickname_or_email = params["email"] || params["nickname"]
1696 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1697 {:ok, _} <- User.try_send_confirmation_email(user) do
1699 |> json_response(:no_content, "")
1703 def try_render(conn, target, params)
1704 when is_binary(target) do
1705 case render(conn, target, params) do
1706 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1711 def try_render(conn, _, _) do
1712 render_error(conn, :not_implemented, "Can't display this activity")
1715 defp present?(nil), do: false
1716 defp present?(false), do: false
1717 defp present?(_), do: true