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.Object.Fetcher
18 alias Pleroma.Pagination
20 alias Pleroma.ScheduledActivity
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.ConversationView
30 alias Pleroma.Web.MastodonAPI.FilterView
31 alias Pleroma.Web.MastodonAPI.ListView
32 alias Pleroma.Web.MastodonAPI.MastodonAPI
33 alias Pleroma.Web.MastodonAPI.MastodonView
34 alias Pleroma.Web.MastodonAPI.NotificationView
35 alias Pleroma.Web.MastodonAPI.ReportView
36 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
37 alias Pleroma.Web.MastodonAPI.StatusView
38 alias Pleroma.Web.MediaProxy
39 alias Pleroma.Web.OAuth.App
40 alias Pleroma.Web.OAuth.Authorization
41 alias Pleroma.Web.OAuth.Scopes
42 alias Pleroma.Web.OAuth.Token
43 alias Pleroma.Web.TwitterAPI.TwitterAPI
45 alias Pleroma.Web.ControllerHelper
51 Pleroma.Plugs.RateLimitPlug,
53 max_requests: Config.get([:app_account_creation, :max_requests]),
54 interval: Config.get([:app_account_creation, :interval])
56 when action in [:account_register]
59 @local_mastodon_name "Mastodon-Local"
61 action_fallback(:errors)
63 def create_app(conn, params) do
64 scopes = Scopes.fetch_scopes(params, ["read"])
68 |> Map.drop(["scope", "scopes"])
69 |> Map.put("scopes", scopes)
71 with cs <- App.register_changeset(%App{}, app_attrs),
72 false <- cs.changes[:client_name] == @local_mastodon_name,
73 {:ok, app} <- Repo.insert(cs) do
76 |> render("show.json", %{app: app})
85 value_function \\ fn x -> {:ok, x} end
87 if Map.has_key?(params, params_field) do
88 case value_function.(params[params_field]) do
89 {:ok, new_value} -> Map.put(map, map_field, new_value)
97 def update_credentials(%{assigns: %{user: user}} = conn, params) do
102 |> add_if_present(params, "display_name", :name)
103 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
104 |> add_if_present(params, "avatar", :avatar, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
113 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
116 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
127 :skip_thread_containment
129 |> Enum.reduce(%{}, fn key, acc ->
130 add_if_present(acc, params, to_string(key), key, fn value ->
131 {:ok, ControllerHelper.truthy_param?(value)}
134 |> add_if_present(params, "default_scope", :default_scope)
135 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
136 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
138 |> add_if_present(params, "header", :banner, fn value ->
139 with %Plug.Upload{} <- value,
140 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
146 |> Map.put(:emoji, user_info_emojis)
148 info_cng = User.Info.profile_update(user.info, info_params)
150 with changeset <- User.update_changeset(user, user_params),
151 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
152 {:ok, user} <- User.update_and_set_cache(changeset) do
153 if original_user != user do
154 CommonAPI.update(user)
159 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
165 |> json(%{error: "Invalid request"})
169 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
171 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
176 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
177 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
180 |> render("short.json", %{app: app})
184 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
185 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
186 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
187 account = AccountView.render("account.json", %{user: user, for: for_user})
193 |> json(%{error: "Can't find user"})
197 @mastodon_api_level "2.7.2"
199 def masto_instance(conn, _params) do
200 instance = Config.get(:instance)
204 title: Keyword.get(instance, :name),
205 description: Keyword.get(instance, :description),
206 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
207 email: Keyword.get(instance, :email),
209 streaming_api: Pleroma.Web.Endpoint.websocket_url()
211 stats: Stats.get_stats(),
212 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
214 registrations: Pleroma.Config.get([:instance, :registrations_open]),
215 # Extra (not present in Mastodon):
216 max_toot_chars: Keyword.get(instance, :limit),
217 poll_limits: Keyword.get(instance, :poll_limits)
223 def peers(conn, _params) do
224 json(conn, Stats.get_peers())
227 defp mastodonized_emoji do
228 Pleroma.Emoji.get_all()
229 |> Enum.map(fn {shortcode, relative_url, tags} ->
230 url = to_string(URI.merge(Web.base_url(), relative_url))
233 "shortcode" => shortcode,
235 "visible_in_picker" => true,
242 def custom_emojis(conn, _params) do
243 mastodon_emoji = mastodonized_emoji()
244 json(conn, mastodon_emoji)
247 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
250 |> Map.drop(["since_id", "max_id", "min_id"])
253 last = List.last(activities)
260 |> Map.get("limit", "20")
261 |> String.to_integer()
264 if length(activities) <= limit do
270 |> Enum.at(limit * -1)
274 {next_url, prev_url} =
278 Pleroma.Web.Endpoint,
281 Map.merge(params, %{max_id: max_id})
284 Pleroma.Web.Endpoint,
287 Map.merge(params, %{min_id: min_id})
293 Pleroma.Web.Endpoint,
295 Map.merge(params, %{max_id: max_id})
298 Pleroma.Web.Endpoint,
300 Map.merge(params, %{min_id: min_id})
306 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
312 def home_timeline(%{assigns: %{user: user}} = conn, params) do
315 |> Map.put("type", ["Create", "Announce"])
316 |> Map.put("blocking_user", user)
317 |> Map.put("muting_user", user)
318 |> Map.put("user", user)
321 [user.ap_id | user.following]
322 |> ActivityPub.fetch_activities(params)
326 |> add_link_headers(:home_timeline, activities)
327 |> put_view(StatusView)
328 |> render("index.json", %{activities: activities, for: user, as: :activity})
331 def public_timeline(%{assigns: %{user: user}} = conn, params) do
332 local_only = params["local"] in [true, "True", "true", "1"]
336 |> Map.put("type", ["Create", "Announce"])
337 |> Map.put("local_only", local_only)
338 |> Map.put("blocking_user", user)
339 |> Map.put("muting_user", user)
340 |> ActivityPub.fetch_public_activities()
344 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
345 |> put_view(StatusView)
346 |> render("index.json", %{activities: activities, for: user, as: :activity})
349 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
350 with %User{} = user <- User.get_cached_by_id(params["id"]) do
351 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
354 |> add_link_headers(:user_statuses, activities, params["id"])
355 |> put_view(StatusView)
356 |> render("index.json", %{
357 activities: activities,
364 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
367 |> Map.put("type", "Create")
368 |> Map.put("blocking_user", user)
369 |> Map.put("user", user)
370 |> Map.put(:visibility, "direct")
374 |> ActivityPub.fetch_activities_query(params)
375 |> Pagination.fetch_paginated(params)
378 |> add_link_headers(:dm_timeline, activities)
379 |> put_view(StatusView)
380 |> render("index.json", %{activities: activities, for: user, as: :activity})
383 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
384 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
385 true <- Visibility.visible_for_user?(activity, user) do
387 |> put_view(StatusView)
388 |> try_render("status.json", %{activity: activity, for: user})
392 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
393 with %Activity{} = activity <- Activity.get_by_id(id),
395 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
396 "blocking_user" => user,
400 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
402 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
403 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
409 activities: grouped_activities[true] || [],
413 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
418 activities: grouped_activities[false] || [],
422 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
429 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
430 with %Object{} = object <- Object.get_by_id(id),
431 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
432 true <- Visibility.visible_for_user?(activity, user) do
434 |> put_view(StatusView)
435 |> try_render("poll.json", %{object: object, for: user})
440 |> json(%{error: "Record not found"})
445 |> json(%{error: "Record not found"})
449 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
450 with %Object{} = object <- Object.get_by_id(id),
451 true <- object.data["type"] == "Question",
452 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
453 true <- Visibility.visible_for_user?(activity, user),
454 {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
456 |> put_view(StatusView)
457 |> try_render("poll.json", %{object: object, for: user})
462 |> json(%{error: "Record not found"})
467 |> json(%{error: "Record not found"})
472 |> json(%{error: message})
476 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
477 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
479 |> add_link_headers(:scheduled_statuses, scheduled_activities)
480 |> put_view(ScheduledActivityView)
481 |> render("index.json", %{scheduled_activities: scheduled_activities})
485 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
486 with %ScheduledActivity{} = scheduled_activity <-
487 ScheduledActivity.get(user, scheduled_activity_id) do
489 |> put_view(ScheduledActivityView)
490 |> render("show.json", %{scheduled_activity: scheduled_activity})
492 _ -> {:error, :not_found}
496 def update_scheduled_status(
497 %{assigns: %{user: user}} = conn,
498 %{"id" => scheduled_activity_id} = params
500 with %ScheduledActivity{} = scheduled_activity <-
501 ScheduledActivity.get(user, scheduled_activity_id),
502 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
504 |> put_view(ScheduledActivityView)
505 |> render("show.json", %{scheduled_activity: scheduled_activity})
507 nil -> {:error, :not_found}
512 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
513 with %ScheduledActivity{} = scheduled_activity <-
514 ScheduledActivity.get(user, scheduled_activity_id),
515 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
517 |> put_view(ScheduledActivityView)
518 |> render("show.json", %{scheduled_activity: scheduled_activity})
520 nil -> {:error, :not_found}
525 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
526 when length(media_ids) > 0 do
529 |> Map.put("status", ".")
531 post_status(conn, params)
534 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
537 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
539 scheduled_at = params["scheduled_at"]
541 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
542 with {:ok, scheduled_activity} <-
543 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
545 |> put_view(ScheduledActivityView)
546 |> render("show.json", %{scheduled_activity: scheduled_activity})
549 params = Map.drop(params, ["scheduled_at"])
551 case get_cached_status_or_post(conn, params) do
552 {:ignore, message} ->
555 |> json(%{error: message})
560 |> json(%{error: message})
564 |> put_view(StatusView)
565 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
570 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
572 case get_req_header(conn, "idempotency-key") do
574 _ -> Ecto.UUID.generate()
577 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
578 case CommonAPI.post(user, params) do
579 {:ok, activity} -> activity
580 {:error, message} -> {:ignore, message}
585 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
586 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
592 |> json(%{error: "Can't delete this post"})
596 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
597 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
598 %Activity{} = announce <- Activity.normalize(announce.data) do
600 |> put_view(StatusView)
601 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
605 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
606 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
607 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
609 |> put_view(StatusView)
610 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
614 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
615 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
616 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
618 |> put_view(StatusView)
619 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
623 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
624 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
625 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
627 |> put_view(StatusView)
628 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
632 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
633 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
635 |> put_view(StatusView)
636 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
640 |> put_resp_content_type("application/json")
641 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
645 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
646 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
648 |> put_view(StatusView)
649 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
653 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
654 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
655 %User{} = user <- User.get_cached_by_nickname(user.nickname),
656 true <- Visibility.visible_for_user?(activity, user),
657 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
659 |> put_view(StatusView)
660 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
664 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
665 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
666 %User{} = user <- User.get_cached_by_nickname(user.nickname),
667 true <- Visibility.visible_for_user?(activity, user),
668 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
670 |> put_view(StatusView)
671 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
675 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
676 activity = Activity.get_by_id(id)
678 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
680 |> put_view(StatusView)
681 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
685 |> put_resp_content_type("application/json")
686 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
690 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
691 activity = Activity.get_by_id(id)
693 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
695 |> put_view(StatusView)
696 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
700 def notifications(%{assigns: %{user: user}} = conn, params) do
701 notifications = MastodonAPI.get_notifications(user, params)
704 |> add_link_headers(:notifications, notifications)
705 |> put_view(NotificationView)
706 |> render("index.json", %{notifications: notifications, for: user})
709 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
710 with {:ok, notification} <- Notification.get(user, id) do
712 |> put_view(NotificationView)
713 |> render("show.json", %{notification: notification, for: user})
717 |> put_resp_content_type("application/json")
718 |> send_resp(403, Jason.encode!(%{"error" => reason}))
722 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
723 Notification.clear(user)
727 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
728 with {:ok, _notif} <- Notification.dismiss(user, id) do
733 |> put_resp_content_type("application/json")
734 |> send_resp(403, Jason.encode!(%{"error" => reason}))
738 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
739 Notification.destroy_multiple(user, ids)
743 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
745 q = from(u in User, where: u.id in ^id)
746 targets = Repo.all(q)
749 |> put_view(AccountView)
750 |> render("relationships.json", %{user: user, targets: targets})
753 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
754 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
756 def update_media(%{assigns: %{user: user}} = conn, data) do
757 with %Object{} = object <- Repo.get(Object, data["id"]),
758 true <- Object.authorize_mutation(object, user),
759 true <- is_binary(data["description"]),
760 description <- data["description"] do
761 new_data = %{object.data | "name" => description}
765 |> Object.change(%{data: new_data})
768 attachment_data = Map.put(new_data, "id", object.id)
771 |> put_view(StatusView)
772 |> render("attachment.json", %{attachment: attachment_data})
776 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
777 with {:ok, object} <-
780 actor: User.ap_id(user),
781 description: Map.get(data, "description")
783 attachment_data = Map.put(object.data, "id", object.id)
786 |> put_view(StatusView)
787 |> render("attachment.json", %{attachment: attachment_data})
791 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
792 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
793 %{} = attachment_data <- Map.put(object.data, "id", object.id),
794 %{type: type} = rendered <-
795 StatusView.render("attachment.json", %{attachment: attachment_data}) do
796 # Reject if not an image
797 if type == "image" do
799 # Save to the user's info
800 info_changeset = User.Info.mascot_update(user.info, rendered)
804 |> Ecto.Changeset.change()
805 |> Ecto.Changeset.put_embed(:info, info_changeset)
807 {:ok, _user} = User.update_and_set_cache(user_changeset)
813 |> put_resp_content_type("application/json")
814 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
819 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
820 mascot = User.get_mascot(user)
826 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
827 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
828 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
829 q = from(u in User, where: u.ap_id in ^likes)
833 |> put_view(AccountView)
834 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
840 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
841 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
842 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
843 q = from(u in User, where: u.ap_id in ^announces)
847 |> put_view(AccountView)
848 |> render("accounts.json", %{for: user, users: users, as: :user})
854 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
855 local_only = params["local"] in [true, "True", "true", "1"]
858 [params["tag"], params["any"]]
862 |> Enum.map(&String.downcase(&1))
867 |> Enum.map(&String.downcase(&1))
872 |> Enum.map(&String.downcase(&1))
876 |> Map.put("type", "Create")
877 |> Map.put("local_only", local_only)
878 |> Map.put("blocking_user", user)
879 |> Map.put("muting_user", user)
880 |> Map.put("tag", tags)
881 |> Map.put("tag_all", tag_all)
882 |> Map.put("tag_reject", tag_reject)
883 |> ActivityPub.fetch_public_activities()
887 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
888 |> put_view(StatusView)
889 |> render("index.json", %{activities: activities, for: user, as: :activity})
892 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
893 with %User{} = user <- User.get_cached_by_id(id),
894 followers <- MastodonAPI.get_followers(user, params) do
897 for_user && user.id == for_user.id -> followers
898 user.info.hide_followers -> []
903 |> add_link_headers(:followers, followers, user)
904 |> put_view(AccountView)
905 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
909 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
910 with %User{} = user <- User.get_cached_by_id(id),
911 followers <- MastodonAPI.get_friends(user, params) do
914 for_user && user.id == for_user.id -> followers
915 user.info.hide_follows -> []
920 |> add_link_headers(:following, followers, user)
921 |> put_view(AccountView)
922 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
926 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
927 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
929 |> put_view(AccountView)
930 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
934 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
935 with %User{} = follower <- User.get_cached_by_id(id),
936 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: followed, target: follower})
943 |> put_resp_content_type("application/json")
944 |> send_resp(403, Jason.encode!(%{"error" => message}))
948 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
949 with %User{} = follower <- User.get_cached_by_id(id),
950 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
952 |> put_view(AccountView)
953 |> render("relationship.json", %{user: followed, target: follower})
957 |> put_resp_content_type("application/json")
958 |> send_resp(403, Jason.encode!(%{"error" => message}))
962 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
963 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
964 {_, true} <- {:followed, follower.id != followed.id},
965 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
967 |> put_view(AccountView)
968 |> render("relationship.json", %{user: follower, target: followed})
975 |> put_resp_content_type("application/json")
976 |> send_resp(403, Jason.encode!(%{"error" => message}))
980 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
981 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
982 {_, true} <- {:followed, follower.id != followed.id},
983 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
985 |> put_view(AccountView)
986 |> render("account.json", %{user: followed, for: follower})
993 |> put_resp_content_type("application/json")
994 |> send_resp(403, Jason.encode!(%{"error" => message}))
998 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
999 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1000 {_, true} <- {:followed, follower.id != followed.id},
1001 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1003 |> put_view(AccountView)
1004 |> render("relationship.json", %{user: follower, target: followed})
1007 {:error, :not_found}
1014 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1015 with %User{} = muted <- User.get_cached_by_id(id),
1016 {:ok, muter} <- User.mute(muter, muted) do
1018 |> put_view(AccountView)
1019 |> render("relationship.json", %{user: muter, target: muted})
1021 {:error, message} ->
1023 |> put_resp_content_type("application/json")
1024 |> send_resp(403, Jason.encode!(%{"error" => message}))
1028 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1029 with %User{} = muted <- User.get_cached_by_id(id),
1030 {:ok, muter} <- User.unmute(muter, muted) do
1032 |> put_view(AccountView)
1033 |> render("relationship.json", %{user: muter, target: muted})
1035 {:error, message} ->
1037 |> put_resp_content_type("application/json")
1038 |> send_resp(403, Jason.encode!(%{"error" => message}))
1042 def mutes(%{assigns: %{user: user}} = conn, _) do
1043 with muted_accounts <- User.muted_users(user) do
1044 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1049 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1050 with %User{} = blocked <- User.get_cached_by_id(id),
1051 {:ok, blocker} <- User.block(blocker, blocked),
1052 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1054 |> put_view(AccountView)
1055 |> render("relationship.json", %{user: blocker, target: blocked})
1057 {:error, message} ->
1059 |> put_resp_content_type("application/json")
1060 |> send_resp(403, Jason.encode!(%{"error" => message}))
1064 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1065 with %User{} = blocked <- User.get_cached_by_id(id),
1066 {:ok, blocker} <- User.unblock(blocker, blocked),
1067 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1069 |> put_view(AccountView)
1070 |> render("relationship.json", %{user: blocker, target: blocked})
1072 {:error, message} ->
1074 |> put_resp_content_type("application/json")
1075 |> send_resp(403, Jason.encode!(%{"error" => message}))
1079 def blocks(%{assigns: %{user: user}} = conn, _) do
1080 with blocked_accounts <- User.blocked_users(user) do
1081 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1086 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1087 json(conn, info.domain_blocks || [])
1090 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1091 User.block_domain(blocker, domain)
1095 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1096 User.unblock_domain(blocker, domain)
1100 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1101 with %User{} = subscription_target <- User.get_cached_by_id(id),
1102 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1104 |> put_view(AccountView)
1105 |> render("relationship.json", %{user: user, target: subscription_target})
1107 {:error, message} ->
1109 |> put_resp_content_type("application/json")
1110 |> send_resp(403, Jason.encode!(%{"error" => message}))
1114 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1115 with %User{} = subscription_target <- User.get_cached_by_id(id),
1116 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1118 |> put_view(AccountView)
1119 |> render("relationship.json", %{user: user, target: subscription_target})
1121 {:error, message} ->
1123 |> put_resp_content_type("application/json")
1124 |> send_resp(403, Jason.encode!(%{"error" => message}))
1128 def status_search_query_with_gin(q, query) do
1132 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1136 order_by: [desc: :id]
1140 def status_search_query_with_rum(q, query) do
1144 "? @@ plainto_tsquery('english', ?)",
1148 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1152 def status_search(user, query) do
1154 if Regex.match?(~r/https?:/, query) do
1155 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1156 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1157 true <- Visibility.visible_for_user?(activity, user) do
1165 from([a, o] in Activity.with_preloaded_object(Activity),
1166 where: fragment("?->>'type' = 'Create'", a.data),
1167 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1172 if Pleroma.Config.get([:database, :rum_enabled]) do
1173 status_search_query_with_rum(q, query)
1175 status_search_query_with_gin(q, query)
1178 Repo.all(q) ++ fetched
1181 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1182 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1184 statuses = status_search(user, query)
1186 tags_path = Web.base_url() <> "/tag/"
1192 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1193 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1194 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1197 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1199 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1206 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1207 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1209 statuses = status_search(user, query)
1215 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1216 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1219 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1221 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1228 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1229 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1231 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1236 def favourites(%{assigns: %{user: user}} = conn, params) do
1239 |> Map.put("type", "Create")
1240 |> Map.put("favorited_by", user.ap_id)
1241 |> Map.put("blocking_user", user)
1244 ActivityPub.fetch_activities([], params)
1248 |> add_link_headers(:favourites, activities)
1249 |> put_view(StatusView)
1250 |> render("index.json", %{activities: activities, for: user, as: :activity})
1253 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1254 with %User{} = user <- User.get_by_id(id),
1255 false <- user.info.hide_favorites do
1258 |> Map.put("type", "Create")
1259 |> Map.put("favorited_by", user.ap_id)
1260 |> Map.put("blocking_user", for_user)
1264 ["https://www.w3.org/ns/activitystreams#Public"] ++
1265 [for_user.ap_id | for_user.following]
1267 ["https://www.w3.org/ns/activitystreams#Public"]
1272 |> ActivityPub.fetch_activities(params)
1276 |> add_link_headers(:favourites, activities)
1277 |> put_view(StatusView)
1278 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1281 {:error, :not_found}
1286 |> json(%{error: "Can't get favorites"})
1290 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1291 user = User.get_cached_by_id(user.id)
1294 Bookmark.for_user_query(user.id)
1295 |> Pagination.fetch_paginated(params)
1299 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1302 |> add_link_headers(:bookmarks, bookmarks)
1303 |> put_view(StatusView)
1304 |> render("index.json", %{activities: activities, for: user, as: :activity})
1307 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1308 lists = Pleroma.List.for_user(user, opts)
1309 res = ListView.render("lists.json", lists: lists)
1313 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1314 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1315 res = ListView.render("list.json", list: list)
1321 |> json(%{error: "Record not found"})
1325 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1326 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1327 res = ListView.render("lists.json", lists: lists)
1331 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1332 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1333 {:ok, _list} <- Pleroma.List.delete(list) do
1341 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1342 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1343 res = ListView.render("list.json", list: list)
1348 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1350 |> Enum.each(fn account_id ->
1351 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1352 %User{} = followed <- User.get_cached_by_id(account_id) do
1353 Pleroma.List.follow(list, followed)
1360 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1362 |> Enum.each(fn account_id ->
1363 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1364 %User{} = followed <- User.get_cached_by_id(account_id) do
1365 Pleroma.List.unfollow(list, followed)
1372 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1373 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1374 {:ok, users} = Pleroma.List.get_following(list) do
1376 |> put_view(AccountView)
1377 |> render("accounts.json", %{for: user, users: users, as: :user})
1381 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1382 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1383 {:ok, list} <- Pleroma.List.rename(list, title) do
1384 res = ListView.render("list.json", list: list)
1392 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1393 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1396 |> Map.put("type", "Create")
1397 |> Map.put("blocking_user", user)
1398 |> Map.put("muting_user", user)
1400 # we must filter the following list for the user to avoid leaking statuses the user
1401 # does not actually have permission to see (for more info, peruse security issue #270).
1404 |> Enum.filter(fn x -> x in user.following end)
1405 |> ActivityPub.fetch_activities_bounded(following, params)
1409 |> put_view(StatusView)
1410 |> render("index.json", %{activities: activities, for: user, as: :activity})
1415 |> json(%{error: "Error."})
1419 def index(%{assigns: %{user: user}} = conn, _params) do
1420 token = get_session(conn, :oauth_token)
1423 mastodon_emoji = mastodonized_emoji()
1425 limit = Config.get([:instance, :limit])
1428 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1433 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1434 access_token: token,
1436 domain: Pleroma.Web.Endpoint.host(),
1439 unfollow_modal: false,
1442 auto_play_gif: false,
1443 display_sensitive_media: false,
1444 reduce_motion: false,
1445 max_toot_chars: limit,
1446 mascot: User.get_mascot(user)["url"]
1448 poll_limits: Config.get([:instance, :poll_limits]),
1450 delete_others_notice: present?(user.info.is_moderator),
1451 admin: present?(user.info.is_admin)
1455 default_privacy: user.info.default_scope,
1456 default_sensitive: false,
1457 allow_content_types: Config.get([:instance, :allowed_post_formats])
1459 media_attachments: %{
1460 accept_content_types: [
1476 user.info.settings ||
1506 push_subscription: nil,
1508 custom_emojis: mastodon_emoji,
1514 |> put_layout(false)
1515 |> put_view(MastodonView)
1516 |> render("index.html", %{initial_state: initial_state})
1519 |> put_session(:return_to, conn.request_path)
1520 |> redirect(to: "/web/login")
1524 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1525 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1527 with changeset <- Ecto.Changeset.change(user),
1528 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1529 {:ok, _user} <- User.update_and_set_cache(changeset) do
1534 |> put_resp_content_type("application/json")
1535 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1539 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1540 redirect(conn, to: local_mastodon_root_path(conn))
1543 @doc "Local Mastodon FE login init action"
1544 def login(conn, %{"code" => auth_token}) do
1545 with {:ok, app} <- get_or_make_app(),
1546 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1547 {:ok, token} <- Token.exchange_token(app, auth) do
1549 |> put_session(:oauth_token, token.token)
1550 |> redirect(to: local_mastodon_root_path(conn))
1554 @doc "Local Mastodon FE callback action"
1555 def login(conn, _) do
1556 with {:ok, app} <- get_or_make_app() do
1561 response_type: "code",
1562 client_id: app.client_id,
1564 scope: Enum.join(app.scopes, " ")
1567 redirect(conn, to: path)
1571 defp local_mastodon_root_path(conn) do
1572 case get_session(conn, :return_to) do
1574 mastodon_api_path(conn, :index, ["getting-started"])
1577 delete_session(conn, :return_to)
1582 defp get_or_make_app do
1583 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1584 scopes = ["read", "write", "follow", "push"]
1586 with %App{} = app <- Repo.get_by(App, find_attrs) do
1588 if app.scopes == scopes do
1592 |> Ecto.Changeset.change(%{scopes: scopes})
1600 App.register_changeset(
1602 Map.put(find_attrs, :scopes, scopes)
1609 def logout(conn, _) do
1612 |> redirect(to: "/")
1615 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1616 Logger.debug("Unimplemented, returning unmodified relationship")
1618 with %User{} = target <- User.get_cached_by_id(id) do
1620 |> put_view(AccountView)
1621 |> render("relationship.json", %{user: user, target: target})
1625 def empty_array(conn, _) do
1626 Logger.debug("Unimplemented, returning an empty array")
1630 def empty_object(conn, _) do
1631 Logger.debug("Unimplemented, returning an empty object")
1635 def get_filters(%{assigns: %{user: user}} = conn, _) do
1636 filters = Filter.get_filters(user)
1637 res = FilterView.render("filters.json", filters: filters)
1642 %{assigns: %{user: user}} = conn,
1643 %{"phrase" => phrase, "context" => context} = params
1649 hide: Map.get(params, "irreversible", false),
1650 whole_word: Map.get(params, "boolean", true)
1654 {:ok, response} = Filter.create(query)
1655 res = FilterView.render("filter.json", filter: response)
1659 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1660 filter = Filter.get(filter_id, user)
1661 res = FilterView.render("filter.json", filter: filter)
1666 %{assigns: %{user: user}} = conn,
1667 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1671 filter_id: filter_id,
1674 hide: Map.get(params, "irreversible", nil),
1675 whole_word: Map.get(params, "boolean", true)
1679 {:ok, response} = Filter.update(query)
1680 res = FilterView.render("filter.json", filter: response)
1684 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1687 filter_id: filter_id
1690 {:ok, _} = Filter.delete(query)
1696 def errors(conn, {:error, %Changeset{} = changeset}) do
1699 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1700 |> Enum.map_join(", ", fn {_k, v} -> v end)
1704 |> json(%{error: error_message})
1707 def errors(conn, {:error, :not_found}) do
1710 |> json(%{error: "Record not found"})
1713 def errors(conn, _) do
1716 |> json("Something went wrong")
1719 def suggestions(%{assigns: %{user: user}} = conn, _) do
1720 suggestions = Config.get(:suggestions)
1722 if Keyword.get(suggestions, :enabled, false) do
1723 api = Keyword.get(suggestions, :third_party_engine, "")
1724 timeout = Keyword.get(suggestions, :timeout, 5000)
1725 limit = Keyword.get(suggestions, :limit, 23)
1727 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1729 user = user.nickname
1733 |> String.replace("{{host}}", host)
1734 |> String.replace("{{user}}", user)
1736 with {:ok, %{status: 200, body: body}} <-
1741 recv_timeout: timeout,
1745 {:ok, data} <- Jason.decode(body) do
1748 |> Enum.slice(0, limit)
1753 case User.get_or_fetch(x["acct"]) do
1754 {:ok, %User{id: id}} -> id
1760 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1763 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1769 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1776 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1777 with %Activity{} = activity <- Activity.get_by_id(status_id),
1778 true <- Visibility.visible_for_user?(activity, user) do
1782 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1792 def reports(%{assigns: %{user: user}} = conn, params) do
1793 case CommonAPI.report(user, params) do
1796 |> put_view(ReportView)
1797 |> try_render("report.json", %{activity: activity})
1801 |> put_status(:bad_request)
1802 |> json(%{error: err})
1806 def account_register(
1807 %{assigns: %{app: app}} = conn,
1808 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1816 "captcha_answer_data",
1820 |> Map.put("nickname", nickname)
1821 |> Map.put("fullname", params["fullname"] || nickname)
1822 |> Map.put("bio", params["bio"] || "")
1823 |> Map.put("confirm", params["password"])
1825 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1826 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1828 token_type: "Bearer",
1829 access_token: token.token,
1831 created_at: Token.Utils.format_created_at(token)
1837 |> json(Jason.encode!(errors))
1841 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1844 |> json(%{error: "Missing parameters"})
1847 def account_register(conn, _) do
1850 |> json(%{error: "Invalid credentials"})
1853 def conversations(%{assigns: %{user: user}} = conn, params) do
1854 participations = Participation.for_user_with_last_activity_id(user, params)
1857 Enum.map(participations, fn participation ->
1858 ConversationView.render("participation.json", %{participation: participation, user: user})
1862 |> add_link_headers(:conversations, participations)
1863 |> json(conversations)
1866 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1867 with %Participation{} = participation <-
1868 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1869 {:ok, participation} <- Participation.mark_as_read(participation) do
1870 participation_view =
1871 ConversationView.render("participation.json", %{participation: participation, user: user})
1874 |> json(participation_view)
1878 def try_render(conn, target, params)
1879 when is_binary(target) do
1880 res = render(conn, target, params)
1885 |> json(%{error: "Can't display this activity"})
1891 def try_render(conn, _, _) do
1894 |> json(%{error: "Can't display this activity"})
1897 defp present?(nil), do: false
1898 defp present?(false), do: false
1899 defp present?(_), do: true