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))
150 :hide_followers_count,
156 :skip_thread_containment
158 |> Enum.reduce(%{}, fn key, acc ->
159 add_if_present(acc, params, to_string(key), key, fn value ->
160 {:ok, ControllerHelper.truthy_param?(value)}
163 |> add_if_present(params, "default_scope", :default_scope)
164 |> add_if_present(params, "fields", :fields, fn fields ->
165 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
169 |> add_if_present(params, "fields", :raw_fields)
170 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
171 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
173 |> add_if_present(params, "header", :banner, fn value ->
174 with %Plug.Upload{} <- value,
175 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
181 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
182 with %Plug.Upload{} <- value,
183 {:ok, object} <- ActivityPub.upload(value, type: :background) do
189 |> Map.put(:emoji, user_info_emojis)
193 |> User.update_changeset(user_params)
194 |> User.change_info(&User.Info.profile_update(&1, info_params))
196 with {:ok, user} <- User.update_and_set_cache(changeset) do
197 if original_user != user, do: CommonAPI.update(user)
201 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
204 _e -> render_error(conn, :forbidden, "Invalid request")
208 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
209 change = Changeset.change(user, %{avatar: nil})
210 {:ok, user} = User.update_and_set_cache(change)
211 CommonAPI.update(user)
213 json(conn, %{url: nil})
216 def update_avatar(%{assigns: %{user: user}} = conn, params) do
217 {:ok, object} = ActivityPub.upload(params, type: :avatar)
218 change = Changeset.change(user, %{avatar: object.data})
219 {:ok, user} = User.update_and_set_cache(change)
220 CommonAPI.update(user)
221 %{"url" => [%{"href" => href} | _]} = object.data
223 json(conn, %{url: href})
226 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
227 new_info = %{"banner" => %{}}
229 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
230 CommonAPI.update(user)
231 json(conn, %{url: nil})
235 def update_banner(%{assigns: %{user: user}} = conn, params) do
236 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
237 new_info <- %{"banner" => object.data},
238 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
239 CommonAPI.update(user)
240 %{"url" => [%{"href" => href} | _]} = object.data
242 json(conn, %{url: href})
246 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
247 new_info = %{"background" => %{}}
249 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
250 json(conn, %{url: nil})
254 def update_background(%{assigns: %{user: user}} = conn, params) do
255 with {:ok, object} <- ActivityPub.upload(params, type: :background),
256 new_info <- %{"background" => object.data},
257 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
258 %{"url" => [%{"href" => href} | _]} = object.data
260 json(conn, %{url: href})
264 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
265 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
268 AccountView.render("account.json", %{
271 with_pleroma_settings: true,
272 with_chat_token: chat_token
278 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
279 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
282 |> render("short.json", %{app: app})
286 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
287 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
288 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
289 account = AccountView.render("account.json", %{user: user, for: for_user})
292 _e -> render_error(conn, :not_found, "Can't find user")
296 @mastodon_api_level "2.7.2"
298 def masto_instance(conn, _params) do
299 instance = Config.get(:instance)
303 title: Keyword.get(instance, :name),
304 description: Keyword.get(instance, :description),
305 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
306 email: Keyword.get(instance, :email),
308 streaming_api: Pleroma.Web.Endpoint.websocket_url()
310 stats: Stats.get_stats(),
311 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
313 registrations: Pleroma.Config.get([:instance, :registrations_open]),
314 # Extra (not present in Mastodon):
315 max_toot_chars: Keyword.get(instance, :limit),
316 poll_limits: Keyword.get(instance, :poll_limits)
322 def peers(conn, _params) do
323 json(conn, Stats.get_peers())
326 defp mastodonized_emoji do
327 Pleroma.Emoji.get_all()
328 |> Enum.map(fn {shortcode, relative_url, tags} ->
329 url = to_string(URI.merge(Web.base_url(), relative_url))
332 "shortcode" => shortcode,
334 "visible_in_picker" => true,
337 # Assuming that a comma is authorized in the category name
338 "category" => (tags -- ["Custom"]) |> Enum.join(",")
343 def custom_emojis(conn, _params) do
344 mastodon_emoji = mastodonized_emoji()
345 json(conn, mastodon_emoji)
348 def home_timeline(%{assigns: %{user: user}} = conn, params) do
351 |> Map.put("type", ["Create", "Announce"])
352 |> Map.put("blocking_user", user)
353 |> Map.put("muting_user", user)
354 |> Map.put("user", user)
357 [user.ap_id | user.following]
358 |> ActivityPub.fetch_activities(params)
362 |> add_link_headers(activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
367 def public_timeline(%{assigns: %{user: user}} = conn, params) do
368 local_only = params["local"] in [true, "True", "true", "1"]
372 |> Map.put("type", ["Create", "Announce"])
373 |> Map.put("local_only", local_only)
374 |> Map.put("blocking_user", user)
375 |> Map.put("muting_user", user)
376 |> ActivityPub.fetch_public_activities()
380 |> add_link_headers(activities, %{"local" => local_only})
381 |> put_view(StatusView)
382 |> render("index.json", %{activities: activities, for: user, as: :activity})
385 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
386 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
389 |> Map.put("tag", params["tagged"])
391 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
394 |> add_link_headers(activities)
395 |> put_view(StatusView)
396 |> render("index.json", %{
397 activities: activities,
404 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
407 |> Map.put("type", "Create")
408 |> Map.put("blocking_user", user)
409 |> Map.put("user", user)
410 |> Map.put(:visibility, "direct")
414 |> ActivityPub.fetch_activities_query(params)
415 |> Pagination.fetch_paginated(params)
418 |> add_link_headers(activities)
419 |> put_view(StatusView)
420 |> render("index.json", %{activities: activities, for: user, as: :activity})
423 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
429 |> Activity.all_by_ids_with_object()
430 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
433 |> put_view(StatusView)
434 |> render("index.json", activities: activities, for: user, as: :activity)
437 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
438 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
439 true <- Visibility.visible_for_user?(activity, user) do
441 |> put_view(StatusView)
442 |> try_render("status.json", %{activity: activity, for: user})
446 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
447 with %Activity{} = activity <- Activity.get_by_id(id),
449 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
450 "blocking_user" => user,
452 "exclude_id" => activity.id
454 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
460 activities: grouped_activities[true] || [],
464 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
469 activities: grouped_activities[false] || [],
473 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
480 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
481 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
482 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
483 true <- Visibility.visible_for_user?(activity, user) do
485 |> put_view(StatusView)
486 |> try_render("poll.json", %{object: object, for: user})
488 error when is_nil(error) or error == false ->
489 render_error(conn, :not_found, "Record not found")
493 defp get_cached_vote_or_vote(user, object, choices) do
494 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
497 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
498 case CommonAPI.vote(user, object, choices) do
499 {:error, _message} = res -> {:ignore, res}
500 res -> {:commit, res}
507 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
508 with %Object{} = object <- Object.get_by_id(id),
509 true <- object.data["type"] == "Question",
510 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
511 true <- Visibility.visible_for_user?(activity, user),
512 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
514 |> put_view(StatusView)
515 |> try_render("poll.json", %{object: object, for: user})
518 render_error(conn, :not_found, "Record not found")
521 render_error(conn, :not_found, "Record not found")
525 |> put_status(:unprocessable_entity)
526 |> json(%{error: message})
530 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
531 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
533 |> add_link_headers(scheduled_activities)
534 |> put_view(ScheduledActivityView)
535 |> render("index.json", %{scheduled_activities: scheduled_activities})
539 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
540 with %ScheduledActivity{} = scheduled_activity <-
541 ScheduledActivity.get(user, scheduled_activity_id) do
543 |> put_view(ScheduledActivityView)
544 |> render("show.json", %{scheduled_activity: scheduled_activity})
546 _ -> {:error, :not_found}
550 def update_scheduled_status(
551 %{assigns: %{user: user}} = conn,
552 %{"id" => scheduled_activity_id} = params
554 with %ScheduledActivity{} = scheduled_activity <-
555 ScheduledActivity.get(user, scheduled_activity_id),
556 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
558 |> put_view(ScheduledActivityView)
559 |> render("show.json", %{scheduled_activity: scheduled_activity})
561 nil -> {:error, :not_found}
566 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
567 with %ScheduledActivity{} = scheduled_activity <-
568 ScheduledActivity.get(user, scheduled_activity_id),
569 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
571 |> put_view(ScheduledActivityView)
572 |> render("show.json", %{scheduled_activity: scheduled_activity})
574 nil -> {:error, :not_found}
579 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
582 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
584 scheduled_at = params["scheduled_at"]
586 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
587 with {:ok, scheduled_activity} <-
588 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
590 |> put_view(ScheduledActivityView)
591 |> render("show.json", %{scheduled_activity: scheduled_activity})
594 params = Map.drop(params, ["scheduled_at"])
596 case CommonAPI.post(user, params) do
599 |> put_status(:unprocessable_entity)
600 |> json(%{error: message})
604 |> put_view(StatusView)
605 |> try_render("status.json", %{
609 with_direct_conversation_id: true
615 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
616 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
619 _e -> render_error(conn, :forbidden, "Can't delete this post")
623 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
624 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
625 %Activity{} = announce <- Activity.normalize(announce.data) do
627 |> put_view(StatusView)
628 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
632 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
633 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
634 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
636 |> put_view(StatusView)
637 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
641 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
642 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
643 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
645 |> put_view(StatusView)
646 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
650 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
651 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
652 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
654 |> put_view(StatusView)
655 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
659 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
660 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
662 |> put_view(StatusView)
663 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
667 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
668 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
670 |> put_view(StatusView)
671 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
675 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
676 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
677 %User{} = user <- User.get_cached_by_nickname(user.nickname),
678 true <- Visibility.visible_for_user?(activity, user),
679 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
681 |> put_view(StatusView)
682 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
686 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
687 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
688 %User{} = user <- User.get_cached_by_nickname(user.nickname),
689 true <- Visibility.visible_for_user?(activity, user),
690 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
692 |> put_view(StatusView)
693 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
697 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
698 activity = Activity.get_by_id(id)
700 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
702 |> put_view(StatusView)
703 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
707 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
708 activity = Activity.get_by_id(id)
710 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
712 |> put_view(StatusView)
713 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
717 def notifications(%{assigns: %{user: user}} = conn, params) do
718 notifications = MastodonAPI.get_notifications(user, params)
721 |> add_link_headers(notifications)
722 |> put_view(NotificationView)
723 |> render("index.json", %{notifications: notifications, for: user})
726 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
727 with {:ok, notification} <- Notification.get(user, id) do
729 |> put_view(NotificationView)
730 |> render("show.json", %{notification: notification, for: user})
734 |> put_status(:forbidden)
735 |> json(%{"error" => reason})
739 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
740 Notification.clear(user)
744 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
745 with {:ok, _notif} <- Notification.dismiss(user, id) do
750 |> put_status(:forbidden)
751 |> json(%{"error" => reason})
755 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
756 Notification.destroy_multiple(user, ids)
760 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
762 q = from(u in User, where: u.id in ^id)
763 targets = Repo.all(q)
766 |> put_view(AccountView)
767 |> render("relationships.json", %{user: user, targets: targets})
770 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
771 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
773 def update_media(%{assigns: %{user: user}} = conn, data) do
774 with %Object{} = object <- Repo.get(Object, data["id"]),
775 true <- Object.authorize_mutation(object, user),
776 true <- is_binary(data["description"]),
777 description <- data["description"] do
778 new_data = %{object.data | "name" => description}
782 |> Object.change(%{data: new_data})
785 attachment_data = Map.put(new_data, "id", object.id)
788 |> put_view(StatusView)
789 |> render("attachment.json", %{attachment: attachment_data})
793 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
794 with {:ok, object} <-
797 actor: User.ap_id(user),
798 description: Map.get(data, "description")
800 attachment_data = Map.put(object.data, "id", object.id)
803 |> put_view(StatusView)
804 |> render("attachment.json", %{attachment: attachment_data})
808 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
809 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
810 %{} = attachment_data <- Map.put(object.data, "id", object.id),
811 # Reject if not an image
812 %{type: "image"} = rendered <-
813 StatusView.render("attachment.json", %{attachment: attachment_data}) do
815 # Save to the user's info
816 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
820 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
824 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
825 mascot = User.get_mascot(user)
831 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
832 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
833 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
834 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
835 q = from(u in User, where: u.ap_id in ^likes)
839 |> Enum.filter(&(not User.blocks?(user, &1)))
842 |> put_view(AccountView)
843 |> render("accounts.json", %{for: user, users: users, as: :user})
845 {:visible, false} -> {:error, :not_found}
850 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
851 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
852 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
853 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
854 q = from(u in User, where: u.ap_id in ^announces)
858 |> Enum.filter(&(not User.blocks?(user, &1)))
861 |> put_view(AccountView)
862 |> render("accounts.json", %{for: user, users: users, as: :user})
864 {:visible, false} -> {:error, :not_found}
869 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
870 local_only = params["local"] in [true, "True", "true", "1"]
873 [params["tag"], params["any"]]
877 |> Enum.map(&String.downcase(&1))
882 |> Enum.map(&String.downcase(&1))
887 |> Enum.map(&String.downcase(&1))
891 |> Map.put("type", "Create")
892 |> Map.put("local_only", local_only)
893 |> Map.put("blocking_user", user)
894 |> Map.put("muting_user", user)
895 |> Map.put("user", user)
896 |> Map.put("tag", tags)
897 |> Map.put("tag_all", tag_all)
898 |> Map.put("tag_reject", tag_reject)
899 |> ActivityPub.fetch_public_activities()
903 |> add_link_headers(activities, %{"local" => local_only})
904 |> put_view(StatusView)
905 |> render("index.json", %{activities: activities, for: user, as: :activity})
908 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
909 with %User{} = user <- User.get_cached_by_id(id),
910 followers <- MastodonAPI.get_followers(user, params) do
913 for_user && user.id == for_user.id -> followers
914 user.info.hide_followers -> []
919 |> add_link_headers(followers)
920 |> put_view(AccountView)
921 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
925 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
926 with %User{} = user <- User.get_cached_by_id(id),
927 followers <- MastodonAPI.get_friends(user, params) do
930 for_user && user.id == for_user.id -> followers
931 user.info.hide_follows -> []
936 |> add_link_headers(followers)
937 |> put_view(AccountView)
938 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
942 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
943 follow_requests = User.get_follow_requests(followed)
946 |> put_view(AccountView)
947 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
950 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
951 with %User{} = follower <- User.get_cached_by_id(id),
952 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
954 |> put_view(AccountView)
955 |> render("relationship.json", %{user: followed, target: follower})
959 |> put_status(:forbidden)
960 |> json(%{error: message})
964 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
965 with %User{} = follower <- User.get_cached_by_id(id),
966 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
968 |> put_view(AccountView)
969 |> render("relationship.json", %{user: followed, target: follower})
973 |> put_status(:forbidden)
974 |> json(%{error: message})
978 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
979 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
980 {_, true} <- {:followed, follower.id != followed.id},
981 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
983 |> put_view(AccountView)
984 |> render("relationship.json", %{user: follower, target: followed})
991 |> put_status(:forbidden)
992 |> json(%{error: message})
996 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
997 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
998 {_, true} <- {:followed, follower.id != followed.id},
999 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1001 |> put_view(AccountView)
1002 |> render("account.json", %{user: followed, for: follower})
1005 {:error, :not_found}
1007 {:error, message} ->
1009 |> put_status(:forbidden)
1010 |> json(%{error: message})
1014 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1015 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1016 {_, true} <- {:followed, follower.id != followed.id},
1017 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1019 |> put_view(AccountView)
1020 |> render("relationship.json", %{user: follower, target: followed})
1023 {:error, :not_found}
1030 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1032 if Map.has_key?(params, "notifications"),
1033 do: params["notifications"] in [true, "True", "true", "1"],
1036 with %User{} = muted <- User.get_cached_by_id(id),
1037 {:ok, muter} <- User.mute(muter, muted, notifications) do
1039 |> put_view(AccountView)
1040 |> render("relationship.json", %{user: muter, target: muted})
1042 {:error, message} ->
1044 |> put_status(:forbidden)
1045 |> json(%{error: message})
1049 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1050 with %User{} = muted <- User.get_cached_by_id(id),
1051 {:ok, muter} <- User.unmute(muter, muted) do
1053 |> put_view(AccountView)
1054 |> render("relationship.json", %{user: muter, target: muted})
1056 {:error, message} ->
1058 |> put_status(:forbidden)
1059 |> json(%{error: message})
1063 def mutes(%{assigns: %{user: user}} = conn, _) do
1064 with muted_accounts <- User.muted_users(user) do
1065 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1070 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1071 with %User{} = blocked <- User.get_cached_by_id(id),
1072 {:ok, blocker} <- User.block(blocker, blocked),
1073 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1075 |> put_view(AccountView)
1076 |> render("relationship.json", %{user: blocker, target: blocked})
1078 {:error, message} ->
1080 |> put_status(:forbidden)
1081 |> json(%{error: message})
1085 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1086 with %User{} = blocked <- User.get_cached_by_id(id),
1087 {:ok, blocker} <- User.unblock(blocker, blocked),
1088 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1090 |> put_view(AccountView)
1091 |> render("relationship.json", %{user: blocker, target: blocked})
1093 {:error, message} ->
1095 |> put_status(:forbidden)
1096 |> json(%{error: message})
1100 def blocks(%{assigns: %{user: user}} = conn, _) do
1101 with blocked_accounts <- User.blocked_users(user) do
1102 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1107 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1108 json(conn, info.domain_blocks || [])
1111 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1112 User.block_domain(blocker, domain)
1116 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1117 User.unblock_domain(blocker, domain)
1121 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1122 with %User{} = subscription_target <- User.get_cached_by_id(id),
1123 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1125 |> put_view(AccountView)
1126 |> render("relationship.json", %{user: user, target: subscription_target})
1128 {:error, message} ->
1130 |> put_status(:forbidden)
1131 |> json(%{error: message})
1135 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1136 with %User{} = subscription_target <- User.get_cached_by_id(id),
1137 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1139 |> put_view(AccountView)
1140 |> render("relationship.json", %{user: user, target: subscription_target})
1142 {:error, message} ->
1144 |> put_status(:forbidden)
1145 |> json(%{error: message})
1149 def favourites(%{assigns: %{user: user}} = conn, params) do
1152 |> Map.put("type", "Create")
1153 |> Map.put("favorited_by", user.ap_id)
1154 |> Map.put("blocking_user", user)
1157 ActivityPub.fetch_activities([], params)
1161 |> add_link_headers(activities)
1162 |> put_view(StatusView)
1163 |> render("index.json", %{activities: activities, for: user, as: :activity})
1166 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1167 with %User{} = user <- User.get_by_id(id),
1168 false <- user.info.hide_favorites do
1171 |> Map.put("type", "Create")
1172 |> Map.put("favorited_by", user.ap_id)
1173 |> Map.put("blocking_user", for_user)
1177 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1179 [Pleroma.Constants.as_public()]
1184 |> ActivityPub.fetch_activities(params)
1188 |> add_link_headers(activities)
1189 |> put_view(StatusView)
1190 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1192 nil -> {:error, :not_found}
1193 true -> render_error(conn, :forbidden, "Can't get favorites")
1197 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1198 user = User.get_cached_by_id(user.id)
1201 Bookmark.for_user_query(user.id)
1202 |> Pagination.fetch_paginated(params)
1206 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1209 |> add_link_headers(bookmarks)
1210 |> put_view(StatusView)
1211 |> render("index.json", %{activities: activities, for: user, as: :activity})
1214 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1215 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1216 res = ListView.render("lists.json", lists: lists)
1220 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1221 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1224 |> Map.put("type", "Create")
1225 |> Map.put("blocking_user", user)
1226 |> Map.put("user", user)
1227 |> Map.put("muting_user", user)
1229 # we must filter the following list for the user to avoid leaking statuses the user
1230 # does not actually have permission to see (for more info, peruse security issue #270).
1233 |> Enum.filter(fn x -> x in user.following end)
1234 |> ActivityPub.fetch_activities_bounded(following, params)
1238 |> put_view(StatusView)
1239 |> render("index.json", %{activities: activities, for: user, as: :activity})
1241 _e -> render_error(conn, :forbidden, "Error.")
1245 def index(%{assigns: %{user: user}} = conn, _params) do
1246 token = get_session(conn, :oauth_token)
1249 mastodon_emoji = mastodonized_emoji()
1251 limit = Config.get([:instance, :limit])
1254 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1259 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1260 access_token: token,
1262 domain: Pleroma.Web.Endpoint.host(),
1265 unfollow_modal: false,
1268 auto_play_gif: false,
1269 display_sensitive_media: false,
1270 reduce_motion: false,
1271 max_toot_chars: limit,
1272 mascot: User.get_mascot(user)["url"]
1274 poll_limits: Config.get([:instance, :poll_limits]),
1276 delete_others_notice: present?(user.info.is_moderator),
1277 admin: present?(user.info.is_admin)
1281 default_privacy: user.info.default_scope,
1282 default_sensitive: false,
1283 allow_content_types: Config.get([:instance, :allowed_post_formats])
1285 media_attachments: %{
1286 accept_content_types: [
1302 user.info.settings ||
1332 push_subscription: nil,
1334 custom_emojis: mastodon_emoji,
1340 |> put_layout(false)
1341 |> put_view(MastodonView)
1342 |> render("index.html", %{initial_state: initial_state})
1345 |> put_session(:return_to, conn.request_path)
1346 |> redirect(to: "/web/login")
1350 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1351 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
1356 |> put_status(:internal_server_error)
1357 |> json(%{error: inspect(e)})
1361 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1362 redirect(conn, to: local_mastodon_root_path(conn))
1365 @doc "Local Mastodon FE login init action"
1366 def login(conn, %{"code" => auth_token}) do
1367 with {:ok, app} <- get_or_make_app(),
1368 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1369 {:ok, token} <- Token.exchange_token(app, auth) do
1371 |> put_session(:oauth_token, token.token)
1372 |> redirect(to: local_mastodon_root_path(conn))
1376 @doc "Local Mastodon FE callback action"
1377 def login(conn, _) do
1378 with {:ok, app} <- get_or_make_app() do
1383 response_type: "code",
1384 client_id: app.client_id,
1386 scope: Enum.join(app.scopes, " ")
1389 redirect(conn, to: path)
1393 defp local_mastodon_root_path(conn) do
1394 case get_session(conn, :return_to) do
1396 mastodon_api_path(conn, :index, ["getting-started"])
1399 delete_session(conn, :return_to)
1404 defp get_or_make_app do
1405 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1406 scopes = ["read", "write", "follow", "push"]
1408 with %App{} = app <- Repo.get_by(App, find_attrs) do
1410 if app.scopes == scopes do
1414 |> Changeset.change(%{scopes: scopes})
1422 App.register_changeset(
1424 Map.put(find_attrs, :scopes, scopes)
1431 def logout(conn, _) do
1434 |> redirect(to: "/")
1437 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1438 Logger.debug("Unimplemented, returning unmodified relationship")
1440 with %User{} = target <- User.get_cached_by_id(id) do
1442 |> put_view(AccountView)
1443 |> render("relationship.json", %{user: user, target: target})
1447 def empty_array(conn, _) do
1448 Logger.debug("Unimplemented, returning an empty array")
1452 def empty_object(conn, _) do
1453 Logger.debug("Unimplemented, returning an empty object")
1457 def get_filters(%{assigns: %{user: user}} = conn, _) do
1458 filters = Filter.get_filters(user)
1459 res = FilterView.render("filters.json", filters: filters)
1464 %{assigns: %{user: user}} = conn,
1465 %{"phrase" => phrase, "context" => context} = params
1471 hide: Map.get(params, "irreversible", false),
1472 whole_word: Map.get(params, "boolean", true)
1476 {:ok, response} = Filter.create(query)
1477 res = FilterView.render("filter.json", filter: response)
1481 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1482 filter = Filter.get(filter_id, user)
1483 res = FilterView.render("filter.json", filter: filter)
1488 %{assigns: %{user: user}} = conn,
1489 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1493 filter_id: filter_id,
1496 hide: Map.get(params, "irreversible", nil),
1497 whole_word: Map.get(params, "boolean", true)
1501 {:ok, response} = Filter.update(query)
1502 res = FilterView.render("filter.json", filter: response)
1506 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1509 filter_id: filter_id
1512 {:ok, _} = Filter.delete(query)
1516 def suggestions(%{assigns: %{user: user}} = conn, _) do
1517 suggestions = Config.get(:suggestions)
1519 if Keyword.get(suggestions, :enabled, false) do
1520 api = Keyword.get(suggestions, :third_party_engine, "")
1521 timeout = Keyword.get(suggestions, :timeout, 5000)
1522 limit = Keyword.get(suggestions, :limit, 23)
1524 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1526 user = user.nickname
1530 |> String.replace("{{host}}", host)
1531 |> String.replace("{{user}}", user)
1533 with {:ok, %{status: 200, body: body}} <-
1534 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1535 {:ok, data} <- Jason.decode(body) do
1538 |> Enum.slice(0, limit)
1541 |> Map.put("id", fetch_suggestion_id(x))
1542 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1543 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1549 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1556 defp fetch_suggestion_id(attrs) do
1557 case User.get_or_fetch(attrs["acct"]) do
1558 {:ok, %User{id: id}} -> id
1563 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1564 with %Activity{} = activity <- Activity.get_by_id(status_id),
1565 true <- Visibility.visible_for_user?(activity, user) do
1569 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1579 def reports(%{assigns: %{user: user}} = conn, params) do
1580 case CommonAPI.report(user, params) do
1583 |> put_view(ReportView)
1584 |> try_render("report.json", %{activity: activity})
1588 |> put_status(:bad_request)
1589 |> json(%{error: err})
1593 def account_register(
1594 %{assigns: %{app: app}} = conn,
1595 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1603 "captcha_answer_data",
1607 |> Map.put("nickname", nickname)
1608 |> Map.put("fullname", params["fullname"] || nickname)
1609 |> Map.put("bio", params["bio"] || "")
1610 |> Map.put("confirm", params["password"])
1612 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1613 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1615 token_type: "Bearer",
1616 access_token: token.token,
1618 created_at: Token.Utils.format_created_at(token)
1623 |> put_status(:bad_request)
1628 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1629 render_error(conn, :bad_request, "Missing parameters")
1632 def account_register(conn, _) do
1633 render_error(conn, :forbidden, "Invalid credentials")
1636 def conversations(%{assigns: %{user: user}} = conn, params) do
1637 participations = Participation.for_user_with_last_activity_id(user, params)
1640 Enum.map(participations, fn participation ->
1641 ConversationView.render("participation.json", %{participation: participation, for: user})
1645 |> add_link_headers(participations)
1646 |> json(conversations)
1649 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1650 with %Participation{} = participation <-
1651 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1652 {:ok, participation} <- Participation.mark_as_read(participation) do
1653 participation_view =
1654 ConversationView.render("participation.json", %{participation: participation, for: user})
1657 |> json(participation_view)
1661 def password_reset(conn, params) do
1662 nickname_or_email = params["email"] || params["nickname"]
1664 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1666 |> put_status(:no_content)
1669 {:error, "unknown user"} ->
1670 send_resp(conn, :not_found, "")
1673 send_resp(conn, :bad_request, "")
1677 def account_confirmation_resend(conn, params) do
1678 nickname_or_email = params["email"] || params["nickname"]
1680 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1681 {:ok, _} <- User.try_send_confirmation_email(user) do
1683 |> json_response(:no_content, "")
1687 def try_render(conn, target, params)
1688 when is_binary(target) do
1689 case render(conn, target, params) do
1690 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1695 def try_render(conn, _, _) do
1696 render_error(conn, :not_implemented, "Can't display this activity")
1699 defp present?(nil), do: false
1700 defp present?(false), do: false
1701 defp present?(_), do: true