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
14 alias Pleroma.Notification
16 alias Pleroma.Object.Fetcher
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
50 Pleroma.Plugs.RateLimitPlug,
52 max_requests: Config.get([:app_account_creation, :max_requests]),
53 interval: Config.get([:app_account_creation, :interval])
55 when action in [:account_register]
58 @httpoison Application.get_env(:pleroma, :httpoison)
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 favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
776 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
777 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
778 q = from(u in User, where: u.ap_id in ^likes)
782 |> put_view(AccountView)
783 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
789 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
790 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
791 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
792 q = from(u in User, where: u.ap_id in ^announces)
796 |> put_view(AccountView)
797 |> render("accounts.json", %{for: user, users: users, as: :user})
803 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
804 local_only = params["local"] in [true, "True", "true", "1"]
807 [params["tag"], params["any"]]
811 |> Enum.map(&String.downcase(&1))
816 |> Enum.map(&String.downcase(&1))
821 |> Enum.map(&String.downcase(&1))
825 |> Map.put("type", "Create")
826 |> Map.put("local_only", local_only)
827 |> Map.put("blocking_user", user)
828 |> Map.put("muting_user", user)
829 |> Map.put("tag", tags)
830 |> Map.put("tag_all", tag_all)
831 |> Map.put("tag_reject", tag_reject)
832 |> ActivityPub.fetch_public_activities()
836 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
837 |> put_view(StatusView)
838 |> render("index.json", %{activities: activities, for: user, as: :activity})
841 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
842 with %User{} = user <- User.get_cached_by_id(id),
843 followers <- MastodonAPI.get_followers(user, params) do
846 for_user && user.id == for_user.id -> followers
847 user.info.hide_followers -> []
852 |> add_link_headers(:followers, followers, user)
853 |> put_view(AccountView)
854 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
858 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
859 with %User{} = user <- User.get_cached_by_id(id),
860 followers <- MastodonAPI.get_friends(user, params) do
863 for_user && user.id == for_user.id -> followers
864 user.info.hide_follows -> []
869 |> add_link_headers(:following, followers, user)
870 |> put_view(AccountView)
871 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
875 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
876 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
878 |> put_view(AccountView)
879 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
883 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
884 with %User{} = follower <- User.get_cached_by_id(id),
885 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
887 |> put_view(AccountView)
888 |> render("relationship.json", %{user: followed, target: follower})
892 |> put_resp_content_type("application/json")
893 |> send_resp(403, Jason.encode!(%{"error" => message}))
897 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
898 with %User{} = follower <- User.get_cached_by_id(id),
899 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
901 |> put_view(AccountView)
902 |> render("relationship.json", %{user: followed, target: follower})
906 |> put_resp_content_type("application/json")
907 |> send_resp(403, Jason.encode!(%{"error" => message}))
911 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
912 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
913 {_, true} <- {:followed, follower.id != followed.id},
914 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
916 |> put_view(AccountView)
917 |> render("relationship.json", %{user: follower, target: followed})
924 |> put_resp_content_type("application/json")
925 |> send_resp(403, Jason.encode!(%{"error" => message}))
929 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
930 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
931 {_, true} <- {:followed, follower.id != followed.id},
932 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
934 |> put_view(AccountView)
935 |> render("account.json", %{user: followed, for: follower})
942 |> put_resp_content_type("application/json")
943 |> send_resp(403, Jason.encode!(%{"error" => message}))
947 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
948 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
949 {_, true} <- {:followed, follower.id != followed.id},
950 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
952 |> put_view(AccountView)
953 |> render("relationship.json", %{user: follower, target: followed})
963 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
964 with %User{} = muted <- User.get_cached_by_id(id),
965 {:ok, muter} <- User.mute(muter, muted) do
967 |> put_view(AccountView)
968 |> render("relationship.json", %{user: muter, target: muted})
972 |> put_resp_content_type("application/json")
973 |> send_resp(403, Jason.encode!(%{"error" => message}))
977 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
978 with %User{} = muted <- User.get_cached_by_id(id),
979 {:ok, muter} <- User.unmute(muter, muted) do
981 |> put_view(AccountView)
982 |> render("relationship.json", %{user: muter, target: muted})
986 |> put_resp_content_type("application/json")
987 |> send_resp(403, Jason.encode!(%{"error" => message}))
991 def mutes(%{assigns: %{user: user}} = conn, _) do
992 with muted_accounts <- User.muted_users(user) do
993 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
998 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
999 with %User{} = blocked <- User.get_cached_by_id(id),
1000 {:ok, blocker} <- User.block(blocker, blocked),
1001 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1003 |> put_view(AccountView)
1004 |> render("relationship.json", %{user: blocker, target: blocked})
1006 {:error, message} ->
1008 |> put_resp_content_type("application/json")
1009 |> send_resp(403, Jason.encode!(%{"error" => message}))
1013 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1014 with %User{} = blocked <- User.get_cached_by_id(id),
1015 {:ok, blocker} <- User.unblock(blocker, blocked),
1016 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1018 |> put_view(AccountView)
1019 |> render("relationship.json", %{user: blocker, target: blocked})
1021 {:error, message} ->
1023 |> put_resp_content_type("application/json")
1024 |> send_resp(403, Jason.encode!(%{"error" => message}))
1028 def blocks(%{assigns: %{user: user}} = conn, _) do
1029 with blocked_accounts <- User.blocked_users(user) do
1030 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1035 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1036 json(conn, info.domain_blocks || [])
1039 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1040 User.block_domain(blocker, domain)
1044 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1045 User.unblock_domain(blocker, domain)
1049 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1050 with %User{} = subscription_target <- User.get_cached_by_id(id),
1051 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1053 |> put_view(AccountView)
1054 |> render("relationship.json", %{user: user, target: subscription_target})
1056 {:error, message} ->
1058 |> put_resp_content_type("application/json")
1059 |> send_resp(403, Jason.encode!(%{"error" => message}))
1063 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1064 with %User{} = subscription_target <- User.get_cached_by_id(id),
1065 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1067 |> put_view(AccountView)
1068 |> render("relationship.json", %{user: user, target: subscription_target})
1070 {:error, message} ->
1072 |> put_resp_content_type("application/json")
1073 |> send_resp(403, Jason.encode!(%{"error" => message}))
1077 def status_search_query_with_gin(q, query) do
1081 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1085 order_by: [desc: :id]
1089 def status_search_query_with_rum(q, query) do
1093 "? @@ plainto_tsquery('english', ?)",
1097 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1101 def status_search(user, query) do
1103 if Regex.match?(~r/https?:/, query) do
1104 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1105 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1106 true <- Visibility.visible_for_user?(activity, user) do
1114 from([a, o] in Activity.with_preloaded_object(Activity),
1115 where: fragment("?->>'type' = 'Create'", a.data),
1116 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1121 if Pleroma.Config.get([:database, :rum_enabled]) do
1122 status_search_query_with_rum(q, query)
1124 status_search_query_with_gin(q, query)
1127 Repo.all(q) ++ fetched
1130 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1131 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1133 statuses = status_search(user, query)
1135 tags_path = Web.base_url() <> "/tag/"
1141 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1142 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1143 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1146 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1148 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1155 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1156 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1158 statuses = status_search(user, query)
1164 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1165 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1168 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1170 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1177 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1178 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1180 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1185 def favourites(%{assigns: %{user: user}} = conn, params) do
1188 |> Map.put("type", "Create")
1189 |> Map.put("favorited_by", user.ap_id)
1190 |> Map.put("blocking_user", user)
1193 ActivityPub.fetch_activities([], params)
1197 |> add_link_headers(:favourites, activities)
1198 |> put_view(StatusView)
1199 |> render("index.json", %{activities: activities, for: user, as: :activity})
1202 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1203 with %User{} = user <- User.get_by_id(id),
1204 false <- user.info.hide_favorites do
1207 |> Map.put("type", "Create")
1208 |> Map.put("favorited_by", user.ap_id)
1209 |> Map.put("blocking_user", for_user)
1213 ["https://www.w3.org/ns/activitystreams#Public"] ++
1214 [for_user.ap_id | for_user.following]
1216 ["https://www.w3.org/ns/activitystreams#Public"]
1221 |> ActivityPub.fetch_activities(params)
1225 |> add_link_headers(:favourites, activities)
1226 |> put_view(StatusView)
1227 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1230 {:error, :not_found}
1235 |> json(%{error: "Can't get favorites"})
1239 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1240 user = User.get_cached_by_id(user.id)
1243 Bookmark.for_user_query(user.id)
1244 |> Pagination.fetch_paginated(params)
1248 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1251 |> add_link_headers(:bookmarks, bookmarks)
1252 |> put_view(StatusView)
1253 |> render("index.json", %{activities: activities, for: user, as: :activity})
1256 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1257 lists = Pleroma.List.for_user(user, opts)
1258 res = ListView.render("lists.json", lists: lists)
1262 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1263 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1264 res = ListView.render("list.json", list: list)
1270 |> json(%{error: "Record not found"})
1274 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1275 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1276 res = ListView.render("lists.json", lists: lists)
1280 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1281 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1282 {:ok, _list} <- Pleroma.List.delete(list) do
1290 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1291 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1292 res = ListView.render("list.json", list: list)
1297 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1299 |> Enum.each(fn account_id ->
1300 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1301 %User{} = followed <- User.get_cached_by_id(account_id) do
1302 Pleroma.List.follow(list, followed)
1309 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1311 |> Enum.each(fn account_id ->
1312 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1313 %User{} = followed <- User.get_cached_by_id(account_id) do
1314 Pleroma.List.unfollow(list, followed)
1321 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1322 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1323 {:ok, users} = Pleroma.List.get_following(list) do
1325 |> put_view(AccountView)
1326 |> render("accounts.json", %{for: user, users: users, as: :user})
1330 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1331 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1332 {:ok, list} <- Pleroma.List.rename(list, title) do
1333 res = ListView.render("list.json", list: list)
1341 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1342 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1345 |> Map.put("type", "Create")
1346 |> Map.put("blocking_user", user)
1347 |> Map.put("muting_user", user)
1349 # we must filter the following list for the user to avoid leaking statuses the user
1350 # does not actually have permission to see (for more info, peruse security issue #270).
1353 |> Enum.filter(fn x -> x in user.following end)
1354 |> ActivityPub.fetch_activities_bounded(following, params)
1358 |> put_view(StatusView)
1359 |> render("index.json", %{activities: activities, for: user, as: :activity})
1364 |> json(%{error: "Error."})
1368 def index(%{assigns: %{user: user}} = conn, _params) do
1369 token = get_session(conn, :oauth_token)
1372 mastodon_emoji = mastodonized_emoji()
1374 limit = Config.get([:instance, :limit])
1377 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1379 flavour = get_user_flavour(user)
1384 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1385 access_token: token,
1387 domain: Pleroma.Web.Endpoint.host(),
1390 unfollow_modal: false,
1393 auto_play_gif: false,
1394 display_sensitive_media: false,
1395 reduce_motion: false,
1396 max_toot_chars: limit,
1397 mascot: "/images/pleroma-fox-tan-smol.png"
1399 poll_limits: Config.get([:instance, :poll_limits]),
1401 delete_others_notice: present?(user.info.is_moderator),
1402 admin: present?(user.info.is_admin)
1406 default_privacy: user.info.default_scope,
1407 default_sensitive: false,
1408 allow_content_types: Config.get([:instance, :allowed_post_formats])
1410 media_attachments: %{
1411 accept_content_types: [
1427 user.info.settings ||
1457 push_subscription: nil,
1459 custom_emojis: mastodon_emoji,
1465 |> put_layout(false)
1466 |> put_view(MastodonView)
1467 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1470 |> put_session(:return_to, conn.request_path)
1471 |> redirect(to: "/web/login")
1475 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1476 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1478 with changeset <- Ecto.Changeset.change(user),
1479 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1480 {:ok, _user} <- User.update_and_set_cache(changeset) do
1485 |> put_resp_content_type("application/json")
1486 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1490 @supported_flavours ["glitch", "vanilla"]
1492 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1493 when flavour in @supported_flavours do
1494 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1496 with changeset <- Ecto.Changeset.change(user),
1497 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1498 {:ok, user} <- User.update_and_set_cache(changeset),
1499 flavour <- user.info.flavour do
1504 |> put_resp_content_type("application/json")
1505 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1509 def set_flavour(conn, _params) do
1512 |> json(%{error: "Unsupported flavour"})
1515 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1516 json(conn, get_user_flavour(user))
1519 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1523 defp get_user_flavour(_) do
1527 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1528 redirect(conn, to: local_mastodon_root_path(conn))
1531 @doc "Local Mastodon FE login init action"
1532 def login(conn, %{"code" => auth_token}) do
1533 with {:ok, app} <- get_or_make_app(),
1534 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1535 {:ok, token} <- Token.exchange_token(app, auth) do
1537 |> put_session(:oauth_token, token.token)
1538 |> redirect(to: local_mastodon_root_path(conn))
1542 @doc "Local Mastodon FE callback action"
1543 def login(conn, _) do
1544 with {:ok, app} <- get_or_make_app() do
1549 response_type: "code",
1550 client_id: app.client_id,
1552 scope: Enum.join(app.scopes, " ")
1555 redirect(conn, to: path)
1559 defp local_mastodon_root_path(conn) do
1560 case get_session(conn, :return_to) do
1562 mastodon_api_path(conn, :index, ["getting-started"])
1565 delete_session(conn, :return_to)
1570 defp get_or_make_app do
1571 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1572 scopes = ["read", "write", "follow", "push"]
1574 with %App{} = app <- Repo.get_by(App, find_attrs) do
1576 if app.scopes == scopes do
1580 |> Ecto.Changeset.change(%{scopes: scopes})
1588 App.register_changeset(
1590 Map.put(find_attrs, :scopes, scopes)
1597 def logout(conn, _) do
1600 |> redirect(to: "/")
1603 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1604 Logger.debug("Unimplemented, returning unmodified relationship")
1606 with %User{} = target <- User.get_cached_by_id(id) do
1608 |> put_view(AccountView)
1609 |> render("relationship.json", %{user: user, target: target})
1613 def empty_array(conn, _) do
1614 Logger.debug("Unimplemented, returning an empty array")
1618 def empty_object(conn, _) do
1619 Logger.debug("Unimplemented, returning an empty object")
1623 def get_filters(%{assigns: %{user: user}} = conn, _) do
1624 filters = Filter.get_filters(user)
1625 res = FilterView.render("filters.json", filters: filters)
1630 %{assigns: %{user: user}} = conn,
1631 %{"phrase" => phrase, "context" => context} = params
1637 hide: Map.get(params, "irreversible", false),
1638 whole_word: Map.get(params, "boolean", true)
1642 {:ok, response} = Filter.create(query)
1643 res = FilterView.render("filter.json", filter: response)
1647 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1648 filter = Filter.get(filter_id, user)
1649 res = FilterView.render("filter.json", filter: filter)
1654 %{assigns: %{user: user}} = conn,
1655 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1659 filter_id: filter_id,
1662 hide: Map.get(params, "irreversible", nil),
1663 whole_word: Map.get(params, "boolean", true)
1667 {:ok, response} = Filter.update(query)
1668 res = FilterView.render("filter.json", filter: response)
1672 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1675 filter_id: filter_id
1678 {:ok, _} = Filter.delete(query)
1684 def errors(conn, {:error, %Changeset{} = changeset}) do
1687 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1688 |> Enum.map_join(", ", fn {_k, v} -> v end)
1692 |> json(%{error: error_message})
1695 def errors(conn, {:error, :not_found}) do
1698 |> json(%{error: "Record not found"})
1701 def errors(conn, _) do
1704 |> json("Something went wrong")
1707 def suggestions(%{assigns: %{user: user}} = conn, _) do
1708 suggestions = Config.get(:suggestions)
1710 if Keyword.get(suggestions, :enabled, false) do
1711 api = Keyword.get(suggestions, :third_party_engine, "")
1712 timeout = Keyword.get(suggestions, :timeout, 5000)
1713 limit = Keyword.get(suggestions, :limit, 23)
1715 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1717 user = user.nickname
1721 |> String.replace("{{host}}", host)
1722 |> String.replace("{{user}}", user)
1724 with {:ok, %{status: 200, body: body}} <-
1729 recv_timeout: timeout,
1733 {:ok, data} <- Jason.decode(body) do
1736 |> Enum.slice(0, limit)
1741 case User.get_or_fetch(x["acct"]) do
1742 {:ok, %User{id: id}} -> id
1748 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1751 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1757 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1764 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1765 with %Activity{} = activity <- Activity.get_by_id(status_id),
1766 true <- Visibility.visible_for_user?(activity, user) do
1770 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1780 def reports(%{assigns: %{user: user}} = conn, params) do
1781 case CommonAPI.report(user, params) do
1784 |> put_view(ReportView)
1785 |> try_render("report.json", %{activity: activity})
1789 |> put_status(:bad_request)
1790 |> json(%{error: err})
1794 def account_register(
1795 %{assigns: %{app: app}} = conn,
1796 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1804 "captcha_answer_data",
1808 |> Map.put("nickname", nickname)
1809 |> Map.put("fullname", params["fullname"] || nickname)
1810 |> Map.put("bio", params["bio"] || "")
1811 |> Map.put("confirm", params["password"])
1813 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1814 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1816 token_type: "Bearer",
1817 access_token: token.token,
1819 created_at: Token.Utils.format_created_at(token)
1825 |> json(Jason.encode!(errors))
1829 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1832 |> json(%{error: "Missing parameters"})
1835 def account_register(conn, _) do
1838 |> json(%{error: "Invalid credentials"})
1841 def conversations(%{assigns: %{user: user}} = conn, params) do
1842 participations = Participation.for_user_with_last_activity_id(user, params)
1845 Enum.map(participations, fn participation ->
1846 ConversationView.render("participation.json", %{participation: participation, user: user})
1850 |> add_link_headers(:conversations, participations)
1851 |> json(conversations)
1854 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1855 with %Participation{} = participation <-
1856 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1857 {:ok, participation} <- Participation.mark_as_read(participation) do
1858 participation_view =
1859 ConversationView.render("participation.json", %{participation: participation, user: user})
1862 |> json(participation_view)
1866 def try_render(conn, target, params)
1867 when is_binary(target) do
1868 res = render(conn, target, params)
1873 |> json(%{error: "Can't display this activity"})
1879 def try_render(conn, _, _) do
1882 |> json(%{error: "Can't display this activity"})
1885 defp present?(nil), do: false
1886 defp present?(false), do: false
1887 defp present?(_), do: true