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 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
443 with %Object{} = object <- Object.get_by_id(id),
444 true <- object.data["type"] == "Question",
445 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
446 true <- Visibility.visible_for_user?(activity, user),
447 {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
449 |> put_view(StatusView)
450 |> try_render("poll.json", %{object: object, for: user})
455 |> json(%{error: "Record not found"})
460 |> json(%{error: "Record not found"})
465 |> json(%{error: message})
469 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
470 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
472 |> add_link_headers(:scheduled_statuses, scheduled_activities)
473 |> put_view(ScheduledActivityView)
474 |> render("index.json", %{scheduled_activities: scheduled_activities})
478 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
479 with %ScheduledActivity{} = scheduled_activity <-
480 ScheduledActivity.get(user, scheduled_activity_id) do
482 |> put_view(ScheduledActivityView)
483 |> render("show.json", %{scheduled_activity: scheduled_activity})
485 _ -> {:error, :not_found}
489 def update_scheduled_status(
490 %{assigns: %{user: user}} = conn,
491 %{"id" => scheduled_activity_id} = params
493 with %ScheduledActivity{} = scheduled_activity <-
494 ScheduledActivity.get(user, scheduled_activity_id),
495 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
497 |> put_view(ScheduledActivityView)
498 |> render("show.json", %{scheduled_activity: scheduled_activity})
500 nil -> {:error, :not_found}
505 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
506 with %ScheduledActivity{} = scheduled_activity <-
507 ScheduledActivity.get(user, scheduled_activity_id),
508 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
510 |> put_view(ScheduledActivityView)
511 |> render("show.json", %{scheduled_activity: scheduled_activity})
513 nil -> {:error, :not_found}
518 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
519 when length(media_ids) > 0 do
522 |> Map.put("status", ".")
524 post_status(conn, params)
527 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
530 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
532 scheduled_at = params["scheduled_at"]
534 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
535 with {:ok, scheduled_activity} <-
536 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
538 |> put_view(ScheduledActivityView)
539 |> render("show.json", %{scheduled_activity: scheduled_activity})
542 params = Map.drop(params, ["scheduled_at"])
544 case get_cached_status_or_post(conn, params) do
545 {:ignore, message} ->
548 |> json(%{error: message})
553 |> json(%{error: message})
557 |> put_view(StatusView)
558 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
563 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
565 case get_req_header(conn, "idempotency-key") do
567 _ -> Ecto.UUID.generate()
570 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
571 case CommonAPI.post(user, params) do
572 {:ok, activity} -> activity
573 {:error, message} -> {:ignore, message}
578 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
579 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
585 |> json(%{error: "Can't delete this post"})
589 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
590 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
591 %Activity{} = announce <- Activity.normalize(announce.data) do
593 |> put_view(StatusView)
594 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
598 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
599 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
600 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
602 |> put_view(StatusView)
603 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
607 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
608 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
609 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
611 |> put_view(StatusView)
612 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
616 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
617 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
618 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
620 |> put_view(StatusView)
621 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
625 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
626 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
628 |> put_view(StatusView)
629 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
633 |> put_resp_content_type("application/json")
634 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
638 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
639 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
641 |> put_view(StatusView)
642 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
646 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
647 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
648 %User{} = user <- User.get_cached_by_nickname(user.nickname),
649 true <- Visibility.visible_for_user?(activity, user),
650 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
658 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
659 %User{} = user <- User.get_cached_by_nickname(user.nickname),
660 true <- Visibility.visible_for_user?(activity, user),
661 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
663 |> put_view(StatusView)
664 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
668 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
669 activity = Activity.get_by_id(id)
671 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
673 |> put_view(StatusView)
674 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
678 |> put_resp_content_type("application/json")
679 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
683 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
684 activity = Activity.get_by_id(id)
686 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
688 |> put_view(StatusView)
689 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
693 def notifications(%{assigns: %{user: user}} = conn, params) do
694 notifications = MastodonAPI.get_notifications(user, params)
697 |> add_link_headers(:notifications, notifications)
698 |> put_view(NotificationView)
699 |> render("index.json", %{notifications: notifications, for: user})
702 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
703 with {:ok, notification} <- Notification.get(user, id) do
705 |> put_view(NotificationView)
706 |> render("show.json", %{notification: notification, for: user})
710 |> put_resp_content_type("application/json")
711 |> send_resp(403, Jason.encode!(%{"error" => reason}))
715 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
716 Notification.clear(user)
720 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
721 with {:ok, _notif} <- Notification.dismiss(user, id) do
726 |> put_resp_content_type("application/json")
727 |> send_resp(403, Jason.encode!(%{"error" => reason}))
731 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
732 Notification.destroy_multiple(user, ids)
736 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
738 q = from(u in User, where: u.id in ^id)
739 targets = Repo.all(q)
742 |> put_view(AccountView)
743 |> render("relationships.json", %{user: user, targets: targets})
746 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
747 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
749 def update_media(%{assigns: %{user: user}} = conn, data) do
750 with %Object{} = object <- Repo.get(Object, data["id"]),
751 true <- Object.authorize_mutation(object, user),
752 true <- is_binary(data["description"]),
753 description <- data["description"] do
754 new_data = %{object.data | "name" => description}
758 |> Object.change(%{data: new_data})
761 attachment_data = Map.put(new_data, "id", object.id)
764 |> put_view(StatusView)
765 |> render("attachment.json", %{attachment: attachment_data})
769 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
770 with {:ok, object} <-
773 actor: User.ap_id(user),
774 description: Map.get(data, "description")
776 attachment_data = Map.put(object.data, "id", object.id)
779 |> put_view(StatusView)
780 |> render("attachment.json", %{attachment: attachment_data})
784 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
785 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
786 %{} = attachment_data <- Map.put(object.data, "id", object.id),
787 %{type: type} = rendered <-
788 StatusView.render("attachment.json", %{attachment: attachment_data}) do
789 # Reject if not an image
790 if type == "image" do
792 # Save to the user's info
793 info_changeset = User.Info.mascot_update(user.info, rendered)
797 |> Ecto.Changeset.change()
798 |> Ecto.Changeset.put_embed(:info, info_changeset)
800 {:ok, _user} = User.update_and_set_cache(user_changeset)
806 |> put_resp_content_type("application/json")
807 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
812 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
813 mascot = User.get_mascot(user)
819 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
820 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
821 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
822 q = from(u in User, where: u.ap_id in ^likes)
826 |> put_view(AccountView)
827 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
833 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
834 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
835 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
836 q = from(u in User, where: u.ap_id in ^announces)
840 |> put_view(AccountView)
841 |> render("accounts.json", %{for: user, users: users, as: :user})
847 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
848 local_only = params["local"] in [true, "True", "true", "1"]
851 [params["tag"], params["any"]]
855 |> Enum.map(&String.downcase(&1))
860 |> Enum.map(&String.downcase(&1))
865 |> Enum.map(&String.downcase(&1))
869 |> Map.put("type", "Create")
870 |> Map.put("local_only", local_only)
871 |> Map.put("blocking_user", user)
872 |> Map.put("muting_user", user)
873 |> Map.put("tag", tags)
874 |> Map.put("tag_all", tag_all)
875 |> Map.put("tag_reject", tag_reject)
876 |> ActivityPub.fetch_public_activities()
880 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
881 |> put_view(StatusView)
882 |> render("index.json", %{activities: activities, for: user, as: :activity})
885 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
886 with %User{} = user <- User.get_cached_by_id(id),
887 followers <- MastodonAPI.get_followers(user, params) do
890 for_user && user.id == for_user.id -> followers
891 user.info.hide_followers -> []
896 |> add_link_headers(:followers, followers, user)
897 |> put_view(AccountView)
898 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
902 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
903 with %User{} = user <- User.get_cached_by_id(id),
904 followers <- MastodonAPI.get_friends(user, params) do
907 for_user && user.id == for_user.id -> followers
908 user.info.hide_follows -> []
913 |> add_link_headers(:following, followers, user)
914 |> put_view(AccountView)
915 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
919 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
920 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
922 |> put_view(AccountView)
923 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
927 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
928 with %User{} = follower <- User.get_cached_by_id(id),
929 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
931 |> put_view(AccountView)
932 |> render("relationship.json", %{user: followed, target: follower})
936 |> put_resp_content_type("application/json")
937 |> send_resp(403, Jason.encode!(%{"error" => message}))
941 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
942 with %User{} = follower <- User.get_cached_by_id(id),
943 {:ok, follower} <- CommonAPI.reject_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 follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
956 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
957 {_, true} <- {:followed, follower.id != followed.id},
958 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
960 |> put_view(AccountView)
961 |> render("relationship.json", %{user: follower, target: followed})
968 |> put_resp_content_type("application/json")
969 |> send_resp(403, Jason.encode!(%{"error" => message}))
973 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
974 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
975 {_, true} <- {:followed, follower.id != followed.id},
976 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
978 |> put_view(AccountView)
979 |> render("account.json", %{user: followed, for: follower})
986 |> put_resp_content_type("application/json")
987 |> send_resp(403, Jason.encode!(%{"error" => message}))
991 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
992 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
993 {_, true} <- {:followed, follower.id != followed.id},
994 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
996 |> put_view(AccountView)
997 |> render("relationship.json", %{user: follower, target: followed})
1000 {:error, :not_found}
1007 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1008 with %User{} = muted <- User.get_cached_by_id(id),
1009 {:ok, muter} <- User.mute(muter, muted) do
1011 |> put_view(AccountView)
1012 |> render("relationship.json", %{user: muter, target: muted})
1014 {:error, message} ->
1016 |> put_resp_content_type("application/json")
1017 |> send_resp(403, Jason.encode!(%{"error" => message}))
1021 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1022 with %User{} = muted <- User.get_cached_by_id(id),
1023 {:ok, muter} <- User.unmute(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 mutes(%{assigns: %{user: user}} = conn, _) do
1036 with muted_accounts <- User.muted_users(user) do
1037 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1042 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1043 with %User{} = blocked <- User.get_cached_by_id(id),
1044 {:ok, blocker} <- User.block(blocker, blocked),
1045 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1047 |> put_view(AccountView)
1048 |> render("relationship.json", %{user: blocker, target: blocked})
1050 {:error, message} ->
1052 |> put_resp_content_type("application/json")
1053 |> send_resp(403, Jason.encode!(%{"error" => message}))
1057 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1058 with %User{} = blocked <- User.get_cached_by_id(id),
1059 {:ok, blocker} <- User.unblock(blocker, blocked),
1060 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1062 |> put_view(AccountView)
1063 |> render("relationship.json", %{user: blocker, target: blocked})
1065 {:error, message} ->
1067 |> put_resp_content_type("application/json")
1068 |> send_resp(403, Jason.encode!(%{"error" => message}))
1072 def blocks(%{assigns: %{user: user}} = conn, _) do
1073 with blocked_accounts <- User.blocked_users(user) do
1074 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1079 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1080 json(conn, info.domain_blocks || [])
1083 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1084 User.block_domain(blocker, domain)
1088 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1089 User.unblock_domain(blocker, domain)
1093 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1094 with %User{} = subscription_target <- User.get_cached_by_id(id),
1095 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1097 |> put_view(AccountView)
1098 |> render("relationship.json", %{user: user, target: subscription_target})
1100 {:error, message} ->
1102 |> put_resp_content_type("application/json")
1103 |> send_resp(403, Jason.encode!(%{"error" => message}))
1107 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1108 with %User{} = subscription_target <- User.get_cached_by_id(id),
1109 {:ok, subscription_target} = User.unsubscribe(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 favourites(%{assigns: %{user: user}} = conn, params) do
1124 |> Map.put("type", "Create")
1125 |> Map.put("favorited_by", user.ap_id)
1126 |> Map.put("blocking_user", user)
1129 ActivityPub.fetch_activities([], params)
1133 |> add_link_headers(:favourites, activities)
1134 |> put_view(StatusView)
1135 |> render("index.json", %{activities: activities, for: user, as: :activity})
1138 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1139 with %User{} = user <- User.get_by_id(id),
1140 false <- user.info.hide_favorites do
1143 |> Map.put("type", "Create")
1144 |> Map.put("favorited_by", user.ap_id)
1145 |> Map.put("blocking_user", for_user)
1149 ["https://www.w3.org/ns/activitystreams#Public"] ++
1150 [for_user.ap_id | for_user.following]
1152 ["https://www.w3.org/ns/activitystreams#Public"]
1157 |> ActivityPub.fetch_activities(params)
1161 |> add_link_headers(:favourites, activities)
1162 |> put_view(StatusView)
1163 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1166 {:error, :not_found}
1171 |> json(%{error: "Can't get favorites"})
1175 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1176 user = User.get_cached_by_id(user.id)
1179 Bookmark.for_user_query(user.id)
1180 |> Pagination.fetch_paginated(params)
1184 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1187 |> add_link_headers(:bookmarks, bookmarks)
1188 |> put_view(StatusView)
1189 |> render("index.json", %{activities: activities, for: user, as: :activity})
1192 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1193 lists = Pleroma.List.for_user(user, opts)
1194 res = ListView.render("lists.json", lists: lists)
1198 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1199 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1200 res = ListView.render("list.json", list: list)
1206 |> json(%{error: "Record not found"})
1210 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1211 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1212 res = ListView.render("lists.json", lists: lists)
1216 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1217 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1218 {:ok, _list} <- Pleroma.List.delete(list) do
1226 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1227 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1228 res = ListView.render("list.json", list: list)
1233 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1235 |> Enum.each(fn account_id ->
1236 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1237 %User{} = followed <- User.get_cached_by_id(account_id) do
1238 Pleroma.List.follow(list, followed)
1245 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1247 |> Enum.each(fn account_id ->
1248 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1249 %User{} = followed <- User.get_cached_by_id(account_id) do
1250 Pleroma.List.unfollow(list, followed)
1257 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1258 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1259 {:ok, users} = Pleroma.List.get_following(list) do
1261 |> put_view(AccountView)
1262 |> render("accounts.json", %{for: user, users: users, as: :user})
1266 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1267 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1268 {:ok, list} <- Pleroma.List.rename(list, title) do
1269 res = ListView.render("list.json", list: list)
1277 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1278 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1281 |> Map.put("type", "Create")
1282 |> Map.put("blocking_user", user)
1283 |> Map.put("muting_user", user)
1285 # we must filter the following list for the user to avoid leaking statuses the user
1286 # does not actually have permission to see (for more info, peruse security issue #270).
1289 |> Enum.filter(fn x -> x in user.following end)
1290 |> ActivityPub.fetch_activities_bounded(following, params)
1294 |> put_view(StatusView)
1295 |> render("index.json", %{activities: activities, for: user, as: :activity})
1300 |> json(%{error: "Error."})
1304 def index(%{assigns: %{user: user}} = conn, _params) do
1305 token = get_session(conn, :oauth_token)
1308 mastodon_emoji = mastodonized_emoji()
1310 limit = Config.get([:instance, :limit])
1313 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1318 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1319 access_token: token,
1321 domain: Pleroma.Web.Endpoint.host(),
1324 unfollow_modal: false,
1327 auto_play_gif: false,
1328 display_sensitive_media: false,
1329 reduce_motion: false,
1330 max_toot_chars: limit,
1331 mascot: User.get_mascot(user)["url"]
1333 poll_limits: Config.get([:instance, :poll_limits]),
1335 delete_others_notice: present?(user.info.is_moderator),
1336 admin: present?(user.info.is_admin)
1340 default_privacy: user.info.default_scope,
1341 default_sensitive: false,
1342 allow_content_types: Config.get([:instance, :allowed_post_formats])
1344 media_attachments: %{
1345 accept_content_types: [
1361 user.info.settings ||
1391 push_subscription: nil,
1393 custom_emojis: mastodon_emoji,
1399 |> put_layout(false)
1400 |> put_view(MastodonView)
1401 |> render("index.html", %{initial_state: initial_state})
1404 |> put_session(:return_to, conn.request_path)
1405 |> redirect(to: "/web/login")
1409 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1410 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1412 with changeset <- Ecto.Changeset.change(user),
1413 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1414 {:ok, _user} <- User.update_and_set_cache(changeset) do
1419 |> put_resp_content_type("application/json")
1420 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1424 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1425 redirect(conn, to: local_mastodon_root_path(conn))
1428 @doc "Local Mastodon FE login init action"
1429 def login(conn, %{"code" => auth_token}) do
1430 with {:ok, app} <- get_or_make_app(),
1431 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1432 {:ok, token} <- Token.exchange_token(app, auth) do
1434 |> put_session(:oauth_token, token.token)
1435 |> redirect(to: local_mastodon_root_path(conn))
1439 @doc "Local Mastodon FE callback action"
1440 def login(conn, _) do
1441 with {:ok, app} <- get_or_make_app() do
1446 response_type: "code",
1447 client_id: app.client_id,
1449 scope: Enum.join(app.scopes, " ")
1452 redirect(conn, to: path)
1456 defp local_mastodon_root_path(conn) do
1457 case get_session(conn, :return_to) do
1459 mastodon_api_path(conn, :index, ["getting-started"])
1462 delete_session(conn, :return_to)
1467 defp get_or_make_app do
1468 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1469 scopes = ["read", "write", "follow", "push"]
1471 with %App{} = app <- Repo.get_by(App, find_attrs) do
1473 if app.scopes == scopes do
1477 |> Ecto.Changeset.change(%{scopes: scopes})
1485 App.register_changeset(
1487 Map.put(find_attrs, :scopes, scopes)
1494 def logout(conn, _) do
1497 |> redirect(to: "/")
1500 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1501 Logger.debug("Unimplemented, returning unmodified relationship")
1503 with %User{} = target <- User.get_cached_by_id(id) do
1505 |> put_view(AccountView)
1506 |> render("relationship.json", %{user: user, target: target})
1510 def empty_array(conn, _) do
1511 Logger.debug("Unimplemented, returning an empty array")
1515 def empty_object(conn, _) do
1516 Logger.debug("Unimplemented, returning an empty object")
1520 def get_filters(%{assigns: %{user: user}} = conn, _) do
1521 filters = Filter.get_filters(user)
1522 res = FilterView.render("filters.json", filters: filters)
1527 %{assigns: %{user: user}} = conn,
1528 %{"phrase" => phrase, "context" => context} = params
1534 hide: Map.get(params, "irreversible", false),
1535 whole_word: Map.get(params, "boolean", true)
1539 {:ok, response} = Filter.create(query)
1540 res = FilterView.render("filter.json", filter: response)
1544 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1545 filter = Filter.get(filter_id, user)
1546 res = FilterView.render("filter.json", filter: filter)
1551 %{assigns: %{user: user}} = conn,
1552 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1556 filter_id: filter_id,
1559 hide: Map.get(params, "irreversible", nil),
1560 whole_word: Map.get(params, "boolean", true)
1564 {:ok, response} = Filter.update(query)
1565 res = FilterView.render("filter.json", filter: response)
1569 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1572 filter_id: filter_id
1575 {:ok, _} = Filter.delete(query)
1581 def errors(conn, {:error, %Changeset{} = changeset}) do
1584 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1585 |> Enum.map_join(", ", fn {_k, v} -> v end)
1589 |> json(%{error: error_message})
1592 def errors(conn, {:error, :not_found}) do
1595 |> json(%{error: "Record not found"})
1598 def errors(conn, _) do
1601 |> json("Something went wrong")
1604 def suggestions(%{assigns: %{user: user}} = conn, _) do
1605 suggestions = Config.get(:suggestions)
1607 if Keyword.get(suggestions, :enabled, false) do
1608 api = Keyword.get(suggestions, :third_party_engine, "")
1609 timeout = Keyword.get(suggestions, :timeout, 5000)
1610 limit = Keyword.get(suggestions, :limit, 23)
1612 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1614 user = user.nickname
1618 |> String.replace("{{host}}", host)
1619 |> String.replace("{{user}}", user)
1621 with {:ok, %{status: 200, body: body}} <-
1626 recv_timeout: timeout,
1630 {:ok, data} <- Jason.decode(body) do
1633 |> Enum.slice(0, limit)
1638 case User.get_or_fetch(x["acct"]) do
1639 {:ok, %User{id: id}} -> id
1645 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1648 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1654 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1661 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1662 with %Activity{} = activity <- Activity.get_by_id(status_id),
1663 true <- Visibility.visible_for_user?(activity, user) do
1667 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1677 def reports(%{assigns: %{user: user}} = conn, params) do
1678 case CommonAPI.report(user, params) do
1681 |> put_view(ReportView)
1682 |> try_render("report.json", %{activity: activity})
1686 |> put_status(:bad_request)
1687 |> json(%{error: err})
1691 def account_register(
1692 %{assigns: %{app: app}} = conn,
1693 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1701 "captcha_answer_data",
1705 |> Map.put("nickname", nickname)
1706 |> Map.put("fullname", params["fullname"] || nickname)
1707 |> Map.put("bio", params["bio"] || "")
1708 |> Map.put("confirm", params["password"])
1710 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1711 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1713 token_type: "Bearer",
1714 access_token: token.token,
1716 created_at: Token.Utils.format_created_at(token)
1722 |> json(Jason.encode!(errors))
1726 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1729 |> json(%{error: "Missing parameters"})
1732 def account_register(conn, _) do
1735 |> json(%{error: "Invalid credentials"})
1738 def conversations(%{assigns: %{user: user}} = conn, params) do
1739 participations = Participation.for_user_with_last_activity_id(user, params)
1742 Enum.map(participations, fn participation ->
1743 ConversationView.render("participation.json", %{participation: participation, user: user})
1747 |> add_link_headers(:conversations, participations)
1748 |> json(conversations)
1751 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1752 with %Participation{} = participation <-
1753 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1754 {:ok, participation} <- Participation.mark_as_read(participation) do
1755 participation_view =
1756 ConversationView.render("participation.json", %{participation: participation, user: user})
1759 |> json(participation_view)
1763 def try_render(conn, target, params)
1764 when is_binary(target) do
1765 res = render(conn, target, params)
1770 |> json(%{error: "Can't display this activity"})
1776 def try_render(conn, _, _) do
1779 |> json(%{error: "Can't display this activity"})
1782 defp present?(nil), do: false
1783 defp present?(false), do: false
1784 defp present?(_), do: true