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.Notification
13 alias Pleroma.Object.Fetcher
14 alias Pleroma.Pagination
16 alias Pleroma.ScheduledActivity
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.AppView
25 alias Pleroma.Web.MastodonAPI.FilterView
26 alias Pleroma.Web.MastodonAPI.ListView
27 alias Pleroma.Web.MastodonAPI.MastodonAPI
28 alias Pleroma.Web.MastodonAPI.MastodonView
29 alias Pleroma.Web.MastodonAPI.NotificationView
30 alias Pleroma.Web.MastodonAPI.ReportView
31 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
32 alias Pleroma.Web.MastodonAPI.StatusView
33 alias Pleroma.Web.MediaProxy
34 alias Pleroma.Web.OAuth.App
35 alias Pleroma.Web.OAuth.Authorization
36 alias Pleroma.Web.OAuth.Token
38 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
43 @httpoison Application.get_env(:pleroma, :httpoison)
44 @local_mastodon_name "Mastodon-Local"
46 action_fallback(:errors)
48 def create_app(conn, params) do
49 scopes = oauth_scopes(params, ["read"])
53 |> Map.drop(["scope", "scopes"])
54 |> Map.put("scopes", scopes)
56 with cs <- App.register_changeset(%App{}, app_attrs),
57 false <- cs.changes[:client_name] == @local_mastodon_name,
58 {:ok, app} <- Repo.insert(cs) do
61 |> render("show.json", %{app: app})
70 value_function \\ fn x -> {:ok, x} end
72 if Map.has_key?(params, params_field) do
73 case value_function.(params[params_field]) do
74 {:ok, new_value} -> Map.put(map, map_field, new_value)
82 def update_credentials(%{assigns: %{user: user}} = conn, params) do
87 |> add_if_present(params, "display_name", :name)
88 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
89 |> add_if_present(params, "avatar", :avatar, fn value ->
90 with %Plug.Upload{} <- value,
91 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
100 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
101 |> add_if_present(params, "header", :banner, fn value ->
102 with %Plug.Upload{} <- value,
103 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
110 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
112 with changeset <- User.update_changeset(user, user_params),
113 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
114 {:ok, user} <- User.update_and_set_cache(changeset) do
115 if original_user != user do
116 CommonAPI.update(user)
119 json(conn, AccountView.render("account.json", %{user: user, for: user}))
124 |> json(%{error: "Invalid request"})
128 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
129 account = AccountView.render("account.json", %{user: user, for: user})
133 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
134 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
137 |> render("short.json", %{app: app})
141 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
142 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
143 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
144 account = AccountView.render("account.json", %{user: user, for: for_user})
150 |> json(%{error: "Can't find user"})
154 @mastodon_api_level "2.5.0"
156 def masto_instance(conn, _params) do
157 instance = Config.get(:instance)
161 title: Keyword.get(instance, :name),
162 description: Keyword.get(instance, :description),
163 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
164 email: Keyword.get(instance, :email),
166 streaming_api: Pleroma.Web.Endpoint.websocket_url()
168 stats: Stats.get_stats(),
169 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
171 registrations: Pleroma.Config.get([:instance, :registrations_open]),
172 # Extra (not present in Mastodon):
173 max_toot_chars: Keyword.get(instance, :limit)
179 def peers(conn, _params) do
180 json(conn, Stats.get_peers())
183 defp mastodonized_emoji do
184 Pleroma.Emoji.get_all()
185 |> Enum.map(fn {shortcode, relative_url, tags} ->
186 url = to_string(URI.merge(Web.base_url(), relative_url))
189 "shortcode" => shortcode,
191 "visible_in_picker" => true,
198 def custom_emojis(conn, _params) do
199 mastodon_emoji = mastodonized_emoji()
200 json(conn, mastodon_emoji)
203 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
206 |> Map.drop(["since_id", "max_id", "min_id"])
209 last = List.last(activities)
216 |> Map.get("limit", "20")
217 |> String.to_integer()
220 if length(activities) <= limit do
226 |> Enum.at(limit * -1)
230 {next_url, prev_url} =
234 Pleroma.Web.Endpoint,
237 Map.merge(params, %{max_id: max_id})
240 Pleroma.Web.Endpoint,
243 Map.merge(params, %{min_id: min_id})
249 Pleroma.Web.Endpoint,
251 Map.merge(params, %{max_id: max_id})
254 Pleroma.Web.Endpoint,
256 Map.merge(params, %{min_id: min_id})
262 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
268 def home_timeline(%{assigns: %{user: user}} = conn, params) do
271 |> Map.put("type", ["Create", "Announce"])
272 |> Map.put("blocking_user", user)
273 |> Map.put("muting_user", user)
274 |> Map.put("user", user)
277 [user.ap_id | user.following]
278 |> ActivityPub.fetch_activities(params)
279 |> ActivityPub.contain_timeline(user)
283 |> add_link_headers(:home_timeline, activities)
284 |> put_view(StatusView)
285 |> render("index.json", %{activities: activities, for: user, as: :activity})
288 def public_timeline(%{assigns: %{user: user}} = conn, params) do
289 local_only = params["local"] in [true, "True", "true", "1"]
293 |> Map.put("type", ["Create", "Announce"])
294 |> Map.put("local_only", local_only)
295 |> Map.put("blocking_user", user)
296 |> Map.put("muting_user", user)
297 |> ActivityPub.fetch_public_activities()
301 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
302 |> put_view(StatusView)
303 |> render("index.json", %{activities: activities, for: user, as: :activity})
306 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
307 with %User{} = user <- User.get_cached_by_id(params["id"]) do
308 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
311 |> add_link_headers(:user_statuses, activities, params["id"])
312 |> put_view(StatusView)
313 |> render("index.json", %{
314 activities: activities,
321 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
324 |> Map.put("type", "Create")
325 |> Map.put("blocking_user", user)
326 |> Map.put("user", user)
327 |> Map.put(:visibility, "direct")
331 |> ActivityPub.fetch_activities_query(params)
332 |> Pagination.fetch_paginated(params)
335 |> add_link_headers(:dm_timeline, activities)
336 |> put_view(StatusView)
337 |> render("index.json", %{activities: activities, for: user, as: :activity})
340 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
341 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
342 true <- Visibility.visible_for_user?(activity, user) do
344 |> put_view(StatusView)
345 |> try_render("status.json", %{activity: activity, for: user})
349 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
350 with %Activity{} = activity <- Activity.get_by_id(id),
352 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
353 "blocking_user" => user,
357 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
359 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
360 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
366 activities: grouped_activities[true] || [],
370 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
375 activities: grouped_activities[false] || [],
379 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
386 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
387 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
389 |> add_link_headers(:scheduled_statuses, scheduled_activities)
390 |> put_view(ScheduledActivityView)
391 |> render("index.json", %{scheduled_activities: scheduled_activities})
395 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
396 with %ScheduledActivity{} = scheduled_activity <-
397 ScheduledActivity.get(user, scheduled_activity_id) do
399 |> put_view(ScheduledActivityView)
400 |> render("show.json", %{scheduled_activity: scheduled_activity})
402 _ -> {:error, :not_found}
406 def update_scheduled_status(
407 %{assigns: %{user: user}} = conn,
408 %{"id" => scheduled_activity_id} = params
410 with %ScheduledActivity{} = scheduled_activity <-
411 ScheduledActivity.get(user, scheduled_activity_id),
412 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
414 |> put_view(ScheduledActivityView)
415 |> render("show.json", %{scheduled_activity: scheduled_activity})
417 nil -> {:error, :not_found}
422 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id),
425 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
427 |> put_view(ScheduledActivityView)
428 |> render("show.json", %{scheduled_activity: scheduled_activity})
430 nil -> {:error, :not_found}
435 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
436 when length(media_ids) > 0 do
439 |> Map.put("status", ".")
441 post_status(conn, params)
444 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
447 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
450 case get_req_header(conn, "idempotency-key") do
452 _ -> Ecto.UUID.generate()
455 scheduled_at = params["scheduled_at"]
457 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
458 with {:ok, scheduled_activity} <-
459 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
461 |> put_view(ScheduledActivityView)
462 |> render("show.json", %{scheduled_activity: scheduled_activity})
465 params = Map.drop(params, ["scheduled_at"])
468 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
469 CommonAPI.post(user, params)
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
479 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
485 |> json(%{error: "Can't delete this post"})
489 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
490 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
491 %Activity{} = announce <- Activity.normalize(announce.data) do
493 |> put_view(StatusView)
494 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
498 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
499 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
500 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
502 |> put_view(StatusView)
503 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
507 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
508 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
509 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
511 |> put_view(StatusView)
512 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
516 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
517 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
518 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
525 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
526 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
528 |> put_view(StatusView)
529 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
533 |> put_resp_content_type("application/json")
534 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
538 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
539 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
541 |> put_view(StatusView)
542 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
546 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
547 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
548 %Object{} = object <- Object.normalize(activity),
549 %User{} = user <- User.get_cached_by_nickname(user.nickname),
550 true <- Visibility.visible_for_user?(activity, user),
551 {:ok, user} <- User.bookmark(user, object.data["id"]) do
553 |> put_view(StatusView)
554 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
558 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
559 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
560 %Object{} = object <- Object.normalize(activity),
561 %User{} = user <- User.get_cached_by_nickname(user.nickname),
562 true <- Visibility.visible_for_user?(activity, user),
563 {:ok, user} <- User.unbookmark(user, object.data["id"]) do
565 |> put_view(StatusView)
566 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
570 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
571 activity = Activity.get_by_id(id)
573 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
575 |> put_view(StatusView)
576 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
580 |> put_resp_content_type("application/json")
581 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
585 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
586 activity = Activity.get_by_id(id)
588 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
590 |> put_view(StatusView)
591 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
595 def notifications(%{assigns: %{user: user}} = conn, params) do
596 notifications = MastodonAPI.get_notifications(user, params)
599 |> add_link_headers(:notifications, notifications)
600 |> put_view(NotificationView)
601 |> render("index.json", %{notifications: notifications, for: user})
604 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
605 with {:ok, notification} <- Notification.get(user, id) do
607 |> put_view(NotificationView)
608 |> render("show.json", %{notification: notification, for: user})
612 |> put_resp_content_type("application/json")
613 |> send_resp(403, Jason.encode!(%{"error" => reason}))
617 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
618 Notification.clear(user)
622 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
623 with {:ok, _notif} <- Notification.dismiss(user, id) do
628 |> put_resp_content_type("application/json")
629 |> send_resp(403, Jason.encode!(%{"error" => reason}))
633 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
634 Notification.destroy_multiple(user, ids)
638 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
640 q = from(u in User, where: u.id in ^id)
641 targets = Repo.all(q)
644 |> put_view(AccountView)
645 |> render("relationships.json", %{user: user, targets: targets})
648 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
649 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
651 def update_media(%{assigns: %{user: user}} = conn, data) do
652 with %Object{} = object <- Repo.get(Object, data["id"]),
653 true <- Object.authorize_mutation(object, user),
654 true <- is_binary(data["description"]),
655 description <- data["description"] do
656 new_data = %{object.data | "name" => description}
660 |> Object.change(%{data: new_data})
663 attachment_data = Map.put(new_data, "id", object.id)
666 |> put_view(StatusView)
667 |> render("attachment.json", %{attachment: attachment_data})
671 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
672 with {:ok, object} <-
675 actor: User.ap_id(user),
676 description: Map.get(data, "description")
678 attachment_data = Map.put(object.data, "id", object.id)
681 |> put_view(StatusView)
682 |> render("attachment.json", %{attachment: attachment_data})
686 def favourited_by(conn, %{"id" => id}) do
687 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
688 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
689 q = from(u in User, where: u.ap_id in ^likes)
693 |> put_view(AccountView)
694 |> render(AccountView, "accounts.json", %{users: users, as: :user})
700 def reblogged_by(conn, %{"id" => id}) do
701 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
702 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
703 q = from(u in User, where: u.ap_id in ^announces)
707 |> put_view(AccountView)
708 |> render("accounts.json", %{users: users, as: :user})
714 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
715 local_only = params["local"] in [true, "True", "true", "1"]
718 [params["tag"], params["any"]]
722 |> Enum.map(&String.downcase(&1))
727 |> Enum.map(&String.downcase(&1))
732 |> Enum.map(&String.downcase(&1))
736 |> Map.put("type", "Create")
737 |> Map.put("local_only", local_only)
738 |> Map.put("blocking_user", user)
739 |> Map.put("muting_user", user)
740 |> Map.put("tag", tags)
741 |> Map.put("tag_all", tag_all)
742 |> Map.put("tag_reject", tag_reject)
743 |> ActivityPub.fetch_public_activities()
747 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
748 |> put_view(StatusView)
749 |> render("index.json", %{activities: activities, for: user, as: :activity})
752 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
753 with %User{} = user <- User.get_cached_by_id(id),
754 followers <- MastodonAPI.get_followers(user, params) do
757 for_user && user.id == for_user.id -> followers
758 user.info.hide_followers -> []
763 |> add_link_headers(:followers, followers, user)
764 |> put_view(AccountView)
765 |> render("accounts.json", %{users: followers, as: :user})
769 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
770 with %User{} = user <- User.get_cached_by_id(id),
771 followers <- MastodonAPI.get_friends(user, params) do
774 for_user && user.id == for_user.id -> followers
775 user.info.hide_follows -> []
780 |> add_link_headers(:following, followers, user)
781 |> put_view(AccountView)
782 |> render("accounts.json", %{users: followers, as: :user})
786 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
787 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
789 |> put_view(AccountView)
790 |> render("accounts.json", %{users: follow_requests, as: :user})
794 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
795 with %User{} = follower <- User.get_cached_by_id(id),
796 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
798 |> put_view(AccountView)
799 |> render("relationship.json", %{user: followed, target: follower})
803 |> put_resp_content_type("application/json")
804 |> send_resp(403, Jason.encode!(%{"error" => message}))
808 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
809 with %User{} = follower <- User.get_cached_by_id(id),
810 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
812 |> put_view(AccountView)
813 |> render("relationship.json", %{user: followed, target: follower})
817 |> put_resp_content_type("application/json")
818 |> send_resp(403, Jason.encode!(%{"error" => message}))
822 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
823 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
824 {_, true} <- {:followed, follower.id != followed.id},
825 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
827 |> put_view(AccountView)
828 |> render("relationship.json", %{user: follower, target: followed})
835 |> put_resp_content_type("application/json")
836 |> send_resp(403, Jason.encode!(%{"error" => message}))
840 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
841 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
842 {_, true} <- {:followed, follower.id != followed.id},
843 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
845 |> put_view(AccountView)
846 |> render("account.json", %{user: followed, for: follower})
853 |> put_resp_content_type("application/json")
854 |> send_resp(403, Jason.encode!(%{"error" => message}))
858 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
859 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
860 {_, true} <- {:followed, follower.id != followed.id},
861 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
863 |> put_view(AccountView)
864 |> render("relationship.json", %{user: follower, target: followed})
874 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
875 with %User{} = muted <- User.get_cached_by_id(id),
876 {:ok, muter} <- User.mute(muter, muted) do
878 |> put_view(AccountView)
879 |> render("relationship.json", %{user: muter, target: muted})
883 |> put_resp_content_type("application/json")
884 |> send_resp(403, Jason.encode!(%{"error" => message}))
888 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
889 with %User{} = muted <- User.get_cached_by_id(id),
890 {:ok, muter} <- User.unmute(muter, muted) do
892 |> put_view(AccountView)
893 |> render("relationship.json", %{user: muter, target: muted})
897 |> put_resp_content_type("application/json")
898 |> send_resp(403, Jason.encode!(%{"error" => message}))
902 def mutes(%{assigns: %{user: user}} = conn, _) do
903 with muted_accounts <- User.muted_users(user) do
904 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
909 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
910 with %User{} = blocked <- User.get_cached_by_id(id),
911 {:ok, blocker} <- User.block(blocker, blocked),
912 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
914 |> put_view(AccountView)
915 |> render("relationship.json", %{user: blocker, target: blocked})
919 |> put_resp_content_type("application/json")
920 |> send_resp(403, Jason.encode!(%{"error" => message}))
924 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
925 with %User{} = blocked <- User.get_cached_by_id(id),
926 {:ok, blocker} <- User.unblock(blocker, blocked),
927 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
929 |> put_view(AccountView)
930 |> render("relationship.json", %{user: blocker, target: blocked})
934 |> put_resp_content_type("application/json")
935 |> send_resp(403, Jason.encode!(%{"error" => message}))
939 def blocks(%{assigns: %{user: user}} = conn, _) do
940 with blocked_accounts <- User.blocked_users(user) do
941 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
946 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
947 json(conn, info.domain_blocks || [])
950 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
951 User.block_domain(blocker, domain)
955 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
956 User.unblock_domain(blocker, domain)
960 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
961 with %User{} = subscription_target <- User.get_cached_by_id(id),
962 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
964 |> put_view(AccountView)
965 |> render("relationship.json", %{user: user, target: subscription_target})
969 |> put_resp_content_type("application/json")
970 |> send_resp(403, Jason.encode!(%{"error" => message}))
974 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
975 with %User{} = subscription_target <- User.get_cached_by_id(id),
976 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
978 |> put_view(AccountView)
979 |> render("relationship.json", %{user: user, target: subscription_target})
983 |> put_resp_content_type("application/json")
984 |> send_resp(403, Jason.encode!(%{"error" => message}))
988 def status_search(user, query) do
990 if Regex.match?(~r/https?:/, query) do
991 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
992 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
993 true <- Visibility.visible_for_user?(activity, user) do
1002 [a, o] in Activity.with_preloaded_object(Activity),
1003 where: fragment("?->>'type' = 'Create'", a.data),
1004 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1007 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1012 order_by: [desc: :id]
1015 Repo.all(q) ++ fetched
1018 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1019 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1021 statuses = status_search(user, query)
1023 tags_path = Web.base_url() <> "/tag/"
1029 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1030 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1031 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1034 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1036 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1043 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1044 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1046 statuses = status_search(user, query)
1052 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1053 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1056 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1058 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1065 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1066 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1068 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1073 def favourites(%{assigns: %{user: user}} = conn, params) do
1076 |> Map.put("type", "Create")
1077 |> Map.put("favorited_by", user.ap_id)
1078 |> Map.put("blocking_user", user)
1081 ActivityPub.fetch_activities([], params)
1085 |> add_link_headers(:favourites, activities)
1086 |> put_view(StatusView)
1087 |> render("index.json", %{activities: activities, for: user, as: :activity})
1090 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1091 with %User{} = user <- User.get_by_id(id),
1092 false <- user.info.hide_favorites do
1095 |> Map.put("type", "Create")
1096 |> Map.put("favorited_by", user.ap_id)
1097 |> Map.put("blocking_user", for_user)
1101 ["https://www.w3.org/ns/activitystreams#Public"] ++
1102 [for_user.ap_id | for_user.following]
1104 ["https://www.w3.org/ns/activitystreams#Public"]
1109 |> ActivityPub.fetch_activities(params)
1113 |> add_link_headers(:favourites, activities)
1114 |> put_view(StatusView)
1115 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1118 {:error, :not_found}
1123 |> json(%{error: "Can't get favorites"})
1127 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1128 user = User.get_cached_by_id(user.id)
1132 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1136 |> put_view(StatusView)
1137 |> render("index.json", %{activities: activities, for: user, as: :activity})
1140 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1141 lists = Pleroma.List.for_user(user, opts)
1142 res = ListView.render("lists.json", lists: lists)
1146 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1147 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1148 res = ListView.render("list.json", list: list)
1154 |> json(%{error: "Record not found"})
1158 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1159 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1160 res = ListView.render("lists.json", lists: lists)
1164 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1165 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1166 {:ok, _list} <- Pleroma.List.delete(list) do
1174 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1175 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1176 res = ListView.render("list.json", list: list)
1181 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1183 |> Enum.each(fn account_id ->
1184 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1185 %User{} = followed <- User.get_cached_by_id(account_id) do
1186 Pleroma.List.follow(list, followed)
1193 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1195 |> Enum.each(fn account_id ->
1196 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1197 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1198 Pleroma.List.unfollow(list, followed)
1205 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1206 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1207 {:ok, users} = Pleroma.List.get_following(list) do
1209 |> put_view(AccountView)
1210 |> render("accounts.json", %{users: users, as: :user})
1214 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1215 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1216 {:ok, list} <- Pleroma.List.rename(list, title) do
1217 res = ListView.render("list.json", list: list)
1225 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1226 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1229 |> Map.put("type", "Create")
1230 |> Map.put("blocking_user", user)
1231 |> Map.put("muting_user", user)
1233 # we must filter the following list for the user to avoid leaking statuses the user
1234 # does not actually have permission to see (for more info, peruse security issue #270).
1237 |> Enum.filter(fn x -> x in user.following end)
1238 |> ActivityPub.fetch_activities_bounded(following, params)
1242 |> put_view(StatusView)
1243 |> render("index.json", %{activities: activities, for: user, as: :activity})
1248 |> json(%{error: "Error."})
1252 def index(%{assigns: %{user: user}} = conn, _params) do
1253 token = get_session(conn, :oauth_token)
1256 mastodon_emoji = mastodonized_emoji()
1258 limit = Config.get([:instance, :limit])
1261 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1263 flavour = get_user_flavour(user)
1268 streaming_api_base_url:
1269 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1270 access_token: token,
1272 domain: Pleroma.Web.Endpoint.host(),
1275 unfollow_modal: false,
1278 auto_play_gif: false,
1279 display_sensitive_media: false,
1280 reduce_motion: false,
1281 max_toot_chars: limit,
1282 mascot: "/images/pleroma-fox-tan-smol.png"
1285 delete_others_notice: present?(user.info.is_moderator),
1286 admin: present?(user.info.is_admin)
1290 default_privacy: user.info.default_scope,
1291 default_sensitive: false,
1292 allow_content_types: Config.get([:instance, :allowed_post_formats])
1294 media_attachments: %{
1295 accept_content_types: [
1311 user.info.settings ||
1341 push_subscription: nil,
1343 custom_emojis: mastodon_emoji,
1349 |> put_layout(false)
1350 |> put_view(MastodonView)
1351 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1354 |> put_session(:return_to, conn.request_path)
1355 |> redirect(to: "/web/login")
1359 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1360 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1362 with changeset <- Ecto.Changeset.change(user),
1363 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1364 {:ok, _user} <- User.update_and_set_cache(changeset) do
1369 |> put_resp_content_type("application/json")
1370 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1374 @supported_flavours ["glitch", "vanilla"]
1376 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1377 when flavour in @supported_flavours do
1378 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1380 with changeset <- Ecto.Changeset.change(user),
1381 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1382 {:ok, user} <- User.update_and_set_cache(changeset),
1383 flavour <- user.info.flavour do
1388 |> put_resp_content_type("application/json")
1389 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1393 def set_flavour(conn, _params) do
1396 |> json(%{error: "Unsupported flavour"})
1399 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1400 json(conn, get_user_flavour(user))
1403 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1407 defp get_user_flavour(_) do
1411 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1412 redirect(conn, to: local_mastodon_root_path(conn))
1415 @doc "Local Mastodon FE login init action"
1416 def login(conn, %{"code" => auth_token}) do
1417 with {:ok, app} <- get_or_make_app(),
1418 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1419 {:ok, token} <- Token.exchange_token(app, auth) do
1421 |> put_session(:oauth_token, token.token)
1422 |> redirect(to: local_mastodon_root_path(conn))
1426 @doc "Local Mastodon FE callback action"
1427 def login(conn, _) do
1428 with {:ok, app} <- get_or_make_app() do
1433 response_type: "code",
1434 client_id: app.client_id,
1436 scope: Enum.join(app.scopes, " ")
1439 redirect(conn, to: path)
1443 defp local_mastodon_root_path(conn) do
1444 case get_session(conn, :return_to) do
1446 mastodon_api_path(conn, :index, ["getting-started"])
1449 delete_session(conn, :return_to)
1454 defp get_or_make_app do
1455 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1456 scopes = ["read", "write", "follow", "push"]
1458 with %App{} = app <- Repo.get_by(App, find_attrs) do
1460 if app.scopes == scopes do
1464 |> Ecto.Changeset.change(%{scopes: scopes})
1472 App.register_changeset(
1474 Map.put(find_attrs, :scopes, scopes)
1481 def logout(conn, _) do
1484 |> redirect(to: "/")
1487 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1488 Logger.debug("Unimplemented, returning unmodified relationship")
1490 with %User{} = target <- User.get_cached_by_id(id) do
1492 |> put_view(AccountView)
1493 |> render("relationship.json", %{user: user, target: target})
1497 def empty_array(conn, _) do
1498 Logger.debug("Unimplemented, returning an empty array")
1502 def empty_object(conn, _) do
1503 Logger.debug("Unimplemented, returning an empty object")
1507 def get_filters(%{assigns: %{user: user}} = conn, _) do
1508 filters = Filter.get_filters(user)
1509 res = FilterView.render("filters.json", filters: filters)
1514 %{assigns: %{user: user}} = conn,
1515 %{"phrase" => phrase, "context" => context} = params
1521 hide: Map.get(params, "irreversible", nil),
1522 whole_word: Map.get(params, "boolean", true)
1526 {:ok, response} = Filter.create(query)
1527 res = FilterView.render("filter.json", filter: response)
1531 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1532 filter = Filter.get(filter_id, user)
1533 res = FilterView.render("filter.json", filter: filter)
1538 %{assigns: %{user: user}} = conn,
1539 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1543 filter_id: filter_id,
1546 hide: Map.get(params, "irreversible", nil),
1547 whole_word: Map.get(params, "boolean", true)
1551 {:ok, response} = Filter.update(query)
1552 res = FilterView.render("filter.json", filter: response)
1556 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1559 filter_id: filter_id
1562 {:ok, _} = Filter.delete(query)
1568 def errors(conn, {:error, %Changeset{} = changeset}) do
1571 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1572 |> Enum.map_join(", ", fn {_k, v} -> v end)
1576 |> json(%{error: error_message})
1579 def errors(conn, {:error, :not_found}) do
1582 |> json(%{error: "Record not found"})
1585 def errors(conn, _) do
1588 |> json("Something went wrong")
1591 def suggestions(%{assigns: %{user: user}} = conn, _) do
1592 suggestions = Config.get(:suggestions)
1594 if Keyword.get(suggestions, :enabled, false) do
1595 api = Keyword.get(suggestions, :third_party_engine, "")
1596 timeout = Keyword.get(suggestions, :timeout, 5000)
1597 limit = Keyword.get(suggestions, :limit, 23)
1599 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1601 user = user.nickname
1605 |> String.replace("{{host}}", host)
1606 |> String.replace("{{user}}", user)
1608 with {:ok, %{status: 200, body: body}} <-
1613 recv_timeout: timeout,
1617 {:ok, data} <- Jason.decode(body) do
1620 |> Enum.slice(0, limit)
1625 case User.get_or_fetch(x["acct"]) do
1632 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1635 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1641 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1648 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1649 with %Activity{} = activity <- Activity.get_by_id(status_id),
1650 true <- Visibility.visible_for_user?(activity, user) do
1654 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1664 def reports(%{assigns: %{user: user}} = conn, params) do
1665 case CommonAPI.report(user, params) do
1668 |> put_view(ReportView)
1669 |> try_render("report.json", %{activity: activity})
1673 |> put_status(:bad_request)
1674 |> json(%{error: err})
1678 def try_render(conn, target, params)
1679 when is_binary(target) do
1680 res = render(conn, target, params)
1685 |> json(%{error: "Can't display this activity"})
1691 def try_render(conn, _, _) do
1694 |> json(%{error: "Can't display this activity"})
1697 defp present?(nil), do: false
1698 defp present?(false), do: false
1699 defp present?(_), do: true