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
12 alias Pleroma.Notification
14 alias Pleroma.Object.Fetcher
15 alias Pleroma.Pagination
17 alias Pleroma.ScheduledActivity
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.ActivityPub.Visibility
23 alias Pleroma.Web.CommonAPI
24 alias Pleroma.Web.MastodonAPI.AccountView
25 alias Pleroma.Web.MastodonAPI.AppView
26 alias Pleroma.Web.MastodonAPI.FilterView
27 alias Pleroma.Web.MastodonAPI.ListView
28 alias Pleroma.Web.MastodonAPI.MastodonAPI
29 alias Pleroma.Web.MastodonAPI.MastodonView
30 alias Pleroma.Web.MastodonAPI.NotificationView
31 alias Pleroma.Web.MastodonAPI.ReportView
32 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
33 alias Pleroma.Web.MastodonAPI.StatusView
34 alias Pleroma.Web.MediaProxy
35 alias Pleroma.Web.OAuth.App
36 alias Pleroma.Web.OAuth.Authorization
37 alias Pleroma.Web.OAuth.Token
39 alias Pleroma.Web.ControllerHelper
44 @httpoison Application.get_env(:pleroma, :httpoison)
45 @local_mastodon_name "Mastodon-Local"
47 action_fallback(:errors)
49 def create_app(conn, params) do
50 scopes = ControllerHelper.oauth_scopes(params, ["read"])
54 |> Map.drop(["scope", "scopes"])
55 |> Map.put("scopes", scopes)
57 with cs <- App.register_changeset(%App{}, app_attrs),
58 false <- cs.changes[:client_name] == @local_mastodon_name,
59 {:ok, app} <- Repo.insert(cs) do
62 |> render("show.json", %{app: app})
71 value_function \\ fn x -> {:ok, x} end
73 if Map.has_key?(params, params_field) do
74 case value_function.(params[params_field]) do
75 {:ok, new_value} -> Map.put(map, map_field, new_value)
83 def update_credentials(%{assigns: %{user: user}} = conn, params) do
88 |> add_if_present(params, "display_name", :name)
89 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
90 |> add_if_present(params, "avatar", :avatar, fn value ->
91 with %Plug.Upload{} <- value,
92 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
100 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
101 |> Enum.reduce(%{}, fn key, acc ->
102 add_if_present(acc, params, to_string(key), key, fn value ->
103 {:ok, ControllerHelper.truthy_param?(value)}
106 |> add_if_present(params, "default_scope", :default_scope)
107 |> add_if_present(params, "header", :banner, fn value ->
108 with %Plug.Upload{} <- value,
109 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
116 info_cng = User.Info.profile_update(user.info, info_params)
118 with changeset <- User.update_changeset(user, user_params),
119 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
120 {:ok, user} <- User.update_and_set_cache(changeset) do
121 if original_user != user do
122 CommonAPI.update(user)
125 json(conn, AccountView.render("account.json", %{user: user, for: user}))
130 |> json(%{error: "Invalid request"})
134 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
135 account = AccountView.render("account.json", %{user: user, for: user})
139 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
140 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
143 |> render("short.json", %{app: app})
147 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
148 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
149 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
150 account = AccountView.render("account.json", %{user: user, for: for_user})
156 |> json(%{error: "Can't find user"})
160 @mastodon_api_level "2.5.0"
162 def masto_instance(conn, _params) do
163 instance = Config.get(:instance)
167 title: Keyword.get(instance, :name),
168 description: Keyword.get(instance, :description),
169 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
170 email: Keyword.get(instance, :email),
172 streaming_api: Pleroma.Web.Endpoint.websocket_url()
174 stats: Stats.get_stats(),
175 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
177 registrations: Pleroma.Config.get([:instance, :registrations_open]),
178 # Extra (not present in Mastodon):
179 max_toot_chars: Keyword.get(instance, :limit)
185 def peers(conn, _params) do
186 json(conn, Stats.get_peers())
189 defp mastodonized_emoji do
190 Pleroma.Emoji.get_all()
191 |> Enum.map(fn {shortcode, relative_url, tags} ->
192 url = to_string(URI.merge(Web.base_url(), relative_url))
195 "shortcode" => shortcode,
197 "visible_in_picker" => true,
204 def custom_emojis(conn, _params) do
205 mastodon_emoji = mastodonized_emoji()
206 json(conn, mastodon_emoji)
209 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
212 |> Map.drop(["since_id", "max_id", "min_id"])
215 last = List.last(activities)
222 |> Map.get("limit", "20")
223 |> String.to_integer()
226 if length(activities) <= limit do
232 |> Enum.at(limit * -1)
236 {next_url, prev_url} =
240 Pleroma.Web.Endpoint,
243 Map.merge(params, %{max_id: max_id})
246 Pleroma.Web.Endpoint,
249 Map.merge(params, %{min_id: min_id})
255 Pleroma.Web.Endpoint,
257 Map.merge(params, %{max_id: max_id})
260 Pleroma.Web.Endpoint,
262 Map.merge(params, %{min_id: min_id})
268 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
274 def home_timeline(%{assigns: %{user: user}} = conn, params) do
277 |> Map.put("type", ["Create", "Announce"])
278 |> Map.put("blocking_user", user)
279 |> Map.put("muting_user", user)
280 |> Map.put("user", user)
283 [user.ap_id | user.following]
284 |> ActivityPub.fetch_activities(params)
285 |> ActivityPub.contain_timeline(user)
288 user = Repo.preload(user, bookmarks: :activity)
291 |> add_link_headers(:home_timeline, activities)
292 |> put_view(StatusView)
293 |> render("index.json", %{activities: activities, for: user, as: :activity})
296 def public_timeline(%{assigns: %{user: user}} = conn, params) do
297 local_only = params["local"] in [true, "True", "true", "1"]
301 |> Map.put("type", ["Create", "Announce"])
302 |> Map.put("local_only", local_only)
303 |> Map.put("blocking_user", user)
304 |> Map.put("muting_user", user)
305 |> ActivityPub.fetch_public_activities()
308 user = Repo.preload(user, bookmarks: :activity)
311 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
312 |> put_view(StatusView)
313 |> render("index.json", %{activities: activities, for: user, as: :activity})
316 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
317 with %User{} = user <- User.get_cached_by_id(params["id"]),
318 reading_user <- Repo.preload(reading_user, :bookmarks) do
319 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
322 |> add_link_headers(:user_statuses, activities, params["id"])
323 |> put_view(StatusView)
324 |> render("index.json", %{
325 activities: activities,
332 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
335 |> Map.put("type", "Create")
336 |> Map.put("blocking_user", user)
337 |> Map.put("user", user)
338 |> Map.put(:visibility, "direct")
342 |> ActivityPub.fetch_activities_query(params)
343 |> Pagination.fetch_paginated(params)
345 user = Repo.preload(user, bookmarks: :activity)
348 |> add_link_headers(:dm_timeline, activities)
349 |> put_view(StatusView)
350 |> render("index.json", %{activities: activities, for: user, as: :activity})
353 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
354 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
355 true <- Visibility.visible_for_user?(activity, user) do
356 user = Repo.preload(user, bookmarks: :activity)
359 |> put_view(StatusView)
360 |> try_render("status.json", %{activity: activity, for: user})
364 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
365 with %Activity{} = activity <- Activity.get_by_id(id),
367 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
368 "blocking_user" => user,
372 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
374 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
375 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
381 activities: grouped_activities[true] || [],
385 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
390 activities: grouped_activities[false] || [],
394 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
401 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
402 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
404 |> add_link_headers(:scheduled_statuses, scheduled_activities)
405 |> put_view(ScheduledActivityView)
406 |> render("index.json", %{scheduled_activities: scheduled_activities})
410 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
411 with %ScheduledActivity{} = scheduled_activity <-
412 ScheduledActivity.get(user, scheduled_activity_id) do
414 |> put_view(ScheduledActivityView)
415 |> render("show.json", %{scheduled_activity: scheduled_activity})
417 _ -> {:error, :not_found}
421 def update_scheduled_status(
422 %{assigns: %{user: user}} = conn,
423 %{"id" => scheduled_activity_id} = params
425 with %ScheduledActivity{} = scheduled_activity <-
426 ScheduledActivity.get(user, scheduled_activity_id),
427 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
429 |> put_view(ScheduledActivityView)
430 |> render("show.json", %{scheduled_activity: scheduled_activity})
432 nil -> {:error, :not_found}
437 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
438 with %ScheduledActivity{} = scheduled_activity <-
439 ScheduledActivity.get(user, scheduled_activity_id),
440 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
442 |> put_view(ScheduledActivityView)
443 |> render("show.json", %{scheduled_activity: scheduled_activity})
445 nil -> {:error, :not_found}
450 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
451 when length(media_ids) > 0 do
454 |> Map.put("status", ".")
456 post_status(conn, params)
459 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
462 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
465 case get_req_header(conn, "idempotency-key") do
467 _ -> Ecto.UUID.generate()
470 scheduled_at = params["scheduled_at"]
472 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
473 with {:ok, scheduled_activity} <-
474 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
476 |> put_view(ScheduledActivityView)
477 |> render("show.json", %{scheduled_activity: scheduled_activity})
480 params = Map.drop(params, ["scheduled_at"])
483 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
484 CommonAPI.post(user, params)
488 |> put_view(StatusView)
489 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
493 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
494 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
500 |> json(%{error: "Can't delete this post"})
504 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
505 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
506 %Activity{} = announce <- Activity.normalize(announce.data) do
507 user = Repo.preload(user, bookmarks: :activity)
510 |> put_view(StatusView)
511 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
515 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
518 user = Repo.preload(user, bookmarks: :activity)
521 |> put_view(StatusView)
522 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
526 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
527 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
528 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
530 |> put_view(StatusView)
531 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
535 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
536 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
537 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
539 |> put_view(StatusView)
540 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
544 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
545 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
547 |> put_view(StatusView)
548 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
552 |> put_resp_content_type("application/json")
553 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
557 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
558 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
560 |> put_view(StatusView)
561 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
565 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
566 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
567 %User{} = user <- User.get_cached_by_nickname(user.nickname),
568 true <- Visibility.visible_for_user?(activity, user),
569 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
570 user = Repo.preload(user, bookmarks: :activity)
573 |> put_view(StatusView)
574 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
578 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
579 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
580 %User{} = user <- User.get_cached_by_nickname(user.nickname),
581 true <- Visibility.visible_for_user?(activity, user),
582 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
583 user = Repo.preload(user, bookmarks: :activity)
586 |> put_view(StatusView)
587 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
591 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
592 activity = Activity.get_by_id(id)
594 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
596 |> put_view(StatusView)
597 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
601 |> put_resp_content_type("application/json")
602 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
606 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
607 activity = Activity.get_by_id(id)
609 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
611 |> put_view(StatusView)
612 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
616 def notifications(%{assigns: %{user: user}} = conn, params) do
617 notifications = MastodonAPI.get_notifications(user, params)
620 |> add_link_headers(:notifications, notifications)
621 |> put_view(NotificationView)
622 |> render("index.json", %{notifications: notifications, for: user})
625 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
626 with {:ok, notification} <- Notification.get(user, id) do
628 |> put_view(NotificationView)
629 |> render("show.json", %{notification: notification, for: user})
633 |> put_resp_content_type("application/json")
634 |> send_resp(403, Jason.encode!(%{"error" => reason}))
638 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
639 Notification.clear(user)
643 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
644 with {:ok, _notif} <- Notification.dismiss(user, id) do
649 |> put_resp_content_type("application/json")
650 |> send_resp(403, Jason.encode!(%{"error" => reason}))
654 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
655 Notification.destroy_multiple(user, ids)
659 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
661 q = from(u in User, where: u.id in ^id)
662 targets = Repo.all(q)
665 |> put_view(AccountView)
666 |> render("relationships.json", %{user: user, targets: targets})
669 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
670 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
672 def update_media(%{assigns: %{user: user}} = conn, data) do
673 with %Object{} = object <- Repo.get(Object, data["id"]),
674 true <- Object.authorize_mutation(object, user),
675 true <- is_binary(data["description"]),
676 description <- data["description"] do
677 new_data = %{object.data | "name" => description}
681 |> Object.change(%{data: new_data})
684 attachment_data = Map.put(new_data, "id", object.id)
687 |> put_view(StatusView)
688 |> render("attachment.json", %{attachment: attachment_data})
692 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
693 with {:ok, object} <-
696 actor: User.ap_id(user),
697 description: Map.get(data, "description")
699 attachment_data = Map.put(object.data, "id", object.id)
702 |> put_view(StatusView)
703 |> render("attachment.json", %{attachment: attachment_data})
707 def favourited_by(conn, %{"id" => id}) do
708 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
709 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
710 q = from(u in User, where: u.ap_id in ^likes)
714 |> put_view(AccountView)
715 |> render(AccountView, "accounts.json", %{users: users, as: :user})
721 def reblogged_by(conn, %{"id" => id}) do
722 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
723 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
724 q = from(u in User, where: u.ap_id in ^announces)
728 |> put_view(AccountView)
729 |> render("accounts.json", %{users: users, as: :user})
735 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
736 local_only = params["local"] in [true, "True", "true", "1"]
739 [params["tag"], params["any"]]
743 |> Enum.map(&String.downcase(&1))
748 |> Enum.map(&String.downcase(&1))
753 |> Enum.map(&String.downcase(&1))
757 |> Map.put("type", "Create")
758 |> Map.put("local_only", local_only)
759 |> Map.put("blocking_user", user)
760 |> Map.put("muting_user", user)
761 |> Map.put("tag", tags)
762 |> Map.put("tag_all", tag_all)
763 |> Map.put("tag_reject", tag_reject)
764 |> ActivityPub.fetch_public_activities()
768 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
769 |> put_view(StatusView)
770 |> render("index.json", %{activities: activities, for: user, as: :activity})
773 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
774 with %User{} = user <- User.get_cached_by_id(id),
775 followers <- MastodonAPI.get_followers(user, params) do
778 for_user && user.id == for_user.id -> followers
779 user.info.hide_followers -> []
784 |> add_link_headers(:followers, followers, user)
785 |> put_view(AccountView)
786 |> render("accounts.json", %{users: followers, as: :user})
790 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
791 with %User{} = user <- User.get_cached_by_id(id),
792 followers <- MastodonAPI.get_friends(user, params) do
795 for_user && user.id == for_user.id -> followers
796 user.info.hide_follows -> []
801 |> add_link_headers(:following, followers, user)
802 |> put_view(AccountView)
803 |> render("accounts.json", %{users: followers, as: :user})
807 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
808 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
810 |> put_view(AccountView)
811 |> render("accounts.json", %{users: follow_requests, as: :user})
815 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
816 with %User{} = follower <- User.get_cached_by_id(id),
817 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
819 |> put_view(AccountView)
820 |> render("relationship.json", %{user: followed, target: follower})
824 |> put_resp_content_type("application/json")
825 |> send_resp(403, Jason.encode!(%{"error" => message}))
829 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
830 with %User{} = follower <- User.get_cached_by_id(id),
831 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
833 |> put_view(AccountView)
834 |> render("relationship.json", %{user: followed, target: follower})
838 |> put_resp_content_type("application/json")
839 |> send_resp(403, Jason.encode!(%{"error" => message}))
843 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
844 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
845 {_, true} <- {:followed, follower.id != followed.id},
846 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
848 |> put_view(AccountView)
849 |> render("relationship.json", %{user: follower, target: followed})
856 |> put_resp_content_type("application/json")
857 |> send_resp(403, Jason.encode!(%{"error" => message}))
861 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
862 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
863 {_, true} <- {:followed, follower.id != followed.id},
864 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
866 |> put_view(AccountView)
867 |> render("account.json", %{user: followed, for: follower})
874 |> put_resp_content_type("application/json")
875 |> send_resp(403, Jason.encode!(%{"error" => message}))
879 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
880 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
881 {_, true} <- {:followed, follower.id != followed.id},
882 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
884 |> put_view(AccountView)
885 |> render("relationship.json", %{user: follower, target: followed})
895 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
896 with %User{} = muted <- User.get_cached_by_id(id),
897 {:ok, muter} <- User.mute(muter, muted) do
899 |> put_view(AccountView)
900 |> render("relationship.json", %{user: muter, target: muted})
904 |> put_resp_content_type("application/json")
905 |> send_resp(403, Jason.encode!(%{"error" => message}))
909 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
910 with %User{} = muted <- User.get_cached_by_id(id),
911 {:ok, muter} <- User.unmute(muter, muted) do
913 |> put_view(AccountView)
914 |> render("relationship.json", %{user: muter, target: muted})
918 |> put_resp_content_type("application/json")
919 |> send_resp(403, Jason.encode!(%{"error" => message}))
923 def mutes(%{assigns: %{user: user}} = conn, _) do
924 with muted_accounts <- User.muted_users(user) do
925 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
930 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
931 with %User{} = blocked <- User.get_cached_by_id(id),
932 {:ok, blocker} <- User.block(blocker, blocked),
933 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
935 |> put_view(AccountView)
936 |> render("relationship.json", %{user: blocker, target: blocked})
940 |> put_resp_content_type("application/json")
941 |> send_resp(403, Jason.encode!(%{"error" => message}))
945 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
946 with %User{} = blocked <- User.get_cached_by_id(id),
947 {:ok, blocker} <- User.unblock(blocker, blocked),
948 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
950 |> put_view(AccountView)
951 |> render("relationship.json", %{user: blocker, target: blocked})
955 |> put_resp_content_type("application/json")
956 |> send_resp(403, Jason.encode!(%{"error" => message}))
960 def blocks(%{assigns: %{user: user}} = conn, _) do
961 with blocked_accounts <- User.blocked_users(user) do
962 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
967 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
968 json(conn, info.domain_blocks || [])
971 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
972 User.block_domain(blocker, domain)
976 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
977 User.unblock_domain(blocker, domain)
981 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
982 with %User{} = subscription_target <- User.get_cached_by_id(id),
983 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
985 |> put_view(AccountView)
986 |> render("relationship.json", %{user: user, target: subscription_target})
990 |> put_resp_content_type("application/json")
991 |> send_resp(403, Jason.encode!(%{"error" => message}))
995 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
996 with %User{} = subscription_target <- User.get_cached_by_id(id),
997 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
999 |> put_view(AccountView)
1000 |> render("relationship.json", %{user: user, target: subscription_target})
1002 {:error, message} ->
1004 |> put_resp_content_type("application/json")
1005 |> send_resp(403, Jason.encode!(%{"error" => message}))
1009 def status_search(user, query) do
1011 if Regex.match?(~r/https?:/, query) do
1012 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1013 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1014 true <- Visibility.visible_for_user?(activity, user) do
1023 [a, o] in Activity.with_preloaded_object(Activity),
1024 where: fragment("?->>'type' = 'Create'", a.data),
1025 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1028 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1033 order_by: [desc: :id]
1036 Repo.all(q) ++ fetched
1039 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1040 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1042 statuses = status_search(user, query)
1044 tags_path = Web.base_url() <> "/tag/"
1050 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1051 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1052 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1055 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1057 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1064 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1065 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1067 statuses = status_search(user, query)
1073 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1074 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1077 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1079 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1086 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1087 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1089 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1094 def favourites(%{assigns: %{user: user}} = conn, params) do
1097 |> Map.put("type", "Create")
1098 |> Map.put("favorited_by", user.ap_id)
1099 |> Map.put("blocking_user", user)
1102 ActivityPub.fetch_activities([], params)
1105 user = Repo.preload(user, bookmarks: :activity)
1108 |> add_link_headers(:favourites, activities)
1109 |> put_view(StatusView)
1110 |> render("index.json", %{activities: activities, for: user, as: :activity})
1113 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1114 with %User{} = user <- User.get_by_id(id),
1115 false <- user.info.hide_favorites do
1118 |> Map.put("type", "Create")
1119 |> Map.put("favorited_by", user.ap_id)
1120 |> Map.put("blocking_user", for_user)
1124 ["https://www.w3.org/ns/activitystreams#Public"] ++
1125 [for_user.ap_id | for_user.following]
1127 ["https://www.w3.org/ns/activitystreams#Public"]
1132 |> ActivityPub.fetch_activities(params)
1136 |> add_link_headers(:favourites, activities)
1137 |> put_view(StatusView)
1138 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1141 {:error, :not_found}
1146 |> json(%{error: "Can't get favorites"})
1150 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1151 user = User.get_cached_by_id(user.id)
1152 user = Repo.preload(user, bookmarks: :activity)
1155 Bookmark.for_user_query(user.id)
1156 |> Pagination.fetch_paginated(params)
1160 |> Enum.map(fn b -> b.activity end)
1163 |> add_link_headers(:bookmarks, bookmarks)
1164 |> put_view(StatusView)
1165 |> render("index.json", %{activities: activities, for: user, as: :activity})
1168 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1169 lists = Pleroma.List.for_user(user, opts)
1170 res = ListView.render("lists.json", lists: lists)
1174 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1175 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1176 res = ListView.render("list.json", list: list)
1182 |> json(%{error: "Record not found"})
1186 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1187 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1188 res = ListView.render("lists.json", lists: lists)
1192 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1193 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1194 {:ok, _list} <- Pleroma.List.delete(list) do
1202 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1203 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1204 res = ListView.render("list.json", list: list)
1209 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1211 |> Enum.each(fn account_id ->
1212 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1213 %User{} = followed <- User.get_cached_by_id(account_id) do
1214 Pleroma.List.follow(list, followed)
1221 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1223 |> Enum.each(fn account_id ->
1224 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1225 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1226 Pleroma.List.unfollow(list, followed)
1233 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1234 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1235 {:ok, users} = Pleroma.List.get_following(list) do
1237 |> put_view(AccountView)
1238 |> render("accounts.json", %{users: users, as: :user})
1242 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1243 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1244 {:ok, list} <- Pleroma.List.rename(list, title) do
1245 res = ListView.render("list.json", list: list)
1253 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1254 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1257 |> Map.put("type", "Create")
1258 |> Map.put("blocking_user", user)
1259 |> Map.put("muting_user", user)
1261 # we must filter the following list for the user to avoid leaking statuses the user
1262 # does not actually have permission to see (for more info, peruse security issue #270).
1265 |> Enum.filter(fn x -> x in user.following end)
1266 |> ActivityPub.fetch_activities_bounded(following, params)
1269 user = Repo.preload(user, bookmarks: :activity)
1272 |> put_view(StatusView)
1273 |> render("index.json", %{activities: activities, for: user, as: :activity})
1278 |> json(%{error: "Error."})
1282 def index(%{assigns: %{user: user}} = conn, _params) do
1283 token = get_session(conn, :oauth_token)
1286 mastodon_emoji = mastodonized_emoji()
1288 limit = Config.get([:instance, :limit])
1291 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1293 flavour = get_user_flavour(user)
1298 streaming_api_base_url:
1299 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1300 access_token: token,
1302 domain: Pleroma.Web.Endpoint.host(),
1305 unfollow_modal: false,
1308 auto_play_gif: false,
1309 display_sensitive_media: false,
1310 reduce_motion: false,
1311 max_toot_chars: limit,
1312 mascot: "/images/pleroma-fox-tan-smol.png"
1315 delete_others_notice: present?(user.info.is_moderator),
1316 admin: present?(user.info.is_admin)
1320 default_privacy: user.info.default_scope,
1321 default_sensitive: false,
1322 allow_content_types: Config.get([:instance, :allowed_post_formats])
1324 media_attachments: %{
1325 accept_content_types: [
1341 user.info.settings ||
1371 push_subscription: nil,
1373 custom_emojis: mastodon_emoji,
1379 |> put_layout(false)
1380 |> put_view(MastodonView)
1381 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1384 |> put_session(:return_to, conn.request_path)
1385 |> redirect(to: "/web/login")
1389 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1390 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1392 with changeset <- Ecto.Changeset.change(user),
1393 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1394 {:ok, _user} <- User.update_and_set_cache(changeset) do
1399 |> put_resp_content_type("application/json")
1400 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1404 @supported_flavours ["glitch", "vanilla"]
1406 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1407 when flavour in @supported_flavours do
1408 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1410 with changeset <- Ecto.Changeset.change(user),
1411 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1412 {:ok, user} <- User.update_and_set_cache(changeset),
1413 flavour <- user.info.flavour do
1418 |> put_resp_content_type("application/json")
1419 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1423 def set_flavour(conn, _params) do
1426 |> json(%{error: "Unsupported flavour"})
1429 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1430 json(conn, get_user_flavour(user))
1433 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1437 defp get_user_flavour(_) do
1441 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1442 redirect(conn, to: local_mastodon_root_path(conn))
1445 @doc "Local Mastodon FE login init action"
1446 def login(conn, %{"code" => auth_token}) do
1447 with {:ok, app} <- get_or_make_app(),
1448 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1449 {:ok, token} <- Token.exchange_token(app, auth) do
1451 |> put_session(:oauth_token, token.token)
1452 |> redirect(to: local_mastodon_root_path(conn))
1456 @doc "Local Mastodon FE callback action"
1457 def login(conn, _) do
1458 with {:ok, app} <- get_or_make_app() do
1463 response_type: "code",
1464 client_id: app.client_id,
1466 scope: Enum.join(app.scopes, " ")
1469 redirect(conn, to: path)
1473 defp local_mastodon_root_path(conn) do
1474 case get_session(conn, :return_to) do
1476 mastodon_api_path(conn, :index, ["getting-started"])
1479 delete_session(conn, :return_to)
1484 defp get_or_make_app do
1485 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1486 scopes = ["read", "write", "follow", "push"]
1488 with %App{} = app <- Repo.get_by(App, find_attrs) do
1490 if app.scopes == scopes do
1494 |> Ecto.Changeset.change(%{scopes: scopes})
1502 App.register_changeset(
1504 Map.put(find_attrs, :scopes, scopes)
1511 def logout(conn, _) do
1514 |> redirect(to: "/")
1517 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1518 Logger.debug("Unimplemented, returning unmodified relationship")
1520 with %User{} = target <- User.get_cached_by_id(id) do
1522 |> put_view(AccountView)
1523 |> render("relationship.json", %{user: user, target: target})
1527 def empty_array(conn, _) do
1528 Logger.debug("Unimplemented, returning an empty array")
1532 def empty_object(conn, _) do
1533 Logger.debug("Unimplemented, returning an empty object")
1537 def get_filters(%{assigns: %{user: user}} = conn, _) do
1538 filters = Filter.get_filters(user)
1539 res = FilterView.render("filters.json", filters: filters)
1544 %{assigns: %{user: user}} = conn,
1545 %{"phrase" => phrase, "context" => context} = params
1551 hide: Map.get(params, "irreversible", nil),
1552 whole_word: Map.get(params, "boolean", true)
1556 {:ok, response} = Filter.create(query)
1557 res = FilterView.render("filter.json", filter: response)
1561 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1562 filter = Filter.get(filter_id, user)
1563 res = FilterView.render("filter.json", filter: filter)
1568 %{assigns: %{user: user}} = conn,
1569 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1573 filter_id: filter_id,
1576 hide: Map.get(params, "irreversible", nil),
1577 whole_word: Map.get(params, "boolean", true)
1581 {:ok, response} = Filter.update(query)
1582 res = FilterView.render("filter.json", filter: response)
1586 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1589 filter_id: filter_id
1592 {:ok, _} = Filter.delete(query)
1598 def errors(conn, {:error, %Changeset{} = changeset}) do
1601 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1602 |> Enum.map_join(", ", fn {_k, v} -> v end)
1606 |> json(%{error: error_message})
1609 def errors(conn, {:error, :not_found}) do
1612 |> json(%{error: "Record not found"})
1615 def errors(conn, _) do
1618 |> json("Something went wrong")
1621 def suggestions(%{assigns: %{user: user}} = conn, _) do
1622 suggestions = Config.get(:suggestions)
1624 if Keyword.get(suggestions, :enabled, false) do
1625 api = Keyword.get(suggestions, :third_party_engine, "")
1626 timeout = Keyword.get(suggestions, :timeout, 5000)
1627 limit = Keyword.get(suggestions, :limit, 23)
1629 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1631 user = user.nickname
1635 |> String.replace("{{host}}", host)
1636 |> String.replace("{{user}}", user)
1638 with {:ok, %{status: 200, body: body}} <-
1643 recv_timeout: timeout,
1647 {:ok, data} <- Jason.decode(body) do
1650 |> Enum.slice(0, limit)
1655 case User.get_or_fetch(x["acct"]) do
1662 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1665 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1671 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1678 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1679 with %Activity{} = activity <- Activity.get_by_id(status_id),
1680 true <- Visibility.visible_for_user?(activity, user) do
1684 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1694 def reports(%{assigns: %{user: user}} = conn, params) do
1695 case CommonAPI.report(user, params) do
1698 |> put_view(ReportView)
1699 |> try_render("report.json", %{activity: activity})
1703 |> put_status(:bad_request)
1704 |> json(%{error: err})
1708 def try_render(conn, target, params)
1709 when is_binary(target) do
1710 res = render(conn, target, params)
1715 |> json(%{error: "Can't display this activity"})
1721 def try_render(conn, _, _) do
1724 |> json(%{error: "Can't display this activity"})
1727 defp present?(nil), do: false
1728 defp present?(false), do: false
1729 defp present?(_), do: true