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
19 alias Pleroma.Notification
21 alias Pleroma.Pagination
22 alias Pleroma.Plugs.RateLimiter
24 alias Pleroma.ScheduledActivity
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Visibility
30 alias Pleroma.Web.CommonAPI
31 alias Pleroma.Web.MastodonAPI.AccountView
32 alias Pleroma.Web.MastodonAPI.AppView
33 alias Pleroma.Web.MastodonAPI.ConversationView
34 alias Pleroma.Web.MastodonAPI.FilterView
35 alias Pleroma.Web.MastodonAPI.ListView
36 alias Pleroma.Web.MastodonAPI.MastodonAPI
37 alias Pleroma.Web.MastodonAPI.MastodonView
38 alias Pleroma.Web.MastodonAPI.NotificationView
39 alias Pleroma.Web.MastodonAPI.ReportView
40 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
41 alias Pleroma.Web.MastodonAPI.StatusView
42 alias Pleroma.Web.MediaProxy
43 alias Pleroma.Web.OAuth.App
44 alias Pleroma.Web.OAuth.Authorization
45 alias Pleroma.Web.OAuth.Scopes
46 alias Pleroma.Web.OAuth.Token
47 alias Pleroma.Web.TwitterAPI.TwitterAPI
49 alias Pleroma.Web.ControllerHelper
53 require Pleroma.Constants
55 @rate_limited_relations_actions ~w(follow unfollow)a
57 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
58 post_status delete_status)a
62 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
63 when action in ~w(reblog_status unreblog_status)a
68 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
69 when action in ~w(fav_status unfav_status)a
74 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
77 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
78 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
79 plug(RateLimiter, :app_account_creation when action == :account_register)
80 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
81 plug(RateLimiter, :password_reset when action == :password_reset)
82 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
84 @local_mastodon_name "Mastodon-Local"
86 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
88 def create_app(conn, params) do
89 scopes = Scopes.fetch_scopes(params, ["read"])
93 |> Map.drop(["scope", "scopes"])
94 |> Map.put("scopes", scopes)
96 with cs <- App.register_changeset(%App{}, app_attrs),
97 false <- cs.changes[:client_name] == @local_mastodon_name,
98 {:ok, app} <- Repo.insert(cs) do
101 |> render("show.json", %{app: app})
110 value_function \\ fn x -> {:ok, x} end
112 if Map.has_key?(params, params_field) do
113 case value_function.(params[params_field]) do
114 {:ok, new_value} -> Map.put(map, map_field, new_value)
122 def update_credentials(%{assigns: %{user: user}} = conn, params) do
127 |> add_if_present(params, "display_name", :name)
128 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
129 |> add_if_present(params, "avatar", :avatar, fn value ->
130 with %Plug.Upload{} <- value,
131 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
138 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
142 |> Map.get(:emoji, [])
143 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
150 :hide_followers_count,
156 :skip_thread_containment,
159 |> Enum.reduce(%{}, fn key, acc ->
160 add_if_present(acc, params, to_string(key), key, fn value ->
161 {:ok, ControllerHelper.truthy_param?(value)}
164 |> add_if_present(params, "default_scope", :default_scope)
165 |> add_if_present(params, "fields", :fields, fn fields ->
166 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
170 |> add_if_present(params, "fields", :raw_fields)
171 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
172 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
174 |> add_if_present(params, "header", :banner, fn value ->
175 with %Plug.Upload{} <- value,
176 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
182 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
183 with %Plug.Upload{} <- value,
184 {:ok, object} <- ActivityPub.upload(value, type: :background) do
190 |> Map.put(:emoji, user_info_emojis)
194 |> User.update_changeset(user_params)
195 |> User.change_info(&User.Info.profile_update(&1, info_params))
197 with {:ok, user} <- User.update_and_set_cache(changeset) do
198 if original_user != user, do: CommonAPI.update(user)
202 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
205 _e -> render_error(conn, :forbidden, "Invalid request")
209 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
210 change = Changeset.change(user, %{avatar: nil})
211 {:ok, user} = User.update_and_set_cache(change)
212 CommonAPI.update(user)
214 json(conn, %{url: nil})
217 def update_avatar(%{assigns: %{user: user}} = conn, params) do
218 {:ok, object} = ActivityPub.upload(params, type: :avatar)
219 change = Changeset.change(user, %{avatar: object.data})
220 {:ok, user} = User.update_and_set_cache(change)
221 CommonAPI.update(user)
222 %{"url" => [%{"href" => href} | _]} = object.data
224 json(conn, %{url: href})
227 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
228 new_info = %{"banner" => %{}}
230 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
231 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 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
240 CommonAPI.update(user)
241 %{"url" => [%{"href" => href} | _]} = object.data
243 json(conn, %{url: href})
247 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
248 new_info = %{"background" => %{}}
250 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
251 json(conn, %{url: nil})
255 def update_background(%{assigns: %{user: user}} = conn, params) do
256 with {:ok, object} <- ActivityPub.upload(params, type: :background),
257 new_info <- %{"background" => object.data},
258 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
259 %{"url" => [%{"href" => href} | _]} = object.data
261 json(conn, %{url: href})
265 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
266 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
269 AccountView.render("account.json", %{
272 with_pleroma_settings: true,
273 with_chat_token: chat_token
279 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
280 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
283 |> render("short.json", %{app: app})
287 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
288 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
289 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
290 account = AccountView.render("account.json", %{user: user, for: for_user})
293 _e -> render_error(conn, :not_found, "Can't find user")
297 @mastodon_api_level "2.7.2"
299 def masto_instance(conn, _params) do
300 instance = Config.get(:instance)
304 title: Keyword.get(instance, :name),
305 description: Keyword.get(instance, :description),
306 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
307 email: Keyword.get(instance, :email),
309 streaming_api: Pleroma.Web.Endpoint.websocket_url()
311 stats: Stats.get_stats(),
312 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
314 registrations: Pleroma.Config.get([:instance, :registrations_open]),
315 # Extra (not present in Mastodon):
316 max_toot_chars: Keyword.get(instance, :limit),
317 poll_limits: Keyword.get(instance, :poll_limits)
323 def peers(conn, _params) do
324 json(conn, Stats.get_peers())
327 defp mastodonized_emoji do
328 Pleroma.Emoji.get_all()
329 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
330 url = to_string(URI.merge(Web.base_url(), relative_url))
333 "shortcode" => shortcode,
335 "visible_in_picker" => true,
338 # Assuming that a comma is authorized in the category name
339 "category" => (tags -- ["Custom"]) |> Enum.join(",")
344 def custom_emojis(conn, _params) do
345 mastodon_emoji = mastodonized_emoji()
346 json(conn, mastodon_emoji)
349 def home_timeline(%{assigns: %{user: user}} = conn, params) do
352 |> Map.put("type", ["Create", "Announce"])
353 |> Map.put("blocking_user", user)
354 |> Map.put("muting_user", user)
355 |> Map.put("user", user)
358 [user.ap_id | user.following]
359 |> ActivityPub.fetch_activities(params)
363 |> add_link_headers(activities)
364 |> put_view(StatusView)
365 |> render("index.json", %{activities: activities, for: user, as: :activity})
368 def public_timeline(%{assigns: %{user: user}} = conn, params) do
369 local_only = params["local"] in [true, "True", "true", "1"]
373 |> Map.put("type", ["Create", "Announce"])
374 |> Map.put("local_only", local_only)
375 |> Map.put("blocking_user", user)
376 |> Map.put("muting_user", user)
377 |> ActivityPub.fetch_public_activities()
381 |> add_link_headers(activities, %{"local" => local_only})
382 |> put_view(StatusView)
383 |> render("index.json", %{activities: activities, for: user, as: :activity})
386 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
387 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
390 |> Map.put("tag", params["tagged"])
392 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
395 |> add_link_headers(activities)
396 |> put_view(StatusView)
397 |> render("index.json", %{
398 activities: activities,
405 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
408 |> Map.put("type", "Create")
409 |> Map.put("blocking_user", user)
410 |> Map.put("user", user)
411 |> Map.put(:visibility, "direct")
415 |> ActivityPub.fetch_activities_query(params)
416 |> Pagination.fetch_paginated(params)
419 |> add_link_headers(activities)
420 |> put_view(StatusView)
421 |> render("index.json", %{activities: activities, for: user, as: :activity})
424 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
430 |> Activity.all_by_ids_with_object()
431 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
434 |> put_view(StatusView)
435 |> render("index.json", activities: activities, for: user, as: :activity)
438 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
439 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
440 true <- Visibility.visible_for_user?(activity, user) do
442 |> put_view(StatusView)
443 |> try_render("status.json", %{activity: activity, for: user})
447 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
448 with %Activity{} = activity <- Activity.get_by_id(id),
450 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
451 "blocking_user" => user,
453 "exclude_id" => activity.id
455 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
461 activities: grouped_activities[true] || [],
465 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
470 activities: grouped_activities[false] || [],
474 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
481 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
482 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
483 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
484 true <- Visibility.visible_for_user?(activity, user) do
486 |> put_view(StatusView)
487 |> try_render("poll.json", %{object: object, for: user})
489 error when is_nil(error) or error == false ->
490 render_error(conn, :not_found, "Record not found")
494 defp get_cached_vote_or_vote(user, object, choices) do
495 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
498 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
499 case CommonAPI.vote(user, object, choices) do
500 {:error, _message} = res -> {:ignore, res}
501 res -> {:commit, res}
508 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
509 with %Object{} = object <- Object.get_by_id(id),
510 true <- object.data["type"] == "Question",
511 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
512 true <- Visibility.visible_for_user?(activity, user),
513 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
515 |> put_view(StatusView)
516 |> try_render("poll.json", %{object: object, for: user})
519 render_error(conn, :not_found, "Record not found")
522 render_error(conn, :not_found, "Record not found")
526 |> put_status(:unprocessable_entity)
527 |> json(%{error: message})
531 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
532 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
534 |> add_link_headers(scheduled_activities)
535 |> put_view(ScheduledActivityView)
536 |> render("index.json", %{scheduled_activities: scheduled_activities})
540 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
541 with %ScheduledActivity{} = scheduled_activity <-
542 ScheduledActivity.get(user, scheduled_activity_id) do
544 |> put_view(ScheduledActivityView)
545 |> render("show.json", %{scheduled_activity: scheduled_activity})
547 _ -> {:error, :not_found}
551 def update_scheduled_status(
552 %{assigns: %{user: user}} = conn,
553 %{"id" => scheduled_activity_id} = params
555 with %ScheduledActivity{} = scheduled_activity <-
556 ScheduledActivity.get(user, scheduled_activity_id),
557 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
559 |> put_view(ScheduledActivityView)
560 |> render("show.json", %{scheduled_activity: scheduled_activity})
562 nil -> {:error, :not_found}
567 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
568 with %ScheduledActivity{} = scheduled_activity <-
569 ScheduledActivity.get(user, scheduled_activity_id),
570 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
572 |> put_view(ScheduledActivityView)
573 |> render("show.json", %{scheduled_activity: scheduled_activity})
575 nil -> {:error, :not_found}
580 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
583 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
585 scheduled_at = params["scheduled_at"]
587 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
588 with {:ok, scheduled_activity} <-
589 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
591 |> put_view(ScheduledActivityView)
592 |> render("show.json", %{scheduled_activity: scheduled_activity})
595 params = Map.drop(params, ["scheduled_at"])
597 case CommonAPI.post(user, params) do
600 |> put_status(:unprocessable_entity)
601 |> json(%{error: message})
605 |> put_view(StatusView)
606 |> try_render("status.json", %{
610 with_direct_conversation_id: true
616 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
617 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
620 _e -> render_error(conn, :forbidden, "Can't delete this post")
624 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
625 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
626 %Activity{} = announce <- Activity.normalize(announce.data) do
628 |> put_view(StatusView)
629 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
633 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
634 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
635 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
637 |> put_view(StatusView)
638 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
642 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
643 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
644 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
646 |> put_view(StatusView)
647 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
651 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
652 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
653 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
655 |> put_view(StatusView)
656 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
660 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
661 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
663 |> put_view(StatusView)
664 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
668 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
669 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
671 |> put_view(StatusView)
672 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
676 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
677 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
678 %User{} = user <- User.get_cached_by_nickname(user.nickname),
679 true <- Visibility.visible_for_user?(activity, user),
680 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
682 |> put_view(StatusView)
683 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
687 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
688 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
689 %User{} = user <- User.get_cached_by_nickname(user.nickname),
690 true <- Visibility.visible_for_user?(activity, user),
691 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
693 |> put_view(StatusView)
694 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
698 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
699 activity = Activity.get_by_id(id)
701 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
703 |> put_view(StatusView)
704 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
708 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
709 activity = Activity.get_by_id(id)
711 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
713 |> put_view(StatusView)
714 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
718 def notifications(%{assigns: %{user: user}} = conn, params) do
719 notifications = MastodonAPI.get_notifications(user, params)
722 |> add_link_headers(notifications)
723 |> put_view(NotificationView)
724 |> render("index.json", %{notifications: notifications, for: user})
727 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
728 with {:ok, notification} <- Notification.get(user, id) do
730 |> put_view(NotificationView)
731 |> render("show.json", %{notification: notification, for: user})
735 |> put_status(:forbidden)
736 |> json(%{"error" => reason})
740 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
741 Notification.clear(user)
745 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
746 with {:ok, _notif} <- Notification.dismiss(user, id) do
751 |> put_status(:forbidden)
752 |> json(%{"error" => reason})
756 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
757 Notification.destroy_multiple(user, ids)
761 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
763 q = from(u in User, where: u.id in ^id)
764 targets = Repo.all(q)
767 |> put_view(AccountView)
768 |> render("relationships.json", %{user: user, targets: targets})
771 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
772 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
774 def update_media(%{assigns: %{user: user}} = conn, data) do
775 with %Object{} = object <- Repo.get(Object, data["id"]),
776 true <- Object.authorize_mutation(object, user),
777 true <- is_binary(data["description"]),
778 description <- data["description"] do
779 new_data = %{object.data | "name" => description}
783 |> Object.change(%{data: new_data})
786 attachment_data = Map.put(new_data, "id", object.id)
789 |> put_view(StatusView)
790 |> render("attachment.json", %{attachment: attachment_data})
794 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
795 with {:ok, object} <-
798 actor: User.ap_id(user),
799 description: Map.get(data, "description")
801 attachment_data = Map.put(object.data, "id", object.id)
804 |> put_view(StatusView)
805 |> render("attachment.json", %{attachment: attachment_data})
809 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
810 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
811 %{} = attachment_data <- Map.put(object.data, "id", object.id),
812 # Reject if not an image
813 %{type: "image"} = rendered <-
814 StatusView.render("attachment.json", %{attachment: attachment_data}) do
816 # Save to the user's info
817 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
821 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
825 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
826 mascot = User.get_mascot(user)
832 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
833 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
834 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
835 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
836 q = from(u in User, where: u.ap_id in ^likes)
840 |> Enum.filter(&(not User.blocks?(user, &1)))
843 |> put_view(AccountView)
844 |> render("accounts.json", %{for: user, users: users, as: :user})
846 {:visible, false} -> {:error, :not_found}
851 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
852 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
853 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
854 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
855 q = from(u in User, where: u.ap_id in ^announces)
859 |> Enum.filter(&(not User.blocks?(user, &1)))
862 |> put_view(AccountView)
863 |> render("accounts.json", %{for: user, users: users, as: :user})
865 {:visible, false} -> {:error, :not_found}
870 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
871 local_only = params["local"] in [true, "True", "true", "1"]
874 [params["tag"], params["any"]]
878 |> Enum.map(&String.downcase(&1))
883 |> Enum.map(&String.downcase(&1))
888 |> Enum.map(&String.downcase(&1))
892 |> Map.put("type", "Create")
893 |> Map.put("local_only", local_only)
894 |> Map.put("blocking_user", user)
895 |> Map.put("muting_user", user)
896 |> Map.put("user", user)
897 |> Map.put("tag", tags)
898 |> Map.put("tag_all", tag_all)
899 |> Map.put("tag_reject", tag_reject)
900 |> ActivityPub.fetch_public_activities()
904 |> add_link_headers(activities, %{"local" => local_only})
905 |> put_view(StatusView)
906 |> render("index.json", %{activities: activities, for: user, as: :activity})
909 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
910 with %User{} = user <- User.get_cached_by_id(id),
911 followers <- MastodonAPI.get_followers(user, params) do
914 for_user && user.id == for_user.id -> followers
915 user.info.hide_followers -> []
920 |> add_link_headers(followers)
921 |> put_view(AccountView)
922 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
926 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
927 with %User{} = user <- User.get_cached_by_id(id),
928 followers <- MastodonAPI.get_friends(user, params) do
931 for_user && user.id == for_user.id -> followers
932 user.info.hide_follows -> []
937 |> add_link_headers(followers)
938 |> put_view(AccountView)
939 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
943 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
944 follow_requests = User.get_follow_requests(followed)
947 |> put_view(AccountView)
948 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
951 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
952 with %User{} = follower <- User.get_cached_by_id(id),
953 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
955 |> put_view(AccountView)
956 |> render("relationship.json", %{user: followed, target: follower})
960 |> put_status(:forbidden)
961 |> json(%{error: message})
965 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
966 with %User{} = follower <- User.get_cached_by_id(id),
967 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
969 |> put_view(AccountView)
970 |> render("relationship.json", %{user: followed, target: follower})
974 |> put_status(:forbidden)
975 |> json(%{error: message})
979 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
980 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
981 {_, true} <- {:followed, follower.id != followed.id},
982 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
984 |> put_view(AccountView)
985 |> render("relationship.json", %{user: follower, target: followed})
992 |> put_status(:forbidden)
993 |> json(%{error: message})
997 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
998 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
999 {_, true} <- {:followed, follower.id != followed.id},
1000 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1002 |> put_view(AccountView)
1003 |> render("account.json", %{user: followed, for: follower})
1006 {:error, :not_found}
1008 {:error, message} ->
1010 |> put_status(:forbidden)
1011 |> json(%{error: message})
1015 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1016 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1017 {_, true} <- {:followed, follower.id != followed.id},
1018 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1020 |> put_view(AccountView)
1021 |> render("relationship.json", %{user: follower, target: followed})
1024 {:error, :not_found}
1031 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1033 if Map.has_key?(params, "notifications"),
1034 do: params["notifications"] in [true, "True", "true", "1"],
1037 with %User{} = muted <- User.get_cached_by_id(id),
1038 {:ok, muter} <- User.mute(muter, muted, notifications) do
1040 |> put_view(AccountView)
1041 |> render("relationship.json", %{user: muter, target: muted})
1043 {:error, message} ->
1045 |> put_status(:forbidden)
1046 |> json(%{error: message})
1050 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1051 with %User{} = muted <- User.get_cached_by_id(id),
1052 {:ok, muter} <- User.unmute(muter, muted) do
1054 |> put_view(AccountView)
1055 |> render("relationship.json", %{user: muter, target: muted})
1057 {:error, message} ->
1059 |> put_status(:forbidden)
1060 |> json(%{error: message})
1064 def mutes(%{assigns: %{user: user}} = conn, _) do
1065 with muted_accounts <- User.muted_users(user) do
1066 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1071 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1072 with %User{} = blocked <- User.get_cached_by_id(id),
1073 {:ok, blocker} <- User.block(blocker, blocked),
1074 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1076 |> put_view(AccountView)
1077 |> render("relationship.json", %{user: blocker, target: blocked})
1079 {:error, message} ->
1081 |> put_status(:forbidden)
1082 |> json(%{error: message})
1086 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1087 with %User{} = blocked <- User.get_cached_by_id(id),
1088 {:ok, blocker} <- User.unblock(blocker, blocked),
1089 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1091 |> put_view(AccountView)
1092 |> render("relationship.json", %{user: blocker, target: blocked})
1094 {:error, message} ->
1096 |> put_status(:forbidden)
1097 |> json(%{error: message})
1101 def blocks(%{assigns: %{user: user}} = conn, _) do
1102 with blocked_accounts <- User.blocked_users(user) do
1103 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1108 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1109 json(conn, info.domain_blocks || [])
1112 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1113 User.block_domain(blocker, domain)
1117 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1118 User.unblock_domain(blocker, domain)
1122 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1123 with %User{} = subscription_target <- User.get_cached_by_id(id),
1124 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1126 |> put_view(AccountView)
1127 |> render("relationship.json", %{user: user, target: subscription_target})
1129 {:error, message} ->
1131 |> put_status(:forbidden)
1132 |> json(%{error: message})
1136 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1137 with %User{} = subscription_target <- User.get_cached_by_id(id),
1138 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1140 |> put_view(AccountView)
1141 |> render("relationship.json", %{user: user, target: subscription_target})
1143 {:error, message} ->
1145 |> put_status(:forbidden)
1146 |> json(%{error: message})
1150 def favourites(%{assigns: %{user: user}} = conn, params) do
1153 |> Map.put("type", "Create")
1154 |> Map.put("favorited_by", user.ap_id)
1155 |> Map.put("blocking_user", user)
1158 ActivityPub.fetch_activities([], params)
1162 |> add_link_headers(activities)
1163 |> put_view(StatusView)
1164 |> render("index.json", %{activities: activities, for: user, as: :activity})
1167 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1168 with %User{} = user <- User.get_by_id(id),
1169 false <- user.info.hide_favorites do
1172 |> Map.put("type", "Create")
1173 |> Map.put("favorited_by", user.ap_id)
1174 |> Map.put("blocking_user", for_user)
1178 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1180 [Pleroma.Constants.as_public()]
1185 |> ActivityPub.fetch_activities(params)
1189 |> add_link_headers(activities)
1190 |> put_view(StatusView)
1191 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1193 nil -> {:error, :not_found}
1194 true -> render_error(conn, :forbidden, "Can't get favorites")
1198 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1199 user = User.get_cached_by_id(user.id)
1202 Bookmark.for_user_query(user.id)
1203 |> Pagination.fetch_paginated(params)
1207 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1210 |> add_link_headers(bookmarks)
1211 |> put_view(StatusView)
1212 |> render("index.json", %{activities: activities, for: user, as: :activity})
1215 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1216 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1217 res = ListView.render("lists.json", lists: lists)
1221 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1222 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1225 |> Map.put("type", "Create")
1226 |> Map.put("blocking_user", user)
1227 |> Map.put("user", user)
1228 |> Map.put("muting_user", user)
1230 # we must filter the following list for the user to avoid leaking statuses the user
1231 # does not actually have permission to see (for more info, peruse security issue #270).
1234 |> Enum.filter(fn x -> x in user.following end)
1235 |> ActivityPub.fetch_activities_bounded(following, params)
1239 |> put_view(StatusView)
1240 |> render("index.json", %{activities: activities, for: user, as: :activity})
1242 _e -> render_error(conn, :forbidden, "Error.")
1246 def index(%{assigns: %{user: user}} = conn, _params) do
1247 token = get_session(conn, :oauth_token)
1250 mastodon_emoji = mastodonized_emoji()
1252 limit = Config.get([:instance, :limit])
1255 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1260 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1261 access_token: token,
1263 domain: Pleroma.Web.Endpoint.host(),
1266 unfollow_modal: false,
1269 auto_play_gif: false,
1270 display_sensitive_media: false,
1271 reduce_motion: false,
1272 max_toot_chars: limit,
1273 mascot: User.get_mascot(user)["url"]
1275 poll_limits: Config.get([:instance, :poll_limits]),
1277 delete_others_notice: present?(user.info.is_moderator),
1278 admin: present?(user.info.is_admin)
1282 default_privacy: user.info.default_scope,
1283 default_sensitive: false,
1284 allow_content_types: Config.get([:instance, :allowed_post_formats])
1286 media_attachments: %{
1287 accept_content_types: [
1303 user.info.settings ||
1333 push_subscription: nil,
1335 custom_emojis: mastodon_emoji,
1341 |> put_layout(false)
1342 |> put_view(MastodonView)
1343 |> render("index.html", %{initial_state: initial_state})
1346 |> put_session(:return_to, conn.request_path)
1347 |> redirect(to: "/web/login")
1351 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1352 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
1357 |> put_status(:internal_server_error)
1358 |> json(%{error: inspect(e)})
1362 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1363 redirect(conn, to: local_mastodon_root_path(conn))
1366 @doc "Local Mastodon FE login init action"
1367 def login(conn, %{"code" => auth_token}) do
1368 with {:ok, app} <- get_or_make_app(),
1369 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1370 {:ok, token} <- Token.exchange_token(app, auth) do
1372 |> put_session(:oauth_token, token.token)
1373 |> redirect(to: local_mastodon_root_path(conn))
1377 @doc "Local Mastodon FE callback action"
1378 def login(conn, _) do
1379 with {:ok, app} <- get_or_make_app() do
1384 response_type: "code",
1385 client_id: app.client_id,
1387 scope: Enum.join(app.scopes, " ")
1390 redirect(conn, to: path)
1394 defp local_mastodon_root_path(conn) do
1395 case get_session(conn, :return_to) do
1397 mastodon_api_path(conn, :index, ["getting-started"])
1400 delete_session(conn, :return_to)
1405 defp get_or_make_app do
1406 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1407 scopes = ["read", "write", "follow", "push"]
1409 with %App{} = app <- Repo.get_by(App, find_attrs) do
1411 if app.scopes == scopes do
1415 |> Changeset.change(%{scopes: scopes})
1423 App.register_changeset(
1425 Map.put(find_attrs, :scopes, scopes)
1432 def logout(conn, _) do
1435 |> redirect(to: "/")
1438 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1439 Logger.debug("Unimplemented, returning unmodified relationship")
1441 with %User{} = target <- User.get_cached_by_id(id) do
1443 |> put_view(AccountView)
1444 |> render("relationship.json", %{user: user, target: target})
1448 def empty_array(conn, _) do
1449 Logger.debug("Unimplemented, returning an empty array")
1453 def empty_object(conn, _) do
1454 Logger.debug("Unimplemented, returning an empty object")
1458 def get_filters(%{assigns: %{user: user}} = conn, _) do
1459 filters = Filter.get_filters(user)
1460 res = FilterView.render("filters.json", filters: filters)
1465 %{assigns: %{user: user}} = conn,
1466 %{"phrase" => phrase, "context" => context} = params
1472 hide: Map.get(params, "irreversible", false),
1473 whole_word: Map.get(params, "boolean", true)
1477 {:ok, response} = Filter.create(query)
1478 res = FilterView.render("filter.json", filter: response)
1482 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1483 filter = Filter.get(filter_id, user)
1484 res = FilterView.render("filter.json", filter: filter)
1489 %{assigns: %{user: user}} = conn,
1490 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1494 filter_id: filter_id,
1497 hide: Map.get(params, "irreversible", nil),
1498 whole_word: Map.get(params, "boolean", true)
1502 {:ok, response} = Filter.update(query)
1503 res = FilterView.render("filter.json", filter: response)
1507 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1510 filter_id: filter_id
1513 {:ok, _} = Filter.delete(query)
1517 def suggestions(%{assigns: %{user: user}} = conn, _) do
1518 suggestions = Config.get(:suggestions)
1520 if Keyword.get(suggestions, :enabled, false) do
1521 api = Keyword.get(suggestions, :third_party_engine, "")
1522 timeout = Keyword.get(suggestions, :timeout, 5000)
1523 limit = Keyword.get(suggestions, :limit, 23)
1525 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1527 user = user.nickname
1531 |> String.replace("{{host}}", host)
1532 |> String.replace("{{user}}", user)
1534 with {:ok, %{status: 200, body: body}} <-
1535 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1536 {:ok, data} <- Jason.decode(body) do
1539 |> Enum.slice(0, limit)
1542 |> Map.put("id", fetch_suggestion_id(x))
1543 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1544 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1550 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1557 defp fetch_suggestion_id(attrs) do
1558 case User.get_or_fetch(attrs["acct"]) do
1559 {:ok, %User{id: id}} -> id
1564 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1565 with %Activity{} = activity <- Activity.get_by_id(status_id),
1566 true <- Visibility.visible_for_user?(activity, user) do
1570 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1580 def reports(%{assigns: %{user: user}} = conn, params) do
1581 case CommonAPI.report(user, params) do
1584 |> put_view(ReportView)
1585 |> try_render("report.json", %{activity: activity})
1589 |> put_status(:bad_request)
1590 |> json(%{error: err})
1594 def account_register(
1595 %{assigns: %{app: app}} = conn,
1596 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1604 "captcha_answer_data",
1608 |> Map.put("nickname", nickname)
1609 |> Map.put("fullname", params["fullname"] || nickname)
1610 |> Map.put("bio", params["bio"] || "")
1611 |> Map.put("confirm", params["password"])
1613 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1614 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1616 token_type: "Bearer",
1617 access_token: token.token,
1619 created_at: Token.Utils.format_created_at(token)
1624 |> put_status(:bad_request)
1629 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1630 render_error(conn, :bad_request, "Missing parameters")
1633 def account_register(conn, _) do
1634 render_error(conn, :forbidden, "Invalid credentials")
1637 def conversations(%{assigns: %{user: user}} = conn, params) do
1638 participations = Participation.for_user_with_last_activity_id(user, params)
1641 Enum.map(participations, fn participation ->
1642 ConversationView.render("participation.json", %{participation: participation, for: user})
1646 |> add_link_headers(participations)
1647 |> json(conversations)
1650 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1651 with %Participation{} = participation <-
1652 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1653 {:ok, participation} <- Participation.mark_as_read(participation) do
1654 participation_view =
1655 ConversationView.render("participation.json", %{participation: participation, for: user})
1658 |> json(participation_view)
1662 def password_reset(conn, params) do
1663 nickname_or_email = params["email"] || params["nickname"]
1665 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1667 |> put_status(:no_content)
1670 {:error, "unknown user"} ->
1671 send_resp(conn, :not_found, "")
1674 send_resp(conn, :bad_request, "")
1678 def account_confirmation_resend(conn, params) do
1679 nickname_or_email = params["email"] || params["nickname"]
1681 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1682 {:ok, _} <- User.try_send_confirmation_email(user) do
1684 |> json_response(:no_content, "")
1688 def try_render(conn, target, params)
1689 when is_binary(target) do
1690 case render(conn, target, params) do
1691 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1696 def try_render(conn, _, _) do
1697 render_error(conn, :not_implemented, "Can't display this activity")
1700 defp present?(nil), do: false
1701 defp present?(false), do: false
1702 defp present?(_), do: true