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 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
846 q = from(u in User, where: u.ap_id in ^likes)
850 |> Enum.filter(&(not User.blocks?(user, &1)))
853 |> put_view(AccountView)
854 |> render("accounts.json", %{for: user, users: users, as: :user})
860 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
861 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
862 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
863 q = from(u in User, where: u.ap_id in ^announces)
867 |> Enum.filter(&(not User.blocks?(user, &1)))
870 |> put_view(AccountView)
871 |> render("accounts.json", %{for: user, users: users, as: :user})
877 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
878 local_only = params["local"] in [true, "True", "true", "1"]
881 [params["tag"], params["any"]]
885 |> Enum.map(&String.downcase(&1))
890 |> Enum.map(&String.downcase(&1))
895 |> Enum.map(&String.downcase(&1))
899 |> Map.put("type", "Create")
900 |> Map.put("local_only", local_only)
901 |> Map.put("blocking_user", user)
902 |> Map.put("muting_user", user)
903 |> Map.put("user", user)
904 |> Map.put("tag", tags)
905 |> Map.put("tag_all", tag_all)
906 |> Map.put("tag_reject", tag_reject)
907 |> ActivityPub.fetch_public_activities()
911 |> add_link_headers(activities, %{"local" => local_only})
912 |> put_view(StatusView)
913 |> render("index.json", %{activities: activities, for: user, as: :activity})
916 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
917 with %User{} = user <- User.get_cached_by_id(id),
918 followers <- MastodonAPI.get_followers(user, params) do
921 for_user && user.id == for_user.id -> followers
922 user.info.hide_followers -> []
927 |> add_link_headers(followers)
928 |> put_view(AccountView)
929 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
933 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
934 with %User{} = user <- User.get_cached_by_id(id),
935 followers <- MastodonAPI.get_friends(user, params) do
938 for_user && user.id == for_user.id -> followers
939 user.info.hide_follows -> []
944 |> add_link_headers(followers)
945 |> put_view(AccountView)
946 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
950 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
951 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
953 |> put_view(AccountView)
954 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
958 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
959 with %User{} = follower <- User.get_cached_by_id(id),
960 {:ok, follower} <- CommonAPI.accept_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 reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
973 with %User{} = follower <- User.get_cached_by_id(id),
974 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: followed, target: follower})
981 |> put_status(:forbidden)
982 |> json(%{error: message})
986 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
987 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
988 {_, true} <- {:followed, follower.id != followed.id},
989 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
991 |> put_view(AccountView)
992 |> render("relationship.json", %{user: follower, target: followed})
999 |> put_status(:forbidden)
1000 |> json(%{error: message})
1004 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1005 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1006 {_, true} <- {:followed, follower.id != followed.id},
1007 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1009 |> put_view(AccountView)
1010 |> render("account.json", %{user: followed, for: follower})
1013 {:error, :not_found}
1015 {:error, message} ->
1017 |> put_status(:forbidden)
1018 |> json(%{error: message})
1022 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1023 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1024 {_, true} <- {:followed, follower.id != followed.id},
1025 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1027 |> put_view(AccountView)
1028 |> render("relationship.json", %{user: follower, target: followed})
1031 {:error, :not_found}
1038 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1040 if Map.has_key?(params, "notifications"),
1041 do: params["notifications"] in [true, "True", "true", "1"],
1044 with %User{} = muted <- User.get_cached_by_id(id),
1045 {:ok, muter} <- User.mute(muter, muted, notifications) 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 unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1058 with %User{} = muted <- User.get_cached_by_id(id),
1059 {:ok, muter} <- User.unmute(muter, muted) do
1061 |> put_view(AccountView)
1062 |> render("relationship.json", %{user: muter, target: muted})
1064 {:error, message} ->
1066 |> put_status(:forbidden)
1067 |> json(%{error: message})
1071 def mutes(%{assigns: %{user: user}} = conn, _) do
1072 with muted_accounts <- User.muted_users(user) do
1073 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1078 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1079 with %User{} = blocked <- User.get_cached_by_id(id),
1080 {:ok, blocker} <- User.block(blocker, blocked),
1081 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1083 |> put_view(AccountView)
1084 |> render("relationship.json", %{user: blocker, target: blocked})
1086 {:error, message} ->
1088 |> put_status(:forbidden)
1089 |> json(%{error: message})
1093 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1094 with %User{} = blocked <- User.get_cached_by_id(id),
1095 {:ok, blocker} <- User.unblock(blocker, blocked),
1096 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1098 |> put_view(AccountView)
1099 |> render("relationship.json", %{user: blocker, target: blocked})
1101 {:error, message} ->
1103 |> put_status(:forbidden)
1104 |> json(%{error: message})
1108 def blocks(%{assigns: %{user: user}} = conn, _) do
1109 with blocked_accounts <- User.blocked_users(user) do
1110 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1115 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1116 json(conn, info.domain_blocks || [])
1119 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1120 User.block_domain(blocker, domain)
1124 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1125 User.unblock_domain(blocker, domain)
1129 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1130 with %User{} = subscription_target <- User.get_cached_by_id(id),
1131 {:ok, subscription_target} = User.subscribe(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 unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1144 with %User{} = subscription_target <- User.get_cached_by_id(id),
1145 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1147 |> put_view(AccountView)
1148 |> render("relationship.json", %{user: user, target: subscription_target})
1150 {:error, message} ->
1152 |> put_status(:forbidden)
1153 |> json(%{error: message})
1157 def favourites(%{assigns: %{user: user}} = conn, params) do
1160 |> Map.put("type", "Create")
1161 |> Map.put("favorited_by", user.ap_id)
1162 |> Map.put("blocking_user", user)
1165 ActivityPub.fetch_activities([], params)
1169 |> add_link_headers(activities)
1170 |> put_view(StatusView)
1171 |> render("index.json", %{activities: activities, for: user, as: :activity})
1174 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1175 with %User{} = user <- User.get_by_id(id),
1176 false <- user.info.hide_favorites do
1179 |> Map.put("type", "Create")
1180 |> Map.put("favorited_by", user.ap_id)
1181 |> Map.put("blocking_user", for_user)
1185 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1187 [Pleroma.Constants.as_public()]
1192 |> ActivityPub.fetch_activities(params)
1196 |> add_link_headers(activities)
1197 |> put_view(StatusView)
1198 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1200 nil -> {:error, :not_found}
1201 true -> render_error(conn, :forbidden, "Can't get favorites")
1205 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1206 user = User.get_cached_by_id(user.id)
1209 Bookmark.for_user_query(user.id)
1210 |> Pagination.fetch_paginated(params)
1214 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1217 |> add_link_headers(bookmarks)
1218 |> put_view(StatusView)
1219 |> render("index.json", %{activities: activities, for: user, as: :activity})
1222 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1223 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1224 res = ListView.render("lists.json", lists: lists)
1228 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1229 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1232 |> Map.put("type", "Create")
1233 |> Map.put("blocking_user", user)
1234 |> Map.put("user", user)
1235 |> Map.put("muting_user", user)
1237 # we must filter the following list for the user to avoid leaking statuses the user
1238 # does not actually have permission to see (for more info, peruse security issue #270).
1241 |> Enum.filter(fn x -> x in user.following end)
1242 |> ActivityPub.fetch_activities_bounded(following, params)
1246 |> put_view(StatusView)
1247 |> render("index.json", %{activities: activities, for: user, as: :activity})
1249 _e -> render_error(conn, :forbidden, "Error.")
1253 def index(%{assigns: %{user: user}} = conn, _params) do
1254 token = get_session(conn, :oauth_token)
1257 mastodon_emoji = mastodonized_emoji()
1259 limit = Config.get([:instance, :limit])
1262 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1267 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1268 access_token: token,
1270 domain: Pleroma.Web.Endpoint.host(),
1273 unfollow_modal: false,
1276 auto_play_gif: false,
1277 display_sensitive_media: false,
1278 reduce_motion: false,
1279 max_toot_chars: limit,
1280 mascot: User.get_mascot(user)["url"]
1282 poll_limits: Config.get([:instance, :poll_limits]),
1284 delete_others_notice: present?(user.info.is_moderator),
1285 admin: present?(user.info.is_admin)
1289 default_privacy: user.info.default_scope,
1290 default_sensitive: false,
1291 allow_content_types: Config.get([:instance, :allowed_post_formats])
1293 media_attachments: %{
1294 accept_content_types: [
1310 user.info.settings ||
1340 push_subscription: nil,
1342 custom_emojis: mastodon_emoji,
1348 |> put_layout(false)
1349 |> put_view(MastodonView)
1350 |> render("index.html", %{initial_state: initial_state})
1353 |> put_session(:return_to, conn.request_path)
1354 |> redirect(to: "/web/login")
1358 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1359 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1361 with changeset <- Changeset.change(user),
1362 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1363 {:ok, _user} <- User.update_and_set_cache(changeset) do
1368 |> put_status(:internal_server_error)
1369 |> json(%{error: inspect(e)})
1373 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1374 redirect(conn, to: local_mastodon_root_path(conn))
1377 @doc "Local Mastodon FE login init action"
1378 def login(conn, %{"code" => auth_token}) do
1379 with {:ok, app} <- get_or_make_app(),
1380 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1381 {:ok, token} <- Token.exchange_token(app, auth) do
1383 |> put_session(:oauth_token, token.token)
1384 |> redirect(to: local_mastodon_root_path(conn))
1388 @doc "Local Mastodon FE callback action"
1389 def login(conn, _) do
1390 with {:ok, app} <- get_or_make_app() do
1395 response_type: "code",
1396 client_id: app.client_id,
1398 scope: Enum.join(app.scopes, " ")
1401 redirect(conn, to: path)
1405 defp local_mastodon_root_path(conn) do
1406 case get_session(conn, :return_to) do
1408 mastodon_api_path(conn, :index, ["getting-started"])
1411 delete_session(conn, :return_to)
1416 defp get_or_make_app do
1417 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1418 scopes = ["read", "write", "follow", "push"]
1420 with %App{} = app <- Repo.get_by(App, find_attrs) do
1422 if app.scopes == scopes do
1426 |> Changeset.change(%{scopes: scopes})
1434 App.register_changeset(
1436 Map.put(find_attrs, :scopes, scopes)
1443 def logout(conn, _) do
1446 |> redirect(to: "/")
1449 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1450 Logger.debug("Unimplemented, returning unmodified relationship")
1452 with %User{} = target <- User.get_cached_by_id(id) do
1454 |> put_view(AccountView)
1455 |> render("relationship.json", %{user: user, target: target})
1459 def empty_array(conn, _) do
1460 Logger.debug("Unimplemented, returning an empty array")
1464 def empty_object(conn, _) do
1465 Logger.debug("Unimplemented, returning an empty object")
1469 def get_filters(%{assigns: %{user: user}} = conn, _) do
1470 filters = Filter.get_filters(user)
1471 res = FilterView.render("filters.json", filters: filters)
1476 %{assigns: %{user: user}} = conn,
1477 %{"phrase" => phrase, "context" => context} = params
1483 hide: Map.get(params, "irreversible", false),
1484 whole_word: Map.get(params, "boolean", true)
1488 {:ok, response} = Filter.create(query)
1489 res = FilterView.render("filter.json", filter: response)
1493 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1494 filter = Filter.get(filter_id, user)
1495 res = FilterView.render("filter.json", filter: filter)
1500 %{assigns: %{user: user}} = conn,
1501 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1505 filter_id: filter_id,
1508 hide: Map.get(params, "irreversible", nil),
1509 whole_word: Map.get(params, "boolean", true)
1513 {:ok, response} = Filter.update(query)
1514 res = FilterView.render("filter.json", filter: response)
1518 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1521 filter_id: filter_id
1524 {:ok, _} = Filter.delete(query)
1528 def suggestions(%{assigns: %{user: user}} = conn, _) do
1529 suggestions = Config.get(:suggestions)
1531 if Keyword.get(suggestions, :enabled, false) do
1532 api = Keyword.get(suggestions, :third_party_engine, "")
1533 timeout = Keyword.get(suggestions, :timeout, 5000)
1534 limit = Keyword.get(suggestions, :limit, 23)
1536 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1538 user = user.nickname
1542 |> String.replace("{{host}}", host)
1543 |> String.replace("{{user}}", user)
1545 with {:ok, %{status: 200, body: body}} <-
1546 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1547 {:ok, data} <- Jason.decode(body) do
1550 |> Enum.slice(0, limit)
1553 |> Map.put("id", fetch_suggestion_id(x))
1554 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1555 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1561 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1568 defp fetch_suggestion_id(attrs) do
1569 case User.get_or_fetch(attrs["acct"]) do
1570 {:ok, %User{id: id}} -> id
1575 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1576 with %Activity{} = activity <- Activity.get_by_id(status_id),
1577 true <- Visibility.visible_for_user?(activity, user) do
1581 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1591 def reports(%{assigns: %{user: user}} = conn, params) do
1592 case CommonAPI.report(user, params) do
1595 |> put_view(ReportView)
1596 |> try_render("report.json", %{activity: activity})
1600 |> put_status(:bad_request)
1601 |> json(%{error: err})
1605 def account_register(
1606 %{assigns: %{app: app}} = conn,
1607 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1615 "captcha_answer_data",
1619 |> Map.put("nickname", nickname)
1620 |> Map.put("fullname", params["fullname"] || nickname)
1621 |> Map.put("bio", params["bio"] || "")
1622 |> Map.put("confirm", params["password"])
1624 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1625 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1627 token_type: "Bearer",
1628 access_token: token.token,
1630 created_at: Token.Utils.format_created_at(token)
1635 |> put_status(:bad_request)
1640 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1641 render_error(conn, :bad_request, "Missing parameters")
1644 def account_register(conn, _) do
1645 render_error(conn, :forbidden, "Invalid credentials")
1648 def conversations(%{assigns: %{user: user}} = conn, params) do
1649 participations = Participation.for_user_with_last_activity_id(user, params)
1652 Enum.map(participations, fn participation ->
1653 ConversationView.render("participation.json", %{participation: participation, for: user})
1657 |> add_link_headers(participations)
1658 |> json(conversations)
1661 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1662 with %Participation{} = participation <-
1663 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1664 {:ok, participation} <- Participation.mark_as_read(participation) do
1665 participation_view =
1666 ConversationView.render("participation.json", %{participation: participation, for: user})
1669 |> json(participation_view)
1673 def password_reset(conn, params) do
1674 nickname_or_email = params["email"] || params["nickname"]
1676 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1678 |> put_status(:no_content)
1681 {:error, "unknown user"} ->
1682 send_resp(conn, :not_found, "")
1685 send_resp(conn, :bad_request, "")
1689 def account_confirmation_resend(conn, params) do
1690 nickname_or_email = params["email"] || params["nickname"]
1692 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1693 {:ok, _} <- User.try_send_confirmation_email(user) do
1695 |> json_response(:no_content, "")
1699 def try_render(conn, target, params)
1700 when is_binary(target) do
1701 case render(conn, target, params) do
1702 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1707 def try_render(conn, _, _) do
1708 render_error(conn, :not_implemented, "Can't display this activity")
1711 defp present?(nil), do: false
1712 defp present?(false), do: false
1713 defp present?(_), do: true