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)
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 favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
711 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
712 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
713 q = from(u in User, where: u.ap_id in ^likes)
717 |> put_view(AccountView)
718 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
724 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
725 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
726 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
727 q = from(u in User, where: u.ap_id in ^announces)
731 |> put_view(AccountView)
732 |> render("accounts.json", %{for: user, users: users, as: :user})
738 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
739 local_only = params["local"] in [true, "True", "true", "1"]
742 [params["tag"], params["any"]]
746 |> Enum.map(&String.downcase(&1))
751 |> Enum.map(&String.downcase(&1))
756 |> Enum.map(&String.downcase(&1))
760 |> Map.put("type", "Create")
761 |> Map.put("local_only", local_only)
762 |> Map.put("blocking_user", user)
763 |> Map.put("muting_user", user)
764 |> Map.put("tag", tags)
765 |> Map.put("tag_all", tag_all)
766 |> Map.put("tag_reject", tag_reject)
767 |> ActivityPub.fetch_public_activities()
771 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
772 |> put_view(StatusView)
773 |> render("index.json", %{activities: activities, for: user, as: :activity})
776 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
777 with %User{} = user <- User.get_cached_by_id(id),
778 followers <- MastodonAPI.get_followers(user, params) do
781 for_user && user.id == for_user.id -> followers
782 user.info.hide_followers -> []
787 |> add_link_headers(:followers, followers, user)
788 |> put_view(AccountView)
789 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
793 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
794 with %User{} = user <- User.get_cached_by_id(id),
795 followers <- MastodonAPI.get_friends(user, params) do
798 for_user && user.id == for_user.id -> followers
799 user.info.hide_follows -> []
804 |> add_link_headers(:following, followers, user)
805 |> put_view(AccountView)
806 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
810 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
811 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
813 |> put_view(AccountView)
814 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
818 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
819 with %User{} = follower <- User.get_cached_by_id(id),
820 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
822 |> put_view(AccountView)
823 |> render("relationship.json", %{user: followed, target: follower})
827 |> put_resp_content_type("application/json")
828 |> send_resp(403, Jason.encode!(%{"error" => message}))
832 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
833 with %User{} = follower <- User.get_cached_by_id(id),
834 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
836 |> put_view(AccountView)
837 |> render("relationship.json", %{user: followed, target: follower})
841 |> put_resp_content_type("application/json")
842 |> send_resp(403, Jason.encode!(%{"error" => message}))
846 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
847 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
848 {_, true} <- {:followed, follower.id != followed.id},
849 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
851 |> put_view(AccountView)
852 |> render("relationship.json", %{user: follower, target: followed})
859 |> put_resp_content_type("application/json")
860 |> send_resp(403, Jason.encode!(%{"error" => message}))
864 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
865 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
866 {_, true} <- {:followed, follower.id != followed.id},
867 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
869 |> put_view(AccountView)
870 |> render("account.json", %{user: followed, for: follower})
877 |> put_resp_content_type("application/json")
878 |> send_resp(403, Jason.encode!(%{"error" => message}))
882 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
883 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
884 {_, true} <- {:followed, follower.id != followed.id},
885 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
887 |> put_view(AccountView)
888 |> render("relationship.json", %{user: follower, target: followed})
898 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
899 with %User{} = muted <- User.get_cached_by_id(id),
900 {:ok, muter} <- User.mute(muter, muted) do
902 |> put_view(AccountView)
903 |> render("relationship.json", %{user: muter, target: muted})
907 |> put_resp_content_type("application/json")
908 |> send_resp(403, Jason.encode!(%{"error" => message}))
912 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
913 with %User{} = muted <- User.get_cached_by_id(id),
914 {:ok, muter} <- User.unmute(muter, muted) do
916 |> put_view(AccountView)
917 |> render("relationship.json", %{user: muter, target: muted})
921 |> put_resp_content_type("application/json")
922 |> send_resp(403, Jason.encode!(%{"error" => message}))
926 def mutes(%{assigns: %{user: user}} = conn, _) do
927 with muted_accounts <- User.muted_users(user) do
928 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
933 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
934 with %User{} = blocked <- User.get_cached_by_id(id),
935 {:ok, blocker} <- User.block(blocker, blocked),
936 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: blocker, target: blocked})
943 |> put_resp_content_type("application/json")
944 |> send_resp(403, Jason.encode!(%{"error" => message}))
948 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
949 with %User{} = blocked <- User.get_cached_by_id(id),
950 {:ok, blocker} <- User.unblock(blocker, blocked),
951 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
953 |> put_view(AccountView)
954 |> render("relationship.json", %{user: blocker, target: blocked})
958 |> put_resp_content_type("application/json")
959 |> send_resp(403, Jason.encode!(%{"error" => message}))
963 def blocks(%{assigns: %{user: user}} = conn, _) do
964 with blocked_accounts <- User.blocked_users(user) do
965 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
970 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
971 json(conn, info.domain_blocks || [])
974 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
975 User.block_domain(blocker, domain)
979 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
980 User.unblock_domain(blocker, domain)
984 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
985 with %User{} = subscription_target <- User.get_cached_by_id(id),
986 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
988 |> put_view(AccountView)
989 |> render("relationship.json", %{user: user, target: subscription_target})
993 |> put_resp_content_type("application/json")
994 |> send_resp(403, Jason.encode!(%{"error" => message}))
998 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
999 with %User{} = subscription_target <- User.get_cached_by_id(id),
1000 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1002 |> put_view(AccountView)
1003 |> render("relationship.json", %{user: user, target: subscription_target})
1005 {:error, message} ->
1007 |> put_resp_content_type("application/json")
1008 |> send_resp(403, Jason.encode!(%{"error" => message}))
1012 def status_search_query_with_gin(q, query) do
1016 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1020 order_by: [desc: :id]
1024 def status_search_query_with_rum(q, query) do
1028 "? @@ plainto_tsquery('english', ?)",
1032 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1036 def status_search(user, query) do
1038 if Regex.match?(~r/https?:/, query) do
1039 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1040 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1041 true <- Visibility.visible_for_user?(activity, user) do
1049 from([a, o] in Activity.with_preloaded_object(Activity),
1050 where: fragment("?->>'type' = 'Create'", a.data),
1051 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1056 if Pleroma.Config.get([:database, :rum_enabled]) do
1057 status_search_query_with_rum(q, query)
1059 status_search_query_with_gin(q, query)
1062 Repo.all(q) ++ fetched
1065 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1066 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1068 statuses = status_search(user, query)
1070 tags_path = Web.base_url() <> "/tag/"
1076 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1077 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1078 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1081 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1083 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1090 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1091 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1093 statuses = status_search(user, query)
1099 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1100 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1103 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1105 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1112 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1113 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1115 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1120 def favourites(%{assigns: %{user: user}} = conn, params) do
1123 |> Map.put("type", "Create")
1124 |> Map.put("favorited_by", user.ap_id)
1125 |> Map.put("blocking_user", user)
1128 ActivityPub.fetch_activities([], params)
1132 |> add_link_headers(:favourites, activities)
1133 |> put_view(StatusView)
1134 |> render("index.json", %{activities: activities, for: user, as: :activity})
1137 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1138 with %User{} = user <- User.get_by_id(id),
1139 false <- user.info.hide_favorites do
1142 |> Map.put("type", "Create")
1143 |> Map.put("favorited_by", user.ap_id)
1144 |> Map.put("blocking_user", for_user)
1148 ["https://www.w3.org/ns/activitystreams#Public"] ++
1149 [for_user.ap_id | for_user.following]
1151 ["https://www.w3.org/ns/activitystreams#Public"]
1156 |> ActivityPub.fetch_activities(params)
1160 |> add_link_headers(:favourites, activities)
1161 |> put_view(StatusView)
1162 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1165 {:error, :not_found}
1170 |> json(%{error: "Can't get favorites"})
1174 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1175 user = User.get_cached_by_id(user.id)
1178 Bookmark.for_user_query(user.id)
1179 |> Pagination.fetch_paginated(params)
1183 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1186 |> add_link_headers(:bookmarks, bookmarks)
1187 |> put_view(StatusView)
1188 |> render("index.json", %{activities: activities, for: user, as: :activity})
1191 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1192 lists = Pleroma.List.for_user(user, opts)
1193 res = ListView.render("lists.json", lists: lists)
1197 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1198 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1199 res = ListView.render("list.json", list: list)
1205 |> json(%{error: "Record not found"})
1209 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1210 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1211 res = ListView.render("lists.json", lists: lists)
1215 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1216 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1217 {:ok, _list} <- Pleroma.List.delete(list) do
1225 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1226 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1227 res = ListView.render("list.json", list: list)
1232 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1234 |> Enum.each(fn account_id ->
1235 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1236 %User{} = followed <- User.get_cached_by_id(account_id) do
1237 Pleroma.List.follow(list, followed)
1244 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1246 |> Enum.each(fn account_id ->
1247 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1248 %User{} = followed <- User.get_cached_by_id(account_id) do
1249 Pleroma.List.unfollow(list, followed)
1256 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1257 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1258 {:ok, users} = Pleroma.List.get_following(list) do
1260 |> put_view(AccountView)
1261 |> render("accounts.json", %{for: user, users: users, as: :user})
1265 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1266 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1267 {:ok, list} <- Pleroma.List.rename(list, title) do
1268 res = ListView.render("list.json", list: list)
1276 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1277 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1280 |> Map.put("type", "Create")
1281 |> Map.put("blocking_user", user)
1282 |> Map.put("muting_user", user)
1284 # we must filter the following list for the user to avoid leaking statuses the user
1285 # does not actually have permission to see (for more info, peruse security issue #270).
1288 |> Enum.filter(fn x -> x in user.following end)
1289 |> ActivityPub.fetch_activities_bounded(following, params)
1293 |> put_view(StatusView)
1294 |> render("index.json", %{activities: activities, for: user, as: :activity})
1299 |> json(%{error: "Error."})
1303 def index(%{assigns: %{user: user}} = conn, _params) do
1304 token = get_session(conn, :oauth_token)
1307 mastodon_emoji = mastodonized_emoji()
1309 limit = Config.get([:instance, :limit])
1312 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1314 flavour = get_user_flavour(user)
1319 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1320 access_token: token,
1322 domain: Pleroma.Web.Endpoint.host(),
1325 unfollow_modal: false,
1328 auto_play_gif: false,
1329 display_sensitive_media: false,
1330 reduce_motion: false,
1331 max_toot_chars: limit,
1332 mascot: "/images/pleroma-fox-tan-smol.png"
1335 delete_others_notice: present?(user.info.is_moderator),
1336 admin: present?(user.info.is_admin)
1340 default_privacy: user.info.default_scope,
1341 default_sensitive: false,
1342 allow_content_types: Config.get([:instance, :allowed_post_formats])
1344 media_attachments: %{
1345 accept_content_types: [
1361 user.info.settings ||
1391 push_subscription: nil,
1393 custom_emojis: mastodon_emoji,
1399 |> put_layout(false)
1400 |> put_view(MastodonView)
1401 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1404 |> put_session(:return_to, conn.request_path)
1405 |> redirect(to: "/web/login")
1409 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1410 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1412 with changeset <- Ecto.Changeset.change(user),
1413 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1414 {:ok, _user} <- User.update_and_set_cache(changeset) do
1419 |> put_resp_content_type("application/json")
1420 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1424 @supported_flavours ["glitch", "vanilla"]
1426 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1427 when flavour in @supported_flavours do
1428 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1430 with changeset <- Ecto.Changeset.change(user),
1431 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1432 {:ok, user} <- User.update_and_set_cache(changeset),
1433 flavour <- user.info.flavour do
1438 |> put_resp_content_type("application/json")
1439 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1443 def set_flavour(conn, _params) do
1446 |> json(%{error: "Unsupported flavour"})
1449 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1450 json(conn, get_user_flavour(user))
1453 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1457 defp get_user_flavour(_) do
1461 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1462 redirect(conn, to: local_mastodon_root_path(conn))
1465 @doc "Local Mastodon FE login init action"
1466 def login(conn, %{"code" => auth_token}) do
1467 with {:ok, app} <- get_or_make_app(),
1468 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1469 {:ok, token} <- Token.exchange_token(app, auth) do
1471 |> put_session(:oauth_token, token.token)
1472 |> redirect(to: local_mastodon_root_path(conn))
1476 @doc "Local Mastodon FE callback action"
1477 def login(conn, _) do
1478 with {:ok, app} <- get_or_make_app() do
1483 response_type: "code",
1484 client_id: app.client_id,
1486 scope: Enum.join(app.scopes, " ")
1489 redirect(conn, to: path)
1493 defp local_mastodon_root_path(conn) do
1494 case get_session(conn, :return_to) do
1496 mastodon_api_path(conn, :index, ["getting-started"])
1499 delete_session(conn, :return_to)
1504 defp get_or_make_app do
1505 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1506 scopes = ["read", "write", "follow", "push"]
1508 with %App{} = app <- Repo.get_by(App, find_attrs) do
1510 if app.scopes == scopes do
1514 |> Ecto.Changeset.change(%{scopes: scopes})
1522 App.register_changeset(
1524 Map.put(find_attrs, :scopes, scopes)
1531 def logout(conn, _) do
1534 |> redirect(to: "/")
1537 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1538 Logger.debug("Unimplemented, returning unmodified relationship")
1540 with %User{} = target <- User.get_cached_by_id(id) do
1542 |> put_view(AccountView)
1543 |> render("relationship.json", %{user: user, target: target})
1547 def empty_array(conn, _) do
1548 Logger.debug("Unimplemented, returning an empty array")
1552 def empty_object(conn, _) do
1553 Logger.debug("Unimplemented, returning an empty object")
1557 def get_filters(%{assigns: %{user: user}} = conn, _) do
1558 filters = Filter.get_filters(user)
1559 res = FilterView.render("filters.json", filters: filters)
1564 %{assigns: %{user: user}} = conn,
1565 %{"phrase" => phrase, "context" => context} = params
1571 hide: Map.get(params, "irreversible", false),
1572 whole_word: Map.get(params, "boolean", true)
1576 {:ok, response} = Filter.create(query)
1577 res = FilterView.render("filter.json", filter: response)
1581 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1582 filter = Filter.get(filter_id, user)
1583 res = FilterView.render("filter.json", filter: filter)
1588 %{assigns: %{user: user}} = conn,
1589 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1593 filter_id: filter_id,
1596 hide: Map.get(params, "irreversible", nil),
1597 whole_word: Map.get(params, "boolean", true)
1601 {:ok, response} = Filter.update(query)
1602 res = FilterView.render("filter.json", filter: response)
1606 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1609 filter_id: filter_id
1612 {:ok, _} = Filter.delete(query)
1618 def errors(conn, {:error, %Changeset{} = changeset}) do
1621 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1622 |> Enum.map_join(", ", fn {_k, v} -> v end)
1626 |> json(%{error: error_message})
1629 def errors(conn, {:error, :not_found}) do
1632 |> json(%{error: "Record not found"})
1635 def errors(conn, _) do
1638 |> json("Something went wrong")
1641 def suggestions(%{assigns: %{user: user}} = conn, _) do
1642 suggestions = Config.get(:suggestions)
1644 if Keyword.get(suggestions, :enabled, false) do
1645 api = Keyword.get(suggestions, :third_party_engine, "")
1646 timeout = Keyword.get(suggestions, :timeout, 5000)
1647 limit = Keyword.get(suggestions, :limit, 23)
1649 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1651 user = user.nickname
1655 |> String.replace("{{host}}", host)
1656 |> String.replace("{{user}}", user)
1658 with {:ok, %{status: 200, body: body}} <-
1663 recv_timeout: timeout,
1667 {:ok, data} <- Jason.decode(body) do
1670 |> Enum.slice(0, limit)
1675 case User.get_or_fetch(x["acct"]) do
1676 {:ok, %User{id: id}} -> id
1682 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1685 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1691 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1698 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1699 with %Activity{} = activity <- Activity.get_by_id(status_id),
1700 true <- Visibility.visible_for_user?(activity, user) do
1704 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1714 def reports(%{assigns: %{user: user}} = conn, params) do
1715 case CommonAPI.report(user, params) do
1718 |> put_view(ReportView)
1719 |> try_render("report.json", %{activity: activity})
1723 |> put_status(:bad_request)
1724 |> json(%{error: err})
1728 def account_register(
1729 %{assigns: %{app: app}} = conn,
1730 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1738 "captcha_answer_data",
1742 |> Map.put("nickname", nickname)
1743 |> Map.put("fullname", params["fullname"] || nickname)
1744 |> Map.put("bio", params["bio"] || "")
1745 |> Map.put("confirm", params["password"])
1747 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1748 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1750 token_type: "Bearer",
1751 access_token: token.token,
1753 created_at: Token.Utils.format_created_at(token)
1759 |> json(Jason.encode!(errors))
1763 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1766 |> json(%{error: "Missing parameters"})
1769 def account_register(conn, _) do
1772 |> json(%{error: "Invalid credentials"})
1775 def conversations(%{assigns: %{user: user}} = conn, params) do
1776 participations = Participation.for_user_with_last_activity_id(user, params)
1779 Enum.map(participations, fn participation ->
1780 ConversationView.render("participation.json", %{participation: participation, user: user})
1784 |> add_link_headers(:conversations, participations)
1785 |> json(conversations)
1788 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1789 with %Participation{} = participation <-
1790 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1791 {:ok, participation} <- Participation.mark_as_read(participation) do
1792 participation_view =
1793 ConversationView.render("participation.json", %{participation: participation, user: user})
1796 |> json(participation_view)
1800 def try_render(conn, target, params)
1801 when is_binary(target) do
1802 res = render(conn, target, params)
1807 |> json(%{error: "Can't display this activity"})
1813 def try_render(conn, _, _) do
1816 |> json(%{error: "Can't display this activity"})
1819 defp present?(nil), do: false
1820 defp present?(false), do: false
1821 defp present?(_), do: true