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)
206 def peers(conn, _params) do
207 json(conn, Stats.get_peers())
210 defp mastodonized_emoji do
211 Pleroma.Emoji.get_all()
212 |> Enum.map(fn {shortcode, relative_url, tags} ->
213 url = to_string(URI.merge(Web.base_url(), relative_url))
216 "shortcode" => shortcode,
218 "visible_in_picker" => true,
225 def custom_emojis(conn, _params) do
226 mastodon_emoji = mastodonized_emoji()
227 json(conn, mastodon_emoji)
230 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
233 |> Map.drop(["since_id", "max_id", "min_id"])
236 last = List.last(activities)
243 |> Map.get("limit", "20")
244 |> String.to_integer()
247 if length(activities) <= limit do
253 |> Enum.at(limit * -1)
257 {next_url, prev_url} =
261 Pleroma.Web.Endpoint,
264 Map.merge(params, %{max_id: max_id})
267 Pleroma.Web.Endpoint,
270 Map.merge(params, %{min_id: min_id})
276 Pleroma.Web.Endpoint,
278 Map.merge(params, %{max_id: max_id})
281 Pleroma.Web.Endpoint,
283 Map.merge(params, %{min_id: min_id})
289 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
295 def home_timeline(%{assigns: %{user: user}} = conn, params) do
298 |> Map.put("type", ["Create", "Announce"])
299 |> Map.put("blocking_user", user)
300 |> Map.put("muting_user", user)
301 |> Map.put("user", user)
304 [user.ap_id | user.following]
305 |> ActivityPub.fetch_activities(params)
309 |> add_link_headers(:home_timeline, activities)
310 |> put_view(StatusView)
311 |> render("index.json", %{activities: activities, for: user, as: :activity})
314 def public_timeline(%{assigns: %{user: user}} = conn, params) do
315 local_only = params["local"] in [true, "True", "true", "1"]
319 |> Map.put("type", ["Create", "Announce"])
320 |> Map.put("local_only", local_only)
321 |> Map.put("blocking_user", user)
322 |> Map.put("muting_user", user)
323 |> ActivityPub.fetch_public_activities()
327 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
328 |> put_view(StatusView)
329 |> render("index.json", %{activities: activities, for: user, as: :activity})
332 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
333 with %User{} = user <- User.get_cached_by_id(params["id"]) do
334 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
337 |> add_link_headers(:user_statuses, activities, params["id"])
338 |> put_view(StatusView)
339 |> render("index.json", %{
340 activities: activities,
347 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
350 |> Map.put("type", "Create")
351 |> Map.put("blocking_user", user)
352 |> Map.put("user", user)
353 |> Map.put(:visibility, "direct")
357 |> ActivityPub.fetch_activities_query(params)
358 |> Pagination.fetch_paginated(params)
361 |> add_link_headers(:dm_timeline, activities)
362 |> put_view(StatusView)
363 |> render("index.json", %{activities: activities, for: user, as: :activity})
366 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
367 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
368 true <- Visibility.visible_for_user?(activity, user) do
370 |> put_view(StatusView)
371 |> try_render("status.json", %{activity: activity, for: user})
375 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
376 with %Activity{} = activity <- Activity.get_by_id(id),
378 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
379 "blocking_user" => user,
383 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
385 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
386 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
392 activities: grouped_activities[true] || [],
396 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
401 activities: grouped_activities[false] || [],
405 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
412 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
413 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
415 |> add_link_headers(:scheduled_statuses, scheduled_activities)
416 |> put_view(ScheduledActivityView)
417 |> render("index.json", %{scheduled_activities: scheduled_activities})
421 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
422 with %ScheduledActivity{} = scheduled_activity <-
423 ScheduledActivity.get(user, scheduled_activity_id) do
425 |> put_view(ScheduledActivityView)
426 |> render("show.json", %{scheduled_activity: scheduled_activity})
428 _ -> {:error, :not_found}
432 def update_scheduled_status(
433 %{assigns: %{user: user}} = conn,
434 %{"id" => scheduled_activity_id} = params
436 with %ScheduledActivity{} = scheduled_activity <-
437 ScheduledActivity.get(user, scheduled_activity_id),
438 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
440 |> put_view(ScheduledActivityView)
441 |> render("show.json", %{scheduled_activity: scheduled_activity})
443 nil -> {:error, :not_found}
448 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
449 with %ScheduledActivity{} = scheduled_activity <-
450 ScheduledActivity.get(user, scheduled_activity_id),
451 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
453 |> put_view(ScheduledActivityView)
454 |> render("show.json", %{scheduled_activity: scheduled_activity})
456 nil -> {:error, :not_found}
461 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
462 when length(media_ids) > 0 do
465 |> Map.put("status", ".")
467 post_status(conn, params)
470 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
473 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
476 case get_req_header(conn, "idempotency-key") do
478 _ -> Ecto.UUID.generate()
481 scheduled_at = params["scheduled_at"]
483 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
484 with {:ok, scheduled_activity} <-
485 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
487 |> put_view(ScheduledActivityView)
488 |> render("show.json", %{scheduled_activity: scheduled_activity})
491 params = Map.drop(params, ["scheduled_at"])
494 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
495 CommonAPI.post(user, params)
499 |> put_view(StatusView)
500 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
504 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
505 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
511 |> json(%{error: "Can't delete this post"})
515 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
517 %Activity{} = announce <- Activity.normalize(announce.data) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
524 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
526 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
528 |> put_view(StatusView)
529 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
533 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
534 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
535 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
537 |> put_view(StatusView)
538 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
542 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
543 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
544 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
546 |> put_view(StatusView)
547 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
551 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
552 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
554 |> put_view(StatusView)
555 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
559 |> put_resp_content_type("application/json")
560 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
564 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
565 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
567 |> put_view(StatusView)
568 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
572 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
573 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
574 %User{} = user <- User.get_cached_by_nickname(user.nickname),
575 true <- Visibility.visible_for_user?(activity, user),
576 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
578 |> put_view(StatusView)
579 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
583 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
584 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
585 %User{} = user <- User.get_cached_by_nickname(user.nickname),
586 true <- Visibility.visible_for_user?(activity, user),
587 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
589 |> put_view(StatusView)
590 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
594 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
595 activity = Activity.get_by_id(id)
597 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
599 |> put_view(StatusView)
600 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
604 |> put_resp_content_type("application/json")
605 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
609 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
610 activity = Activity.get_by_id(id)
612 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
614 |> put_view(StatusView)
615 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
619 def notifications(%{assigns: %{user: user}} = conn, params) do
620 notifications = MastodonAPI.get_notifications(user, params)
623 |> add_link_headers(:notifications, notifications)
624 |> put_view(NotificationView)
625 |> render("index.json", %{notifications: notifications, for: user})
628 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
629 with {:ok, notification} <- Notification.get(user, id) do
631 |> put_view(NotificationView)
632 |> render("show.json", %{notification: notification, for: user})
636 |> put_resp_content_type("application/json")
637 |> send_resp(403, Jason.encode!(%{"error" => reason}))
641 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
642 Notification.clear(user)
646 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
647 with {:ok, _notif} <- Notification.dismiss(user, id) do
652 |> put_resp_content_type("application/json")
653 |> send_resp(403, Jason.encode!(%{"error" => reason}))
657 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
658 Notification.destroy_multiple(user, ids)
662 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
664 q = from(u in User, where: u.id in ^id)
665 targets = Repo.all(q)
668 |> put_view(AccountView)
669 |> render("relationships.json", %{user: user, targets: targets})
672 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
673 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
675 def update_media(%{assigns: %{user: user}} = conn, data) do
676 with %Object{} = object <- Repo.get(Object, data["id"]),
677 true <- Object.authorize_mutation(object, user),
678 true <- is_binary(data["description"]),
679 description <- data["description"] do
680 new_data = %{object.data | "name" => description}
684 |> Object.change(%{data: new_data})
687 attachment_data = Map.put(new_data, "id", object.id)
690 |> put_view(StatusView)
691 |> render("attachment.json", %{attachment: attachment_data})
695 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
696 with {:ok, object} <-
699 actor: User.ap_id(user),
700 description: Map.get(data, "description")
702 attachment_data = Map.put(object.data, "id", object.id)
705 |> put_view(StatusView)
706 |> render("attachment.json", %{attachment: attachment_data})
710 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
711 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
712 %{} = attachment_data <- Map.put(object.data, "id", object.id),
713 %{type: type} = rendered <-
714 StatusView.render("attachment.json", %{attachment: attachment_data}) do
715 # Reject if not an image
716 if type == "image" do
718 # Save to the user's info
719 info_changeset = User.Info.mascot_update(user.info, rendered)
723 |> Ecto.Changeset.change()
724 |> Ecto.Changeset.put_embed(:info, info_changeset)
726 {:ok, _user} = User.update_and_set_cache(user_changeset)
732 |> put_resp_content_type("application/json")
733 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
738 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
739 mascot = User.get_mascot(user)
745 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
746 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
747 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
748 q = from(u in User, where: u.ap_id in ^likes)
752 |> put_view(AccountView)
753 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
759 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
760 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
761 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
762 q = from(u in User, where: u.ap_id in ^announces)
766 |> put_view(AccountView)
767 |> render("accounts.json", %{for: user, users: users, as: :user})
773 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
774 local_only = params["local"] in [true, "True", "true", "1"]
777 [params["tag"], params["any"]]
781 |> Enum.map(&String.downcase(&1))
786 |> Enum.map(&String.downcase(&1))
791 |> Enum.map(&String.downcase(&1))
795 |> Map.put("type", "Create")
796 |> Map.put("local_only", local_only)
797 |> Map.put("blocking_user", user)
798 |> Map.put("muting_user", user)
799 |> Map.put("tag", tags)
800 |> Map.put("tag_all", tag_all)
801 |> Map.put("tag_reject", tag_reject)
802 |> ActivityPub.fetch_public_activities()
806 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
807 |> put_view(StatusView)
808 |> render("index.json", %{activities: activities, for: user, as: :activity})
811 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
812 with %User{} = user <- User.get_cached_by_id(id),
813 followers <- MastodonAPI.get_followers(user, params) do
816 for_user && user.id == for_user.id -> followers
817 user.info.hide_followers -> []
822 |> add_link_headers(:followers, followers, user)
823 |> put_view(AccountView)
824 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
828 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
829 with %User{} = user <- User.get_cached_by_id(id),
830 followers <- MastodonAPI.get_friends(user, params) do
833 for_user && user.id == for_user.id -> followers
834 user.info.hide_follows -> []
839 |> add_link_headers(:following, followers, user)
840 |> put_view(AccountView)
841 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
845 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
846 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
848 |> put_view(AccountView)
849 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
853 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
854 with %User{} = follower <- User.get_cached_by_id(id),
855 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
857 |> put_view(AccountView)
858 |> render("relationship.json", %{user: followed, target: follower})
862 |> put_resp_content_type("application/json")
863 |> send_resp(403, Jason.encode!(%{"error" => message}))
867 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
868 with %User{} = follower <- User.get_cached_by_id(id),
869 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
871 |> put_view(AccountView)
872 |> render("relationship.json", %{user: followed, target: follower})
876 |> put_resp_content_type("application/json")
877 |> send_resp(403, Jason.encode!(%{"error" => message}))
881 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
882 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
883 {_, true} <- {:followed, follower.id != followed.id},
884 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
886 |> put_view(AccountView)
887 |> render("relationship.json", %{user: follower, target: followed})
894 |> put_resp_content_type("application/json")
895 |> send_resp(403, Jason.encode!(%{"error" => message}))
899 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
900 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
901 {_, true} <- {:followed, follower.id != followed.id},
902 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
904 |> put_view(AccountView)
905 |> render("account.json", %{user: followed, for: follower})
912 |> put_resp_content_type("application/json")
913 |> send_resp(403, Jason.encode!(%{"error" => message}))
917 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
918 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
919 {_, true} <- {:followed, follower.id != followed.id},
920 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
922 |> put_view(AccountView)
923 |> render("relationship.json", %{user: follower, target: followed})
933 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
934 with %User{} = muted <- User.get_cached_by_id(id),
935 {:ok, muter} <- User.mute(muter, muted) do
937 |> put_view(AccountView)
938 |> render("relationship.json", %{user: muter, target: muted})
942 |> put_resp_content_type("application/json")
943 |> send_resp(403, Jason.encode!(%{"error" => message}))
947 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
948 with %User{} = muted <- User.get_cached_by_id(id),
949 {:ok, muter} <- User.unmute(muter, muted) do
951 |> put_view(AccountView)
952 |> render("relationship.json", %{user: muter, target: muted})
956 |> put_resp_content_type("application/json")
957 |> send_resp(403, Jason.encode!(%{"error" => message}))
961 def mutes(%{assigns: %{user: user}} = conn, _) do
962 with muted_accounts <- User.muted_users(user) do
963 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
968 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
969 with %User{} = blocked <- User.get_cached_by_id(id),
970 {:ok, blocker} <- User.block(blocker, blocked),
971 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
973 |> put_view(AccountView)
974 |> render("relationship.json", %{user: blocker, target: blocked})
978 |> put_resp_content_type("application/json")
979 |> send_resp(403, Jason.encode!(%{"error" => message}))
983 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
984 with %User{} = blocked <- User.get_cached_by_id(id),
985 {:ok, blocker} <- User.unblock(blocker, blocked),
986 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
988 |> put_view(AccountView)
989 |> render("relationship.json", %{user: blocker, target: blocked})
993 |> put_resp_content_type("application/json")
994 |> send_resp(403, Jason.encode!(%{"error" => message}))
998 def blocks(%{assigns: %{user: user}} = conn, _) do
999 with blocked_accounts <- User.blocked_users(user) do
1000 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1005 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1006 json(conn, info.domain_blocks || [])
1009 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1010 User.block_domain(blocker, domain)
1014 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1015 User.unblock_domain(blocker, domain)
1019 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1020 with %User{} = subscription_target <- User.get_cached_by_id(id),
1021 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1023 |> put_view(AccountView)
1024 |> render("relationship.json", %{user: user, target: subscription_target})
1026 {:error, message} ->
1028 |> put_resp_content_type("application/json")
1029 |> send_resp(403, Jason.encode!(%{"error" => message}))
1033 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1034 with %User{} = subscription_target <- User.get_cached_by_id(id),
1035 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1037 |> put_view(AccountView)
1038 |> render("relationship.json", %{user: user, target: subscription_target})
1040 {:error, message} ->
1042 |> put_resp_content_type("application/json")
1043 |> send_resp(403, Jason.encode!(%{"error" => message}))
1047 def status_search_query_with_gin(q, query) do
1051 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1055 order_by: [desc: :id]
1059 def status_search_query_with_rum(q, query) do
1063 "? @@ plainto_tsquery('english', ?)",
1067 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1071 def status_search(user, query) do
1073 if Regex.match?(~r/https?:/, query) do
1074 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1075 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1076 true <- Visibility.visible_for_user?(activity, user) do
1084 from([a, o] in Activity.with_preloaded_object(Activity),
1085 where: fragment("?->>'type' = 'Create'", a.data),
1086 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1091 if Pleroma.Config.get([:database, :rum_enabled]) do
1092 status_search_query_with_rum(q, query)
1094 status_search_query_with_gin(q, query)
1097 Repo.all(q) ++ fetched
1100 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1101 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1103 statuses = status_search(user, query)
1105 tags_path = Web.base_url() <> "/tag/"
1111 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1112 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1113 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1116 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1118 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1125 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1126 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1128 statuses = status_search(user, query)
1134 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1135 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1138 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1140 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1147 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1148 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1150 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1155 def favourites(%{assigns: %{user: user}} = conn, params) do
1158 |> Map.put("type", "Create")
1159 |> Map.put("favorited_by", user.ap_id)
1160 |> Map.put("blocking_user", user)
1163 ActivityPub.fetch_activities([], params)
1167 |> add_link_headers(:favourites, activities)
1168 |> put_view(StatusView)
1169 |> render("index.json", %{activities: activities, for: user, as: :activity})
1172 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1173 with %User{} = user <- User.get_by_id(id),
1174 false <- user.info.hide_favorites do
1177 |> Map.put("type", "Create")
1178 |> Map.put("favorited_by", user.ap_id)
1179 |> Map.put("blocking_user", for_user)
1183 ["https://www.w3.org/ns/activitystreams#Public"] ++
1184 [for_user.ap_id | for_user.following]
1186 ["https://www.w3.org/ns/activitystreams#Public"]
1191 |> ActivityPub.fetch_activities(params)
1195 |> add_link_headers(:favourites, activities)
1196 |> put_view(StatusView)
1197 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1200 {:error, :not_found}
1205 |> json(%{error: "Can't get favorites"})
1209 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1210 user = User.get_cached_by_id(user.id)
1213 Bookmark.for_user_query(user.id)
1214 |> Pagination.fetch_paginated(params)
1218 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1221 |> add_link_headers(:bookmarks, bookmarks)
1222 |> put_view(StatusView)
1223 |> render("index.json", %{activities: activities, for: user, as: :activity})
1226 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1227 lists = Pleroma.List.for_user(user, opts)
1228 res = ListView.render("lists.json", lists: lists)
1232 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1233 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1234 res = ListView.render("list.json", list: list)
1240 |> json(%{error: "Record not found"})
1244 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1245 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1246 res = ListView.render("lists.json", lists: lists)
1250 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1251 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1252 {:ok, _list} <- Pleroma.List.delete(list) do
1260 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1261 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1262 res = ListView.render("list.json", list: list)
1267 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1269 |> Enum.each(fn account_id ->
1270 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1271 %User{} = followed <- User.get_cached_by_id(account_id) do
1272 Pleroma.List.follow(list, followed)
1279 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1281 |> Enum.each(fn account_id ->
1282 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1283 %User{} = followed <- User.get_cached_by_id(account_id) do
1284 Pleroma.List.unfollow(list, followed)
1291 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1292 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1293 {:ok, users} = Pleroma.List.get_following(list) do
1295 |> put_view(AccountView)
1296 |> render("accounts.json", %{for: user, users: users, as: :user})
1300 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1301 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1302 {:ok, list} <- Pleroma.List.rename(list, title) do
1303 res = ListView.render("list.json", list: list)
1311 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1312 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1315 |> Map.put("type", "Create")
1316 |> Map.put("blocking_user", user)
1317 |> Map.put("muting_user", user)
1319 # we must filter the following list for the user to avoid leaking statuses the user
1320 # does not actually have permission to see (for more info, peruse security issue #270).
1323 |> Enum.filter(fn x -> x in user.following end)
1324 |> ActivityPub.fetch_activities_bounded(following, params)
1328 |> put_view(StatusView)
1329 |> render("index.json", %{activities: activities, for: user, as: :activity})
1334 |> json(%{error: "Error."})
1338 def index(%{assigns: %{user: user}} = conn, _params) do
1339 token = get_session(conn, :oauth_token)
1342 mastodon_emoji = mastodonized_emoji()
1344 limit = Config.get([:instance, :limit])
1347 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1352 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1353 access_token: token,
1355 domain: Pleroma.Web.Endpoint.host(),
1358 unfollow_modal: false,
1361 auto_play_gif: false,
1362 display_sensitive_media: false,
1363 reduce_motion: false,
1364 max_toot_chars: limit,
1365 mascot: User.get_mascot(user)["url"]
1368 delete_others_notice: present?(user.info.is_moderator),
1369 admin: present?(user.info.is_admin)
1373 default_privacy: user.info.default_scope,
1374 default_sensitive: false,
1375 allow_content_types: Config.get([:instance, :allowed_post_formats])
1377 media_attachments: %{
1378 accept_content_types: [
1394 user.info.settings ||
1424 push_subscription: nil,
1426 custom_emojis: mastodon_emoji,
1432 |> put_layout(false)
1433 |> put_view(MastodonView)
1434 |> render("index.html", %{initial_state: initial_state})
1437 |> put_session(:return_to, conn.request_path)
1438 |> redirect(to: "/web/login")
1442 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1443 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1445 with changeset <- Ecto.Changeset.change(user),
1446 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1447 {:ok, _user} <- User.update_and_set_cache(changeset) do
1452 |> put_resp_content_type("application/json")
1453 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1457 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1458 redirect(conn, to: local_mastodon_root_path(conn))
1461 @doc "Local Mastodon FE login init action"
1462 def login(conn, %{"code" => auth_token}) do
1463 with {:ok, app} <- get_or_make_app(),
1464 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1465 {:ok, token} <- Token.exchange_token(app, auth) do
1467 |> put_session(:oauth_token, token.token)
1468 |> redirect(to: local_mastodon_root_path(conn))
1472 @doc "Local Mastodon FE callback action"
1473 def login(conn, _) do
1474 with {:ok, app} <- get_or_make_app() do
1479 response_type: "code",
1480 client_id: app.client_id,
1482 scope: Enum.join(app.scopes, " ")
1485 redirect(conn, to: path)
1489 defp local_mastodon_root_path(conn) do
1490 case get_session(conn, :return_to) do
1492 mastodon_api_path(conn, :index, ["getting-started"])
1495 delete_session(conn, :return_to)
1500 defp get_or_make_app do
1501 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1502 scopes = ["read", "write", "follow", "push"]
1504 with %App{} = app <- Repo.get_by(App, find_attrs) do
1506 if app.scopes == scopes do
1510 |> Ecto.Changeset.change(%{scopes: scopes})
1518 App.register_changeset(
1520 Map.put(find_attrs, :scopes, scopes)
1527 def logout(conn, _) do
1530 |> redirect(to: "/")
1533 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1534 Logger.debug("Unimplemented, returning unmodified relationship")
1536 with %User{} = target <- User.get_cached_by_id(id) do
1538 |> put_view(AccountView)
1539 |> render("relationship.json", %{user: user, target: target})
1543 def empty_array(conn, _) do
1544 Logger.debug("Unimplemented, returning an empty array")
1548 def empty_object(conn, _) do
1549 Logger.debug("Unimplemented, returning an empty object")
1553 def get_filters(%{assigns: %{user: user}} = conn, _) do
1554 filters = Filter.get_filters(user)
1555 res = FilterView.render("filters.json", filters: filters)
1560 %{assigns: %{user: user}} = conn,
1561 %{"phrase" => phrase, "context" => context} = params
1567 hide: Map.get(params, "irreversible", false),
1568 whole_word: Map.get(params, "boolean", true)
1572 {:ok, response} = Filter.create(query)
1573 res = FilterView.render("filter.json", filter: response)
1577 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1578 filter = Filter.get(filter_id, user)
1579 res = FilterView.render("filter.json", filter: filter)
1584 %{assigns: %{user: user}} = conn,
1585 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1589 filter_id: filter_id,
1592 hide: Map.get(params, "irreversible", nil),
1593 whole_word: Map.get(params, "boolean", true)
1597 {:ok, response} = Filter.update(query)
1598 res = FilterView.render("filter.json", filter: response)
1602 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1605 filter_id: filter_id
1608 {:ok, _} = Filter.delete(query)
1614 def errors(conn, {:error, %Changeset{} = changeset}) do
1617 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1618 |> Enum.map_join(", ", fn {_k, v} -> v end)
1622 |> json(%{error: error_message})
1625 def errors(conn, {:error, :not_found}) do
1628 |> json(%{error: "Record not found"})
1631 def errors(conn, _) do
1634 |> json("Something went wrong")
1637 def suggestions(%{assigns: %{user: user}} = conn, _) do
1638 suggestions = Config.get(:suggestions)
1640 if Keyword.get(suggestions, :enabled, false) do
1641 api = Keyword.get(suggestions, :third_party_engine, "")
1642 timeout = Keyword.get(suggestions, :timeout, 5000)
1643 limit = Keyword.get(suggestions, :limit, 23)
1645 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1647 user = user.nickname
1651 |> String.replace("{{host}}", host)
1652 |> String.replace("{{user}}", user)
1654 with {:ok, %{status: 200, body: body}} <-
1659 recv_timeout: timeout,
1663 {:ok, data} <- Jason.decode(body) do
1666 |> Enum.slice(0, limit)
1671 case User.get_or_fetch(x["acct"]) do
1672 {:ok, %User{id: id}} -> id
1678 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1681 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1687 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1694 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1695 with %Activity{} = activity <- Activity.get_by_id(status_id),
1696 true <- Visibility.visible_for_user?(activity, user) do
1700 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1710 def reports(%{assigns: %{user: user}} = conn, params) do
1711 case CommonAPI.report(user, params) do
1714 |> put_view(ReportView)
1715 |> try_render("report.json", %{activity: activity})
1719 |> put_status(:bad_request)
1720 |> json(%{error: err})
1724 def account_register(
1725 %{assigns: %{app: app}} = conn,
1726 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1734 "captcha_answer_data",
1738 |> Map.put("nickname", nickname)
1739 |> Map.put("fullname", params["fullname"] || nickname)
1740 |> Map.put("bio", params["bio"] || "")
1741 |> Map.put("confirm", params["password"])
1743 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1744 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1746 token_type: "Bearer",
1747 access_token: token.token,
1749 created_at: Token.Utils.format_created_at(token)
1755 |> json(Jason.encode!(errors))
1759 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1762 |> json(%{error: "Missing parameters"})
1765 def account_register(conn, _) do
1768 |> json(%{error: "Invalid credentials"})
1771 def conversations(%{assigns: %{user: user}} = conn, params) do
1772 participations = Participation.for_user_with_last_activity_id(user, params)
1775 Enum.map(participations, fn participation ->
1776 ConversationView.render("participation.json", %{participation: participation, user: user})
1780 |> add_link_headers(:conversations, participations)
1781 |> json(conversations)
1784 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1785 with %Participation{} = participation <-
1786 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1787 {:ok, participation} <- Participation.mark_as_read(participation) do
1788 participation_view =
1789 ConversationView.render("participation.json", %{participation: participation, user: user})
1792 |> json(participation_view)
1796 def try_render(conn, target, params)
1797 when is_binary(target) do
1798 res = render(conn, target, params)
1803 |> json(%{error: "Can't display this activity"})
1809 def try_render(conn, _, _) do
1812 |> json(%{error: "Can't display this activity"})
1815 defp present?(nil), do: false
1816 defp present?(false), do: false
1817 defp present?(_), do: true