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
11 alias Pleroma.Conversation.Participation
13 alias Pleroma.Formatter
15 alias Pleroma.Notification
17 alias Pleroma.Pagination
19 alias Pleroma.ScheduledActivity
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
44 alias Pleroma.Web.ControllerHelper
49 plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
50 plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
52 @local_mastodon_name "Mastodon-Local"
54 action_fallback(:errors)
56 def create_app(conn, params) do
57 scopes = Scopes.fetch_scopes(params, ["read"])
61 |> Map.drop(["scope", "scopes"])
62 |> Map.put("scopes", scopes)
64 with cs <- App.register_changeset(%App{}, app_attrs),
65 false <- cs.changes[:client_name] == @local_mastodon_name,
66 {:ok, app} <- Repo.insert(cs) do
69 |> render("show.json", %{app: app})
78 value_function \\ fn x -> {:ok, x} end
80 if Map.has_key?(params, params_field) do
81 case value_function.(params[params_field]) do
82 {:ok, new_value} -> Map.put(map, map_field, new_value)
90 def update_credentials(%{assigns: %{user: user}} = conn, params) do
95 |> add_if_present(params, "display_name", :name)
96 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
97 |> add_if_present(params, "avatar", :avatar, fn value ->
98 with %Plug.Upload{} <- value,
99 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
106 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
109 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 :skip_thread_containment
122 |> Enum.reduce(%{}, fn key, acc ->
123 add_if_present(acc, params, to_string(key), key, fn value ->
124 {:ok, ControllerHelper.truthy_param?(value)}
127 |> add_if_present(params, "default_scope", :default_scope)
128 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
129 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
131 |> add_if_present(params, "header", :banner, fn value ->
132 with %Plug.Upload{} <- value,
133 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
139 |> Map.put(:emoji, user_info_emojis)
141 info_cng = User.Info.profile_update(user.info, info_params)
143 with changeset <- User.update_changeset(user, user_params),
144 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
145 {:ok, user} <- User.update_and_set_cache(changeset) do
146 if original_user != user do
147 CommonAPI.update(user)
152 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
158 |> json(%{error: "Invalid request"})
162 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
164 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
169 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
170 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
173 |> render("short.json", %{app: app})
177 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
178 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
179 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
180 account = AccountView.render("account.json", %{user: user, for: for_user})
186 |> json(%{error: "Can't find user"})
190 @mastodon_api_level "2.7.2"
192 def masto_instance(conn, _params) do
193 instance = Config.get(:instance)
197 title: Keyword.get(instance, :name),
198 description: Keyword.get(instance, :description),
199 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
200 email: Keyword.get(instance, :email),
202 streaming_api: Pleroma.Web.Endpoint.websocket_url()
204 stats: Stats.get_stats(),
205 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
207 registrations: Pleroma.Config.get([:instance, :registrations_open]),
208 # Extra (not present in Mastodon):
209 max_toot_chars: Keyword.get(instance, :limit),
210 poll_limits: Keyword.get(instance, :poll_limits)
216 def peers(conn, _params) do
217 json(conn, Stats.get_peers())
220 defp mastodonized_emoji do
221 Pleroma.Emoji.get_all()
222 |> Enum.map(fn {shortcode, relative_url, tags} ->
223 url = to_string(URI.merge(Web.base_url(), relative_url))
226 "shortcode" => shortcode,
228 "visible_in_picker" => true,
235 def custom_emojis(conn, _params) do
236 mastodon_emoji = mastodonized_emoji()
237 json(conn, mastodon_emoji)
240 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
243 |> Map.drop(["since_id", "max_id", "min_id"])
246 last = List.last(activities)
253 |> Map.get("limit", "20")
254 |> String.to_integer()
257 if length(activities) <= limit do
263 |> Enum.at(limit * -1)
267 {next_url, prev_url} =
271 Pleroma.Web.Endpoint,
274 Map.merge(params, %{max_id: max_id})
277 Pleroma.Web.Endpoint,
280 Map.merge(params, %{min_id: min_id})
286 Pleroma.Web.Endpoint,
288 Map.merge(params, %{max_id: max_id})
291 Pleroma.Web.Endpoint,
293 Map.merge(params, %{min_id: min_id})
299 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
305 def home_timeline(%{assigns: %{user: user}} = conn, params) do
308 |> Map.put("type", ["Create", "Announce"])
309 |> Map.put("blocking_user", user)
310 |> Map.put("muting_user", user)
311 |> Map.put("user", user)
314 [user.ap_id | user.following]
315 |> ActivityPub.fetch_activities(params)
319 |> add_link_headers(:home_timeline, activities)
320 |> put_view(StatusView)
321 |> render("index.json", %{activities: activities, for: user, as: :activity})
324 def public_timeline(%{assigns: %{user: user}} = conn, params) do
325 local_only = params["local"] in [true, "True", "true", "1"]
329 |> Map.put("type", ["Create", "Announce"])
330 |> Map.put("local_only", local_only)
331 |> Map.put("blocking_user", user)
332 |> Map.put("muting_user", user)
333 |> ActivityPub.fetch_public_activities()
337 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
338 |> put_view(StatusView)
339 |> render("index.json", %{activities: activities, for: user, as: :activity})
342 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
343 with %User{} = user <- User.get_cached_by_id(params["id"]) do
344 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
347 |> add_link_headers(:user_statuses, activities, params["id"])
348 |> put_view(StatusView)
349 |> render("index.json", %{
350 activities: activities,
357 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
360 |> Map.put("type", "Create")
361 |> Map.put("blocking_user", user)
362 |> Map.put("user", user)
363 |> Map.put(:visibility, "direct")
367 |> ActivityPub.fetch_activities_query(params)
368 |> Pagination.fetch_paginated(params)
371 |> add_link_headers(:dm_timeline, activities)
372 |> put_view(StatusView)
373 |> render("index.json", %{activities: activities, for: user, as: :activity})
376 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
378 true <- Visibility.visible_for_user?(activity, user) do
380 |> put_view(StatusView)
381 |> try_render("status.json", %{activity: activity, for: user})
385 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
386 with %Activity{} = activity <- Activity.get_by_id(id),
388 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
389 "blocking_user" => user,
393 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
395 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
396 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
402 activities: grouped_activities[true] || [],
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
411 activities: grouped_activities[false] || [],
415 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
422 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
423 with %Object{} = object <- Object.get_by_id(id),
424 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
425 true <- Visibility.visible_for_user?(activity, user) do
427 |> put_view(StatusView)
428 |> try_render("poll.json", %{object: object, for: user})
433 |> json(%{error: "Record not found"})
438 |> json(%{error: "Record not found"})
442 defp get_cached_vote_or_vote(user, object, choices) do
443 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
446 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
447 case CommonAPI.vote(user, object, choices) do
448 {:error, _message} = res -> {:ignore, res}
449 res -> {:commit, res}
456 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
457 with %Object{} = object <- Object.get_by_id(id),
458 true <- object.data["type"] == "Question",
459 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
460 true <- Visibility.visible_for_user?(activity, user),
461 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
463 |> put_view(StatusView)
464 |> try_render("poll.json", %{object: object, for: user})
469 |> json(%{error: "Record not found"})
474 |> json(%{error: "Record not found"})
479 |> json(%{error: message})
483 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
484 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
486 |> add_link_headers(:scheduled_statuses, scheduled_activities)
487 |> put_view(ScheduledActivityView)
488 |> render("index.json", %{scheduled_activities: scheduled_activities})
492 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
493 with %ScheduledActivity{} = scheduled_activity <-
494 ScheduledActivity.get(user, scheduled_activity_id) do
496 |> put_view(ScheduledActivityView)
497 |> render("show.json", %{scheduled_activity: scheduled_activity})
499 _ -> {:error, :not_found}
503 def update_scheduled_status(
504 %{assigns: %{user: user}} = conn,
505 %{"id" => scheduled_activity_id} = params
507 with %ScheduledActivity{} = scheduled_activity <-
508 ScheduledActivity.get(user, scheduled_activity_id),
509 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
511 |> put_view(ScheduledActivityView)
512 |> render("show.json", %{scheduled_activity: scheduled_activity})
514 nil -> {:error, :not_found}
519 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
520 with %ScheduledActivity{} = scheduled_activity <-
521 ScheduledActivity.get(user, scheduled_activity_id),
522 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
524 |> put_view(ScheduledActivityView)
525 |> render("show.json", %{scheduled_activity: scheduled_activity})
527 nil -> {:error, :not_found}
532 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
533 when length(media_ids) > 0 do
536 |> Map.put("status", ".")
538 post_status(conn, params)
541 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
544 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
546 scheduled_at = params["scheduled_at"]
548 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
549 with {:ok, scheduled_activity} <-
550 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
552 |> put_view(ScheduledActivityView)
553 |> render("show.json", %{scheduled_activity: scheduled_activity})
556 params = Map.drop(params, ["scheduled_at"])
558 case get_cached_status_or_post(conn, params) do
559 {:ignore, message} ->
562 |> json(%{error: message})
567 |> json(%{error: message})
571 |> put_view(StatusView)
572 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
577 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
579 case get_req_header(conn, "idempotency-key") do
581 _ -> Ecto.UUID.generate()
584 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
585 case CommonAPI.post(user, params) do
586 {:ok, activity} -> activity
587 {:error, message} -> {:ignore, message}
592 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
593 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
599 |> json(%{error: "Can't delete this post"})
603 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
604 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
605 %Activity{} = announce <- Activity.normalize(announce.data) do
607 |> put_view(StatusView)
608 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
612 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
613 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
614 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
616 |> put_view(StatusView)
617 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
621 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
622 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
623 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
625 |> put_view(StatusView)
626 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
630 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
631 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
632 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
634 |> put_view(StatusView)
635 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
639 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
640 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
642 |> put_view(StatusView)
643 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
647 |> put_resp_content_type("application/json")
648 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
652 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
653 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
655 |> put_view(StatusView)
656 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
660 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
661 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
662 %User{} = user <- User.get_cached_by_nickname(user.nickname),
663 true <- Visibility.visible_for_user?(activity, user),
664 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
666 |> put_view(StatusView)
667 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
671 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
672 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
673 %User{} = user <- User.get_cached_by_nickname(user.nickname),
674 true <- Visibility.visible_for_user?(activity, user),
675 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
677 |> put_view(StatusView)
678 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
682 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
683 activity = Activity.get_by_id(id)
685 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
687 |> put_view(StatusView)
688 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
692 |> put_resp_content_type("application/json")
693 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
697 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
698 activity = Activity.get_by_id(id)
700 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
702 |> put_view(StatusView)
703 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
707 def notifications(%{assigns: %{user: user}} = conn, params) do
708 notifications = MastodonAPI.get_notifications(user, params)
711 |> add_link_headers(:notifications, notifications)
712 |> put_view(NotificationView)
713 |> render("index.json", %{notifications: notifications, for: user})
716 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
717 with {:ok, notification} <- Notification.get(user, id) do
719 |> put_view(NotificationView)
720 |> render("show.json", %{notification: notification, for: user})
724 |> put_resp_content_type("application/json")
725 |> send_resp(403, Jason.encode!(%{"error" => reason}))
729 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
730 Notification.clear(user)
734 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
735 with {:ok, _notif} <- Notification.dismiss(user, id) do
740 |> put_resp_content_type("application/json")
741 |> send_resp(403, Jason.encode!(%{"error" => reason}))
745 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
746 Notification.destroy_multiple(user, ids)
750 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
752 q = from(u in User, where: u.id in ^id)
753 targets = Repo.all(q)
756 |> put_view(AccountView)
757 |> render("relationships.json", %{user: user, targets: targets})
760 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
761 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
763 def update_media(%{assigns: %{user: user}} = conn, data) do
764 with %Object{} = object <- Repo.get(Object, data["id"]),
765 true <- Object.authorize_mutation(object, user),
766 true <- is_binary(data["description"]),
767 description <- data["description"] do
768 new_data = %{object.data | "name" => description}
772 |> Object.change(%{data: new_data})
775 attachment_data = Map.put(new_data, "id", object.id)
778 |> put_view(StatusView)
779 |> render("attachment.json", %{attachment: attachment_data})
783 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
784 with {:ok, object} <-
787 actor: User.ap_id(user),
788 description: Map.get(data, "description")
790 attachment_data = Map.put(object.data, "id", object.id)
793 |> put_view(StatusView)
794 |> render("attachment.json", %{attachment: attachment_data})
798 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
799 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
800 %{} = attachment_data <- Map.put(object.data, "id", object.id),
801 %{type: type} = rendered <-
802 StatusView.render("attachment.json", %{attachment: attachment_data}) do
803 # Reject if not an image
804 if type == "image" do
806 # Save to the user's info
807 info_changeset = User.Info.mascot_update(user.info, rendered)
811 |> Ecto.Changeset.change()
812 |> Ecto.Changeset.put_embed(:info, info_changeset)
814 {:ok, _user} = User.update_and_set_cache(user_changeset)
820 |> put_resp_content_type("application/json")
821 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
826 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
827 mascot = User.get_mascot(user)
833 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
834 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
835 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
836 q = from(u in User, where: u.ap_id in ^likes)
840 |> put_view(AccountView)
841 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
847 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
848 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
849 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
850 q = from(u in User, where: u.ap_id in ^announces)
854 |> put_view(AccountView)
855 |> render("accounts.json", %{for: user, users: users, as: :user})
861 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
862 local_only = params["local"] in [true, "True", "true", "1"]
865 [params["tag"], params["any"]]
869 |> Enum.map(&String.downcase(&1))
874 |> Enum.map(&String.downcase(&1))
879 |> Enum.map(&String.downcase(&1))
883 |> Map.put("type", "Create")
884 |> Map.put("local_only", local_only)
885 |> Map.put("blocking_user", user)
886 |> Map.put("muting_user", user)
887 |> Map.put("tag", tags)
888 |> Map.put("tag_all", tag_all)
889 |> Map.put("tag_reject", tag_reject)
890 |> ActivityPub.fetch_public_activities()
894 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
895 |> put_view(StatusView)
896 |> render("index.json", %{activities: activities, for: user, as: :activity})
899 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
900 with %User{} = user <- User.get_cached_by_id(id),
901 followers <- MastodonAPI.get_followers(user, params) do
904 for_user && user.id == for_user.id -> followers
905 user.info.hide_followers -> []
910 |> add_link_headers(:followers, followers, user)
911 |> put_view(AccountView)
912 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
916 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
917 with %User{} = user <- User.get_cached_by_id(id),
918 followers <- MastodonAPI.get_friends(user, params) do
921 for_user && user.id == for_user.id -> followers
922 user.info.hide_follows -> []
927 |> add_link_headers(:following, followers, user)
928 |> put_view(AccountView)
929 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
933 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
934 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
936 |> put_view(AccountView)
937 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
941 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
942 with %User{} = follower <- User.get_cached_by_id(id),
943 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
945 |> put_view(AccountView)
946 |> render("relationship.json", %{user: followed, target: follower})
950 |> put_resp_content_type("application/json")
951 |> send_resp(403, Jason.encode!(%{"error" => message}))
955 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
956 with %User{} = follower <- User.get_cached_by_id(id),
957 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
959 |> put_view(AccountView)
960 |> render("relationship.json", %{user: followed, target: follower})
964 |> put_resp_content_type("application/json")
965 |> send_resp(403, Jason.encode!(%{"error" => message}))
969 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
970 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
971 {_, true} <- {:followed, follower.id != followed.id},
972 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
974 |> put_view(AccountView)
975 |> render("relationship.json", %{user: follower, target: followed})
982 |> put_resp_content_type("application/json")
983 |> send_resp(403, Jason.encode!(%{"error" => message}))
987 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
988 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
989 {_, true} <- {:followed, follower.id != followed.id},
990 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
992 |> put_view(AccountView)
993 |> render("account.json", %{user: followed, for: follower})
1000 |> put_resp_content_type("application/json")
1001 |> send_resp(403, Jason.encode!(%{"error" => message}))
1005 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1006 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1007 {_, true} <- {:followed, follower.id != followed.id},
1008 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1010 |> put_view(AccountView)
1011 |> render("relationship.json", %{user: follower, target: followed})
1014 {:error, :not_found}
1021 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1022 with %User{} = muted <- User.get_cached_by_id(id),
1023 {:ok, muter} <- User.mute(muter, muted) do
1025 |> put_view(AccountView)
1026 |> render("relationship.json", %{user: muter, target: muted})
1028 {:error, message} ->
1030 |> put_resp_content_type("application/json")
1031 |> send_resp(403, Jason.encode!(%{"error" => message}))
1035 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1036 with %User{} = muted <- User.get_cached_by_id(id),
1037 {:ok, muter} <- User.unmute(muter, muted) do
1039 |> put_view(AccountView)
1040 |> render("relationship.json", %{user: muter, target: muted})
1042 {:error, message} ->
1044 |> put_resp_content_type("application/json")
1045 |> send_resp(403, Jason.encode!(%{"error" => message}))
1049 def mutes(%{assigns: %{user: user}} = conn, _) do
1050 with muted_accounts <- User.muted_users(user) do
1051 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1056 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1057 with %User{} = blocked <- User.get_cached_by_id(id),
1058 {:ok, blocker} <- User.block(blocker, blocked),
1059 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1061 |> put_view(AccountView)
1062 |> render("relationship.json", %{user: blocker, target: blocked})
1064 {:error, message} ->
1066 |> put_resp_content_type("application/json")
1067 |> send_resp(403, Jason.encode!(%{"error" => message}))
1071 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1072 with %User{} = blocked <- User.get_cached_by_id(id),
1073 {:ok, blocker} <- User.unblock(blocker, blocked),
1074 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1076 |> put_view(AccountView)
1077 |> render("relationship.json", %{user: blocker, target: blocked})
1079 {:error, message} ->
1081 |> put_resp_content_type("application/json")
1082 |> send_resp(403, Jason.encode!(%{"error" => message}))
1086 def blocks(%{assigns: %{user: user}} = conn, _) do
1087 with blocked_accounts <- User.blocked_users(user) do
1088 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1093 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1094 json(conn, info.domain_blocks || [])
1097 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1098 User.block_domain(blocker, domain)
1102 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1103 User.unblock_domain(blocker, domain)
1107 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1108 with %User{} = subscription_target <- User.get_cached_by_id(id),
1109 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1111 |> put_view(AccountView)
1112 |> render("relationship.json", %{user: user, target: subscription_target})
1114 {:error, message} ->
1116 |> put_resp_content_type("application/json")
1117 |> send_resp(403, Jason.encode!(%{"error" => message}))
1121 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1122 with %User{} = subscription_target <- User.get_cached_by_id(id),
1123 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1125 |> put_view(AccountView)
1126 |> render("relationship.json", %{user: user, target: subscription_target})
1128 {:error, message} ->
1130 |> put_resp_content_type("application/json")
1131 |> send_resp(403, Jason.encode!(%{"error" => message}))
1135 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1136 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1137 statuses = Activity.search(user, query)
1138 tags_path = Web.base_url() <> "/tag/"
1144 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1145 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1146 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1149 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1151 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1158 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1159 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1160 statuses = Activity.search(user, query)
1166 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1167 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1170 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1172 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1179 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1180 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1182 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1187 def favourites(%{assigns: %{user: user}} = conn, params) do
1190 |> Map.put("type", "Create")
1191 |> Map.put("favorited_by", user.ap_id)
1192 |> Map.put("blocking_user", user)
1195 ActivityPub.fetch_activities([], params)
1199 |> add_link_headers(:favourites, activities)
1200 |> put_view(StatusView)
1201 |> render("index.json", %{activities: activities, for: user, as: :activity})
1204 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1205 with %User{} = user <- User.get_by_id(id),
1206 false <- user.info.hide_favorites do
1209 |> Map.put("type", "Create")
1210 |> Map.put("favorited_by", user.ap_id)
1211 |> Map.put("blocking_user", for_user)
1215 ["https://www.w3.org/ns/activitystreams#Public"] ++
1216 [for_user.ap_id | for_user.following]
1218 ["https://www.w3.org/ns/activitystreams#Public"]
1223 |> ActivityPub.fetch_activities(params)
1227 |> add_link_headers(:favourites, activities)
1228 |> put_view(StatusView)
1229 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1232 {:error, :not_found}
1237 |> json(%{error: "Can't get favorites"})
1241 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1242 user = User.get_cached_by_id(user.id)
1245 Bookmark.for_user_query(user.id)
1246 |> Pagination.fetch_paginated(params)
1250 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1253 |> add_link_headers(:bookmarks, bookmarks)
1254 |> put_view(StatusView)
1255 |> render("index.json", %{activities: activities, for: user, as: :activity})
1258 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1259 lists = Pleroma.List.for_user(user, opts)
1260 res = ListView.render("lists.json", lists: lists)
1264 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1265 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1266 res = ListView.render("list.json", list: list)
1272 |> json(%{error: "Record not found"})
1276 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1277 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1278 res = ListView.render("lists.json", lists: lists)
1282 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1283 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1284 {:ok, _list} <- Pleroma.List.delete(list) do
1292 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1293 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1294 res = ListView.render("list.json", list: list)
1299 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1301 |> Enum.each(fn account_id ->
1302 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1303 %User{} = followed <- User.get_cached_by_id(account_id) do
1304 Pleroma.List.follow(list, followed)
1311 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1313 |> Enum.each(fn account_id ->
1314 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1315 %User{} = followed <- User.get_cached_by_id(account_id) do
1316 Pleroma.List.unfollow(list, followed)
1323 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1324 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1325 {:ok, users} = Pleroma.List.get_following(list) do
1327 |> put_view(AccountView)
1328 |> render("accounts.json", %{for: user, users: users, as: :user})
1332 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1333 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1334 {:ok, list} <- Pleroma.List.rename(list, title) do
1335 res = ListView.render("list.json", list: list)
1343 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1344 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1347 |> Map.put("type", "Create")
1348 |> Map.put("blocking_user", user)
1349 |> Map.put("muting_user", user)
1351 # we must filter the following list for the user to avoid leaking statuses the user
1352 # does not actually have permission to see (for more info, peruse security issue #270).
1355 |> Enum.filter(fn x -> x in user.following end)
1356 |> ActivityPub.fetch_activities_bounded(following, params)
1360 |> put_view(StatusView)
1361 |> render("index.json", %{activities: activities, for: user, as: :activity})
1366 |> json(%{error: "Error."})
1370 def index(%{assigns: %{user: user}} = conn, _params) do
1371 token = get_session(conn, :oauth_token)
1374 mastodon_emoji = mastodonized_emoji()
1376 limit = Config.get([:instance, :limit])
1379 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1384 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1385 access_token: token,
1387 domain: Pleroma.Web.Endpoint.host(),
1390 unfollow_modal: false,
1393 auto_play_gif: false,
1394 display_sensitive_media: false,
1395 reduce_motion: false,
1396 max_toot_chars: limit,
1397 mascot: User.get_mascot(user)["url"]
1399 poll_limits: Config.get([:instance, :poll_limits]),
1401 delete_others_notice: present?(user.info.is_moderator),
1402 admin: present?(user.info.is_admin)
1406 default_privacy: user.info.default_scope,
1407 default_sensitive: false,
1408 allow_content_types: Config.get([:instance, :allowed_post_formats])
1410 media_attachments: %{
1411 accept_content_types: [
1427 user.info.settings ||
1457 push_subscription: nil,
1459 custom_emojis: mastodon_emoji,
1465 |> put_layout(false)
1466 |> put_view(MastodonView)
1467 |> render("index.html", %{initial_state: initial_state})
1470 |> put_session(:return_to, conn.request_path)
1471 |> redirect(to: "/web/login")
1475 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1476 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1478 with changeset <- Ecto.Changeset.change(user),
1479 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1480 {:ok, _user} <- User.update_and_set_cache(changeset) do
1485 |> put_resp_content_type("application/json")
1486 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1490 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1491 redirect(conn, to: local_mastodon_root_path(conn))
1494 @doc "Local Mastodon FE login init action"
1495 def login(conn, %{"code" => auth_token}) do
1496 with {:ok, app} <- get_or_make_app(),
1497 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1498 {:ok, token} <- Token.exchange_token(app, auth) do
1500 |> put_session(:oauth_token, token.token)
1501 |> redirect(to: local_mastodon_root_path(conn))
1505 @doc "Local Mastodon FE callback action"
1506 def login(conn, _) do
1507 with {:ok, app} <- get_or_make_app() do
1512 response_type: "code",
1513 client_id: app.client_id,
1515 scope: Enum.join(app.scopes, " ")
1518 redirect(conn, to: path)
1522 defp local_mastodon_root_path(conn) do
1523 case get_session(conn, :return_to) do
1525 mastodon_api_path(conn, :index, ["getting-started"])
1528 delete_session(conn, :return_to)
1533 defp get_or_make_app do
1534 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1535 scopes = ["read", "write", "follow", "push"]
1537 with %App{} = app <- Repo.get_by(App, find_attrs) do
1539 if app.scopes == scopes do
1543 |> Ecto.Changeset.change(%{scopes: scopes})
1551 App.register_changeset(
1553 Map.put(find_attrs, :scopes, scopes)
1560 def logout(conn, _) do
1563 |> redirect(to: "/")
1566 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1567 Logger.debug("Unimplemented, returning unmodified relationship")
1569 with %User{} = target <- User.get_cached_by_id(id) do
1571 |> put_view(AccountView)
1572 |> render("relationship.json", %{user: user, target: target})
1576 def empty_array(conn, _) do
1577 Logger.debug("Unimplemented, returning an empty array")
1581 def empty_object(conn, _) do
1582 Logger.debug("Unimplemented, returning an empty object")
1586 def get_filters(%{assigns: %{user: user}} = conn, _) do
1587 filters = Filter.get_filters(user)
1588 res = FilterView.render("filters.json", filters: filters)
1593 %{assigns: %{user: user}} = conn,
1594 %{"phrase" => phrase, "context" => context} = params
1600 hide: Map.get(params, "irreversible", false),
1601 whole_word: Map.get(params, "boolean", true)
1605 {:ok, response} = Filter.create(query)
1606 res = FilterView.render("filter.json", filter: response)
1610 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1611 filter = Filter.get(filter_id, user)
1612 res = FilterView.render("filter.json", filter: filter)
1617 %{assigns: %{user: user}} = conn,
1618 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1622 filter_id: filter_id,
1625 hide: Map.get(params, "irreversible", nil),
1626 whole_word: Map.get(params, "boolean", true)
1630 {:ok, response} = Filter.update(query)
1631 res = FilterView.render("filter.json", filter: response)
1635 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1638 filter_id: filter_id
1641 {:ok, _} = Filter.delete(query)
1647 def errors(conn, {:error, %Changeset{} = changeset}) do
1650 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1651 |> Enum.map_join(", ", fn {_k, v} -> v end)
1655 |> json(%{error: error_message})
1658 def errors(conn, {:error, :not_found}) do
1661 |> json(%{error: "Record not found"})
1664 def errors(conn, _) do
1667 |> json("Something went wrong")
1670 def suggestions(%{assigns: %{user: user}} = conn, _) do
1671 suggestions = Config.get(:suggestions)
1673 if Keyword.get(suggestions, :enabled, false) do
1674 api = Keyword.get(suggestions, :third_party_engine, "")
1675 timeout = Keyword.get(suggestions, :timeout, 5000)
1676 limit = Keyword.get(suggestions, :limit, 23)
1678 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1680 user = user.nickname
1684 |> String.replace("{{host}}", host)
1685 |> String.replace("{{user}}", user)
1687 with {:ok, %{status: 200, body: body}} <-
1692 recv_timeout: timeout,
1696 {:ok, data} <- Jason.decode(body) do
1699 |> Enum.slice(0, limit)
1704 case User.get_or_fetch(x["acct"]) do
1705 {:ok, %User{id: id}} -> id
1711 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1714 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1720 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1727 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1728 with %Activity{} = activity <- Activity.get_by_id(status_id),
1729 true <- Visibility.visible_for_user?(activity, user) do
1733 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1743 def reports(%{assigns: %{user: user}} = conn, params) do
1744 case CommonAPI.report(user, params) do
1747 |> put_view(ReportView)
1748 |> try_render("report.json", %{activity: activity})
1752 |> put_status(:bad_request)
1753 |> json(%{error: err})
1757 def account_register(
1758 %{assigns: %{app: app}} = conn,
1759 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1767 "captcha_answer_data",
1771 |> Map.put("nickname", nickname)
1772 |> Map.put("fullname", params["fullname"] || nickname)
1773 |> Map.put("bio", params["bio"] || "")
1774 |> Map.put("confirm", params["password"])
1776 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1777 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1779 token_type: "Bearer",
1780 access_token: token.token,
1782 created_at: Token.Utils.format_created_at(token)
1788 |> json(Jason.encode!(errors))
1792 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1795 |> json(%{error: "Missing parameters"})
1798 def account_register(conn, _) do
1801 |> json(%{error: "Invalid credentials"})
1804 def conversations(%{assigns: %{user: user}} = conn, params) do
1805 participations = Participation.for_user_with_last_activity_id(user, params)
1808 Enum.map(participations, fn participation ->
1809 ConversationView.render("participation.json", %{participation: participation, user: user})
1813 |> add_link_headers(:conversations, participations)
1814 |> json(conversations)
1817 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1818 with %Participation{} = participation <-
1819 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1820 {:ok, participation} <- Participation.mark_as_read(participation) do
1821 participation_view =
1822 ConversationView.render("participation.json", %{participation: participation, user: user})
1825 |> json(participation_view)
1829 def try_render(conn, target, params)
1830 when is_binary(target) do
1831 res = render(conn, target, params)
1836 |> json(%{error: "Can't display this activity"})
1842 def try_render(conn, _, _) do
1845 |> json(%{error: "Can't display this activity"})
1848 defp present?(nil), do: false
1849 defp present?(false), do: false
1850 defp present?(_), do: true