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))
120 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
121 |> Enum.reduce(%{}, fn key, acc ->
122 add_if_present(acc, params, to_string(key), key, fn value ->
123 {:ok, ControllerHelper.truthy_param?(value)}
126 |> add_if_present(params, "default_scope", :default_scope)
127 |> add_if_present(params, "header", :banner, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
135 |> Map.put(:emoji, user_info_emojis)
137 info_cng = User.Info.profile_update(user.info, info_params)
139 with changeset <- User.update_changeset(user, user_params),
140 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
141 {:ok, user} <- User.update_and_set_cache(changeset) do
142 if original_user != user do
143 CommonAPI.update(user)
146 json(conn, AccountView.render("account.json", %{user: user, for: user}))
151 |> json(%{error: "Invalid request"})
155 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
156 account = AccountView.render("account.json", %{user: user, for: user})
160 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
161 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
164 |> render("short.json", %{app: app})
168 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
169 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
170 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
171 account = AccountView.render("account.json", %{user: user, for: for_user})
177 |> json(%{error: "Can't find user"})
181 @mastodon_api_level "2.7.2"
183 def masto_instance(conn, _params) do
184 instance = Config.get(:instance)
188 title: Keyword.get(instance, :name),
189 description: Keyword.get(instance, :description),
190 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
191 email: Keyword.get(instance, :email),
193 streaming_api: Pleroma.Web.Endpoint.websocket_url()
195 stats: Stats.get_stats(),
196 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
198 registrations: Pleroma.Config.get([:instance, :registrations_open]),
199 # Extra (not present in Mastodon):
200 max_toot_chars: Keyword.get(instance, :limit),
201 poll_limits: Keyword.get(instance, :poll_limits)
207 def peers(conn, _params) do
208 json(conn, Stats.get_peers())
211 defp mastodonized_emoji do
212 Pleroma.Emoji.get_all()
213 |> Enum.map(fn {shortcode, relative_url, tags} ->
214 url = to_string(URI.merge(Web.base_url(), relative_url))
217 "shortcode" => shortcode,
219 "visible_in_picker" => true,
226 def custom_emojis(conn, _params) do
227 mastodon_emoji = mastodonized_emoji()
228 json(conn, mastodon_emoji)
231 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
234 |> Map.drop(["since_id", "max_id", "min_id"])
237 last = List.last(activities)
244 |> Map.get("limit", "20")
245 |> String.to_integer()
248 if length(activities) <= limit do
254 |> Enum.at(limit * -1)
258 {next_url, prev_url} =
262 Pleroma.Web.Endpoint,
265 Map.merge(params, %{max_id: max_id})
268 Pleroma.Web.Endpoint,
271 Map.merge(params, %{min_id: min_id})
277 Pleroma.Web.Endpoint,
279 Map.merge(params, %{max_id: max_id})
282 Pleroma.Web.Endpoint,
284 Map.merge(params, %{min_id: min_id})
290 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
296 def home_timeline(%{assigns: %{user: user}} = conn, params) do
299 |> Map.put("type", ["Create", "Announce"])
300 |> Map.put("blocking_user", user)
301 |> Map.put("muting_user", user)
302 |> Map.put("user", user)
305 [user.ap_id | user.following]
306 |> ActivityPub.fetch_activities(params)
310 |> add_link_headers(:home_timeline, activities)
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
315 def public_timeline(%{assigns: %{user: user}} = conn, params) do
316 local_only = params["local"] in [true, "True", "true", "1"]
320 |> Map.put("type", ["Create", "Announce"])
321 |> Map.put("local_only", local_only)
322 |> Map.put("blocking_user", user)
323 |> Map.put("muting_user", user)
324 |> ActivityPub.fetch_public_activities()
328 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
329 |> put_view(StatusView)
330 |> render("index.json", %{activities: activities, for: user, as: :activity})
333 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
334 with %User{} = user <- User.get_cached_by_id(params["id"]) do
335 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
338 |> add_link_headers(:user_statuses, activities, params["id"])
339 |> put_view(StatusView)
340 |> render("index.json", %{
341 activities: activities,
348 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
351 |> Map.put("type", "Create")
352 |> Map.put("blocking_user", user)
353 |> Map.put("user", user)
354 |> Map.put(:visibility, "direct")
358 |> ActivityPub.fetch_activities_query(params)
359 |> Pagination.fetch_paginated(params)
362 |> add_link_headers(:dm_timeline, activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
367 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
368 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
369 true <- Visibility.visible_for_user?(activity, user) do
371 |> put_view(StatusView)
372 |> try_render("status.json", %{activity: activity, for: user})
376 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with %Activity{} = activity <- Activity.get_by_id(id),
379 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
380 "blocking_user" => user,
384 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
386 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
387 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
393 activities: grouped_activities[true] || [],
397 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
402 activities: grouped_activities[false] || [],
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
413 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
414 with %Object{} = object <- Object.get_by_id(id),
415 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
416 true <- Visibility.visible_for_user?(activity, user) do
418 |> put_view(StatusView)
419 |> try_render("poll.json", %{object: object, for: user})
424 |> json(%{error: "Record not found"})
429 |> json(%{error: "Record not found"})
433 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
434 with %Object{} = object <- Object.get_by_id(id),
435 true <- object.data["type"] == "Question",
436 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
437 true <- Visibility.visible_for_user?(activity, user),
438 {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
440 |> put_view(StatusView)
441 |> try_render("poll.json", %{object: object, for: user})
446 |> json(%{error: "Record not found"})
451 |> json(%{error: "Record not found"})
456 |> json(%{error: message})
460 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
461 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
463 |> add_link_headers(:scheduled_statuses, scheduled_activities)
464 |> put_view(ScheduledActivityView)
465 |> render("index.json", %{scheduled_activities: scheduled_activities})
469 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
470 with %ScheduledActivity{} = scheduled_activity <-
471 ScheduledActivity.get(user, scheduled_activity_id) do
473 |> put_view(ScheduledActivityView)
474 |> render("show.json", %{scheduled_activity: scheduled_activity})
476 _ -> {:error, :not_found}
480 def update_scheduled_status(
481 %{assigns: %{user: user}} = conn,
482 %{"id" => scheduled_activity_id} = params
484 with %ScheduledActivity{} = scheduled_activity <-
485 ScheduledActivity.get(user, scheduled_activity_id),
486 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
488 |> put_view(ScheduledActivityView)
489 |> render("show.json", %{scheduled_activity: scheduled_activity})
491 nil -> {:error, :not_found}
496 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
497 with %ScheduledActivity{} = scheduled_activity <-
498 ScheduledActivity.get(user, scheduled_activity_id),
499 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
501 |> put_view(ScheduledActivityView)
502 |> render("show.json", %{scheduled_activity: scheduled_activity})
504 nil -> {:error, :not_found}
509 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
510 when length(media_ids) > 0 do
513 |> Map.put("status", ".")
515 post_status(conn, params)
518 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
521 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
523 scheduled_at = params["scheduled_at"]
525 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
526 with {:ok, scheduled_activity} <-
527 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
529 |> put_view(ScheduledActivityView)
530 |> render("show.json", %{scheduled_activity: scheduled_activity})
533 params = Map.drop(params, ["scheduled_at"])
535 case get_cached_status_or_post(conn, params) do
536 {:ignore, message} ->
539 |> json(%{error: message})
544 |> json(%{error: message})
548 |> put_view(StatusView)
549 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
554 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
556 case get_req_header(conn, "idempotency-key") do
558 _ -> Ecto.UUID.generate()
561 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
562 case CommonAPI.post(user, params) do
563 {:ok, activity} -> activity
564 {:error, message} -> {:ignore, message}
569 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
570 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
576 |> json(%{error: "Can't delete this post"})
580 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
581 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
582 %Activity{} = announce <- Activity.normalize(announce.data) do
584 |> put_view(StatusView)
585 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
589 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
590 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
591 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
593 |> put_view(StatusView)
594 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
598 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
599 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
600 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
602 |> put_view(StatusView)
603 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
607 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
608 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(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 pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
617 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
619 |> put_view(StatusView)
620 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
624 |> put_resp_content_type("application/json")
625 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
629 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
630 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
632 |> put_view(StatusView)
633 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
637 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
638 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
639 %User{} = user <- User.get_cached_by_nickname(user.nickname),
640 true <- Visibility.visible_for_user?(activity, user),
641 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
643 |> put_view(StatusView)
644 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
648 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
649 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
650 %User{} = user <- User.get_cached_by_nickname(user.nickname),
651 true <- Visibility.visible_for_user?(activity, user),
652 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
654 |> put_view(StatusView)
655 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
659 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
660 activity = Activity.get_by_id(id)
662 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
664 |> put_view(StatusView)
665 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
669 |> put_resp_content_type("application/json")
670 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
674 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
675 activity = Activity.get_by_id(id)
677 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
679 |> put_view(StatusView)
680 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
684 def notifications(%{assigns: %{user: user}} = conn, params) do
685 notifications = MastodonAPI.get_notifications(user, params)
688 |> add_link_headers(:notifications, notifications)
689 |> put_view(NotificationView)
690 |> render("index.json", %{notifications: notifications, for: user})
693 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
694 with {:ok, notification} <- Notification.get(user, id) do
696 |> put_view(NotificationView)
697 |> render("show.json", %{notification: notification, for: user})
701 |> put_resp_content_type("application/json")
702 |> send_resp(403, Jason.encode!(%{"error" => reason}))
706 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
707 Notification.clear(user)
711 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
712 with {:ok, _notif} <- Notification.dismiss(user, id) do
717 |> put_resp_content_type("application/json")
718 |> send_resp(403, Jason.encode!(%{"error" => reason}))
722 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
723 Notification.destroy_multiple(user, ids)
727 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
729 q = from(u in User, where: u.id in ^id)
730 targets = Repo.all(q)
733 |> put_view(AccountView)
734 |> render("relationships.json", %{user: user, targets: targets})
737 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
738 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
740 def update_media(%{assigns: %{user: user}} = conn, data) do
741 with %Object{} = object <- Repo.get(Object, data["id"]),
742 true <- Object.authorize_mutation(object, user),
743 true <- is_binary(data["description"]),
744 description <- data["description"] do
745 new_data = %{object.data | "name" => description}
749 |> Object.change(%{data: new_data})
752 attachment_data = Map.put(new_data, "id", object.id)
755 |> put_view(StatusView)
756 |> render("attachment.json", %{attachment: attachment_data})
760 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
761 with {:ok, object} <-
764 actor: User.ap_id(user),
765 description: Map.get(data, "description")
767 attachment_data = Map.put(object.data, "id", object.id)
770 |> put_view(StatusView)
771 |> render("attachment.json", %{attachment: attachment_data})
775 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
776 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
777 %{} = attachment_data <- Map.put(object.data, "id", object.id),
778 %{type: type} = rendered <-
779 StatusView.render("attachment.json", %{attachment: attachment_data}) do
780 # Reject if not an image
781 if type == "image" do
783 # Save to the user's info
784 info_changeset = User.Info.mascot_update(user.info, rendered)
788 |> Ecto.Changeset.change()
789 |> Ecto.Changeset.put_embed(:info, info_changeset)
791 {:ok, _user} = User.update_and_set_cache(user_changeset)
797 |> put_resp_content_type("application/json")
798 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
803 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
804 mascot = User.get_mascot(user)
810 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
811 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
812 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
813 q = from(u in User, where: u.ap_id in ^likes)
817 |> put_view(AccountView)
818 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
824 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
825 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
826 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
827 q = from(u in User, where: u.ap_id in ^announces)
831 |> put_view(AccountView)
832 |> render("accounts.json", %{for: user, users: users, as: :user})
838 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
839 local_only = params["local"] in [true, "True", "true", "1"]
842 [params["tag"], params["any"]]
846 |> Enum.map(&String.downcase(&1))
851 |> Enum.map(&String.downcase(&1))
856 |> Enum.map(&String.downcase(&1))
860 |> Map.put("type", "Create")
861 |> Map.put("local_only", local_only)
862 |> Map.put("blocking_user", user)
863 |> Map.put("muting_user", user)
864 |> Map.put("tag", tags)
865 |> Map.put("tag_all", tag_all)
866 |> Map.put("tag_reject", tag_reject)
867 |> ActivityPub.fetch_public_activities()
871 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
872 |> put_view(StatusView)
873 |> render("index.json", %{activities: activities, for: user, as: :activity})
876 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
877 with %User{} = user <- User.get_cached_by_id(id),
878 followers <- MastodonAPI.get_followers(user, params) do
881 for_user && user.id == for_user.id -> followers
882 user.info.hide_followers -> []
887 |> add_link_headers(:followers, followers, user)
888 |> put_view(AccountView)
889 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
893 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
894 with %User{} = user <- User.get_cached_by_id(id),
895 followers <- MastodonAPI.get_friends(user, params) do
898 for_user && user.id == for_user.id -> followers
899 user.info.hide_follows -> []
904 |> add_link_headers(:following, followers, user)
905 |> put_view(AccountView)
906 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
910 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
911 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
913 |> put_view(AccountView)
914 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
918 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
919 with %User{} = follower <- User.get_cached_by_id(id),
920 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
922 |> put_view(AccountView)
923 |> render("relationship.json", %{user: followed, target: follower})
927 |> put_resp_content_type("application/json")
928 |> send_resp(403, Jason.encode!(%{"error" => message}))
932 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
933 with %User{} = follower <- User.get_cached_by_id(id),
934 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
936 |> put_view(AccountView)
937 |> render("relationship.json", %{user: followed, target: follower})
941 |> put_resp_content_type("application/json")
942 |> send_resp(403, Jason.encode!(%{"error" => message}))
946 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
947 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
948 {_, true} <- {:followed, follower.id != followed.id},
949 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
951 |> put_view(AccountView)
952 |> render("relationship.json", %{user: follower, target: followed})
959 |> put_resp_content_type("application/json")
960 |> send_resp(403, Jason.encode!(%{"error" => message}))
964 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
965 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
966 {_, true} <- {:followed, follower.id != followed.id},
967 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
969 |> put_view(AccountView)
970 |> render("account.json", %{user: followed, for: follower})
977 |> put_resp_content_type("application/json")
978 |> send_resp(403, Jason.encode!(%{"error" => message}))
982 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
983 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
984 {_, true} <- {:followed, follower.id != followed.id},
985 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
987 |> put_view(AccountView)
988 |> render("relationship.json", %{user: follower, target: followed})
998 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
999 with %User{} = muted <- User.get_cached_by_id(id),
1000 {:ok, muter} <- User.mute(muter, muted) do
1002 |> put_view(AccountView)
1003 |> render("relationship.json", %{user: muter, target: muted})
1005 {:error, message} ->
1007 |> put_resp_content_type("application/json")
1008 |> send_resp(403, Jason.encode!(%{"error" => message}))
1012 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1013 with %User{} = muted <- User.get_cached_by_id(id),
1014 {:ok, muter} <- User.unmute(muter, muted) do
1016 |> put_view(AccountView)
1017 |> render("relationship.json", %{user: muter, target: muted})
1019 {:error, message} ->
1021 |> put_resp_content_type("application/json")
1022 |> send_resp(403, Jason.encode!(%{"error" => message}))
1026 def mutes(%{assigns: %{user: user}} = conn, _) do
1027 with muted_accounts <- User.muted_users(user) do
1028 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1033 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1034 with %User{} = blocked <- User.get_cached_by_id(id),
1035 {:ok, blocker} <- User.block(blocker, blocked),
1036 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1038 |> put_view(AccountView)
1039 |> render("relationship.json", %{user: blocker, target: blocked})
1041 {:error, message} ->
1043 |> put_resp_content_type("application/json")
1044 |> send_resp(403, Jason.encode!(%{"error" => message}))
1048 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1049 with %User{} = blocked <- User.get_cached_by_id(id),
1050 {:ok, blocker} <- User.unblock(blocker, blocked),
1051 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1053 |> put_view(AccountView)
1054 |> render("relationship.json", %{user: blocker, target: blocked})
1056 {:error, message} ->
1058 |> put_resp_content_type("application/json")
1059 |> send_resp(403, Jason.encode!(%{"error" => message}))
1063 def blocks(%{assigns: %{user: user}} = conn, _) do
1064 with blocked_accounts <- User.blocked_users(user) do
1065 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1070 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1071 json(conn, info.domain_blocks || [])
1074 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1075 User.block_domain(blocker, domain)
1079 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1080 User.unblock_domain(blocker, domain)
1084 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1085 with %User{} = subscription_target <- User.get_cached_by_id(id),
1086 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1088 |> put_view(AccountView)
1089 |> render("relationship.json", %{user: user, target: subscription_target})
1091 {:error, message} ->
1093 |> put_resp_content_type("application/json")
1094 |> send_resp(403, Jason.encode!(%{"error" => message}))
1098 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1099 with %User{} = subscription_target <- User.get_cached_by_id(id),
1100 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1102 |> put_view(AccountView)
1103 |> render("relationship.json", %{user: user, target: subscription_target})
1105 {:error, message} ->
1107 |> put_resp_content_type("application/json")
1108 |> send_resp(403, Jason.encode!(%{"error" => message}))
1112 def status_search_query_with_gin(q, query) do
1116 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1120 order_by: [desc: :id]
1124 def status_search_query_with_rum(q, query) do
1128 "? @@ plainto_tsquery('english', ?)",
1132 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1136 def status_search(user, query) do
1138 if Regex.match?(~r/https?:/, query) do
1139 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1140 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1141 true <- Visibility.visible_for_user?(activity, user) do
1149 from([a, o] in Activity.with_preloaded_object(Activity),
1150 where: fragment("?->>'type' = 'Create'", a.data),
1151 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1156 if Pleroma.Config.get([:database, :rum_enabled]) do
1157 status_search_query_with_rum(q, query)
1159 status_search_query_with_gin(q, query)
1162 Repo.all(q) ++ fetched
1165 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1166 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1168 statuses = status_search(user, query)
1170 tags_path = Web.base_url() <> "/tag/"
1176 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1177 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1178 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1181 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1183 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1190 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1191 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1193 statuses = status_search(user, query)
1199 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1200 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1203 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1205 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1212 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1213 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1215 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1220 def favourites(%{assigns: %{user: user}} = conn, params) do
1223 |> Map.put("type", "Create")
1224 |> Map.put("favorited_by", user.ap_id)
1225 |> Map.put("blocking_user", user)
1228 ActivityPub.fetch_activities([], params)
1232 |> add_link_headers(:favourites, activities)
1233 |> put_view(StatusView)
1234 |> render("index.json", %{activities: activities, for: user, as: :activity})
1237 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1238 with %User{} = user <- User.get_by_id(id),
1239 false <- user.info.hide_favorites do
1242 |> Map.put("type", "Create")
1243 |> Map.put("favorited_by", user.ap_id)
1244 |> Map.put("blocking_user", for_user)
1248 ["https://www.w3.org/ns/activitystreams#Public"] ++
1249 [for_user.ap_id | for_user.following]
1251 ["https://www.w3.org/ns/activitystreams#Public"]
1256 |> ActivityPub.fetch_activities(params)
1260 |> add_link_headers(:favourites, activities)
1261 |> put_view(StatusView)
1262 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1265 {:error, :not_found}
1270 |> json(%{error: "Can't get favorites"})
1274 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1275 user = User.get_cached_by_id(user.id)
1278 Bookmark.for_user_query(user.id)
1279 |> Pagination.fetch_paginated(params)
1283 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1286 |> add_link_headers(:bookmarks, bookmarks)
1287 |> put_view(StatusView)
1288 |> render("index.json", %{activities: activities, for: user, as: :activity})
1291 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1292 lists = Pleroma.List.for_user(user, opts)
1293 res = ListView.render("lists.json", lists: lists)
1297 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1298 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1299 res = ListView.render("list.json", list: list)
1305 |> json(%{error: "Record not found"})
1309 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1310 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1311 res = ListView.render("lists.json", lists: lists)
1315 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1316 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1317 {:ok, _list} <- Pleroma.List.delete(list) do
1325 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1326 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1327 res = ListView.render("list.json", list: list)
1332 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1334 |> Enum.each(fn account_id ->
1335 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1336 %User{} = followed <- User.get_cached_by_id(account_id) do
1337 Pleroma.List.follow(list, followed)
1344 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1346 |> Enum.each(fn account_id ->
1347 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1348 %User{} = followed <- User.get_cached_by_id(account_id) do
1349 Pleroma.List.unfollow(list, followed)
1356 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1357 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1358 {:ok, users} = Pleroma.List.get_following(list) do
1360 |> put_view(AccountView)
1361 |> render("accounts.json", %{for: user, users: users, as: :user})
1365 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1366 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1367 {:ok, list} <- Pleroma.List.rename(list, title) do
1368 res = ListView.render("list.json", list: list)
1376 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1377 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1380 |> Map.put("type", "Create")
1381 |> Map.put("blocking_user", user)
1382 |> Map.put("muting_user", user)
1384 # we must filter the following list for the user to avoid leaking statuses the user
1385 # does not actually have permission to see (for more info, peruse security issue #270).
1388 |> Enum.filter(fn x -> x in user.following end)
1389 |> ActivityPub.fetch_activities_bounded(following, params)
1393 |> put_view(StatusView)
1394 |> render("index.json", %{activities: activities, for: user, as: :activity})
1399 |> json(%{error: "Error."})
1403 def index(%{assigns: %{user: user}} = conn, _params) do
1404 token = get_session(conn, :oauth_token)
1407 mastodon_emoji = mastodonized_emoji()
1409 limit = Config.get([:instance, :limit])
1412 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1417 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1418 access_token: token,
1420 domain: Pleroma.Web.Endpoint.host(),
1423 unfollow_modal: false,
1426 auto_play_gif: false,
1427 display_sensitive_media: false,
1428 reduce_motion: false,
1429 max_toot_chars: limit,
1430 mascot: User.get_mascot(user)["url"]
1432 poll_limits: Config.get([:instance, :poll_limits]),
1434 delete_others_notice: present?(user.info.is_moderator),
1435 admin: present?(user.info.is_admin)
1439 default_privacy: user.info.default_scope,
1440 default_sensitive: false,
1441 allow_content_types: Config.get([:instance, :allowed_post_formats])
1443 media_attachments: %{
1444 accept_content_types: [
1460 user.info.settings ||
1490 push_subscription: nil,
1492 custom_emojis: mastodon_emoji,
1498 |> put_layout(false)
1499 |> put_view(MastodonView)
1500 |> render("index.html", %{initial_state: initial_state})
1503 |> put_session(:return_to, conn.request_path)
1504 |> redirect(to: "/web/login")
1508 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1509 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1511 with changeset <- Ecto.Changeset.change(user),
1512 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1513 {:ok, _user} <- User.update_and_set_cache(changeset) do
1518 |> put_resp_content_type("application/json")
1519 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1523 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1524 redirect(conn, to: local_mastodon_root_path(conn))
1527 @doc "Local Mastodon FE login init action"
1528 def login(conn, %{"code" => auth_token}) do
1529 with {:ok, app} <- get_or_make_app(),
1530 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1531 {:ok, token} <- Token.exchange_token(app, auth) do
1533 |> put_session(:oauth_token, token.token)
1534 |> redirect(to: local_mastodon_root_path(conn))
1538 @doc "Local Mastodon FE callback action"
1539 def login(conn, _) do
1540 with {:ok, app} <- get_or_make_app() do
1545 response_type: "code",
1546 client_id: app.client_id,
1548 scope: Enum.join(app.scopes, " ")
1551 redirect(conn, to: path)
1555 defp local_mastodon_root_path(conn) do
1556 case get_session(conn, :return_to) do
1558 mastodon_api_path(conn, :index, ["getting-started"])
1561 delete_session(conn, :return_to)
1566 defp get_or_make_app do
1567 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1568 scopes = ["read", "write", "follow", "push"]
1570 with %App{} = app <- Repo.get_by(App, find_attrs) do
1572 if app.scopes == scopes do
1576 |> Ecto.Changeset.change(%{scopes: scopes})
1584 App.register_changeset(
1586 Map.put(find_attrs, :scopes, scopes)
1593 def logout(conn, _) do
1596 |> redirect(to: "/")
1599 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1600 Logger.debug("Unimplemented, returning unmodified relationship")
1602 with %User{} = target <- User.get_cached_by_id(id) do
1604 |> put_view(AccountView)
1605 |> render("relationship.json", %{user: user, target: target})
1609 def empty_array(conn, _) do
1610 Logger.debug("Unimplemented, returning an empty array")
1614 def empty_object(conn, _) do
1615 Logger.debug("Unimplemented, returning an empty object")
1619 def get_filters(%{assigns: %{user: user}} = conn, _) do
1620 filters = Filter.get_filters(user)
1621 res = FilterView.render("filters.json", filters: filters)
1626 %{assigns: %{user: user}} = conn,
1627 %{"phrase" => phrase, "context" => context} = params
1633 hide: Map.get(params, "irreversible", false),
1634 whole_word: Map.get(params, "boolean", true)
1638 {:ok, response} = Filter.create(query)
1639 res = FilterView.render("filter.json", filter: response)
1643 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1644 filter = Filter.get(filter_id, user)
1645 res = FilterView.render("filter.json", filter: filter)
1650 %{assigns: %{user: user}} = conn,
1651 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1655 filter_id: filter_id,
1658 hide: Map.get(params, "irreversible", nil),
1659 whole_word: Map.get(params, "boolean", true)
1663 {:ok, response} = Filter.update(query)
1664 res = FilterView.render("filter.json", filter: response)
1668 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1671 filter_id: filter_id
1674 {:ok, _} = Filter.delete(query)
1680 def errors(conn, {:error, %Changeset{} = changeset}) do
1683 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1684 |> Enum.map_join(", ", fn {_k, v} -> v end)
1688 |> json(%{error: error_message})
1691 def errors(conn, {:error, :not_found}) do
1694 |> json(%{error: "Record not found"})
1697 def errors(conn, _) do
1700 |> json("Something went wrong")
1703 def suggestions(%{assigns: %{user: user}} = conn, _) do
1704 suggestions = Config.get(:suggestions)
1706 if Keyword.get(suggestions, :enabled, false) do
1707 api = Keyword.get(suggestions, :third_party_engine, "")
1708 timeout = Keyword.get(suggestions, :timeout, 5000)
1709 limit = Keyword.get(suggestions, :limit, 23)
1711 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1713 user = user.nickname
1717 |> String.replace("{{host}}", host)
1718 |> String.replace("{{user}}", user)
1720 with {:ok, %{status: 200, body: body}} <-
1725 recv_timeout: timeout,
1729 {:ok, data} <- Jason.decode(body) do
1732 |> Enum.slice(0, limit)
1737 case User.get_or_fetch(x["acct"]) do
1738 {:ok, %User{id: id}} -> id
1744 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1747 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1753 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1760 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1761 with %Activity{} = activity <- Activity.get_by_id(status_id),
1762 true <- Visibility.visible_for_user?(activity, user) do
1766 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1776 def reports(%{assigns: %{user: user}} = conn, params) do
1777 case CommonAPI.report(user, params) do
1780 |> put_view(ReportView)
1781 |> try_render("report.json", %{activity: activity})
1785 |> put_status(:bad_request)
1786 |> json(%{error: err})
1790 def account_register(
1791 %{assigns: %{app: app}} = conn,
1792 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1800 "captcha_answer_data",
1804 |> Map.put("nickname", nickname)
1805 |> Map.put("fullname", params["fullname"] || nickname)
1806 |> Map.put("bio", params["bio"] || "")
1807 |> Map.put("confirm", params["password"])
1809 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1810 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1812 token_type: "Bearer",
1813 access_token: token.token,
1815 created_at: Token.Utils.format_created_at(token)
1821 |> json(Jason.encode!(errors))
1825 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1828 |> json(%{error: "Missing parameters"})
1831 def account_register(conn, _) do
1834 |> json(%{error: "Invalid credentials"})
1837 def conversations(%{assigns: %{user: user}} = conn, params) do
1838 participations = Participation.for_user_with_last_activity_id(user, params)
1841 Enum.map(participations, fn participation ->
1842 ConversationView.render("participation.json", %{participation: participation, user: user})
1846 |> add_link_headers(:conversations, participations)
1847 |> json(conversations)
1850 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1851 with %Participation{} = participation <-
1852 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1853 {:ok, participation} <- Participation.mark_as_read(participation) do
1854 participation_view =
1855 ConversationView.render("participation.json", %{participation: participation, user: user})
1858 |> json(participation_view)
1862 def try_render(conn, target, params)
1863 when is_binary(target) do
1864 res = render(conn, target, params)
1869 |> json(%{error: "Can't display this activity"})
1875 def try_render(conn, _, _) do
1878 |> json(%{error: "Can't display this activity"})
1881 defp present?(nil), do: false
1882 defp present?(false), do: false
1883 defp present?(_), do: true