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, user)} 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: Pleroma.Web.Endpoint.websocket_url(),
1299 access_token: token,
1301 domain: Pleroma.Web.Endpoint.host(),
1304 unfollow_modal: false,
1307 auto_play_gif: false,
1308 display_sensitive_media: false,
1309 reduce_motion: false,
1310 max_toot_chars: limit,
1311 mascot: "/images/pleroma-fox-tan-smol.png"
1314 delete_others_notice: present?(user.info.is_moderator),
1315 admin: present?(user.info.is_admin)
1319 default_privacy: user.info.default_scope,
1320 default_sensitive: false,
1321 allow_content_types: Config.get([:instance, :allowed_post_formats])
1323 media_attachments: %{
1324 accept_content_types: [
1340 user.info.settings ||
1370 push_subscription: nil,
1372 custom_emojis: mastodon_emoji,
1378 |> put_layout(false)
1379 |> put_view(MastodonView)
1380 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1383 |> put_session(:return_to, conn.request_path)
1384 |> redirect(to: "/web/login")
1388 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1389 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1391 with changeset <- Ecto.Changeset.change(user),
1392 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1393 {:ok, _user} <- User.update_and_set_cache(changeset) do
1398 |> put_resp_content_type("application/json")
1399 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1403 @supported_flavours ["glitch", "vanilla"]
1405 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1406 when flavour in @supported_flavours do
1407 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1409 with changeset <- Ecto.Changeset.change(user),
1410 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1411 {:ok, user} <- User.update_and_set_cache(changeset),
1412 flavour <- user.info.flavour do
1417 |> put_resp_content_type("application/json")
1418 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1422 def set_flavour(conn, _params) do
1425 |> json(%{error: "Unsupported flavour"})
1428 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1429 json(conn, get_user_flavour(user))
1432 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1436 defp get_user_flavour(_) do
1440 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1441 redirect(conn, to: local_mastodon_root_path(conn))
1444 @doc "Local Mastodon FE login init action"
1445 def login(conn, %{"code" => auth_token}) do
1446 with {:ok, app} <- get_or_make_app(),
1447 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1448 {:ok, token} <- Token.exchange_token(app, auth) do
1450 |> put_session(:oauth_token, token.token)
1451 |> redirect(to: local_mastodon_root_path(conn))
1455 @doc "Local Mastodon FE callback action"
1456 def login(conn, _) do
1457 with {:ok, app} <- get_or_make_app() do
1462 response_type: "code",
1463 client_id: app.client_id,
1465 scope: Enum.join(app.scopes, " ")
1468 redirect(conn, to: path)
1472 defp local_mastodon_root_path(conn) do
1473 case get_session(conn, :return_to) do
1475 mastodon_api_path(conn, :index, ["getting-started"])
1478 delete_session(conn, :return_to)
1483 defp get_or_make_app do
1484 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1485 scopes = ["read", "write", "follow", "push"]
1487 with %App{} = app <- Repo.get_by(App, find_attrs) do
1489 if app.scopes == scopes do
1493 |> Ecto.Changeset.change(%{scopes: scopes})
1501 App.register_changeset(
1503 Map.put(find_attrs, :scopes, scopes)
1510 def logout(conn, _) do
1513 |> redirect(to: "/")
1516 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1517 Logger.debug("Unimplemented, returning unmodified relationship")
1519 with %User{} = target <- User.get_cached_by_id(id) do
1521 |> put_view(AccountView)
1522 |> render("relationship.json", %{user: user, target: target})
1526 def empty_array(conn, _) do
1527 Logger.debug("Unimplemented, returning an empty array")
1531 def empty_object(conn, _) do
1532 Logger.debug("Unimplemented, returning an empty object")
1536 def get_filters(%{assigns: %{user: user}} = conn, _) do
1537 filters = Filter.get_filters(user)
1538 res = FilterView.render("filters.json", filters: filters)
1543 %{assigns: %{user: user}} = conn,
1544 %{"phrase" => phrase, "context" => context} = params
1550 hide: Map.get(params, "irreversible", nil),
1551 whole_word: Map.get(params, "boolean", true)
1555 {:ok, response} = Filter.create(query)
1556 res = FilterView.render("filter.json", filter: response)
1560 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1561 filter = Filter.get(filter_id, user)
1562 res = FilterView.render("filter.json", filter: filter)
1567 %{assigns: %{user: user}} = conn,
1568 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1572 filter_id: filter_id,
1575 hide: Map.get(params, "irreversible", nil),
1576 whole_word: Map.get(params, "boolean", true)
1580 {:ok, response} = Filter.update(query)
1581 res = FilterView.render("filter.json", filter: response)
1585 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1588 filter_id: filter_id
1591 {:ok, _} = Filter.delete(query)
1597 def errors(conn, {:error, %Changeset{} = changeset}) do
1600 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1601 |> Enum.map_join(", ", fn {_k, v} -> v end)
1605 |> json(%{error: error_message})
1608 def errors(conn, {:error, :not_found}) do
1611 |> json(%{error: "Record not found"})
1614 def errors(conn, _) do
1617 |> json("Something went wrong")
1620 def suggestions(%{assigns: %{user: user}} = conn, _) do
1621 suggestions = Config.get(:suggestions)
1623 if Keyword.get(suggestions, :enabled, false) do
1624 api = Keyword.get(suggestions, :third_party_engine, "")
1625 timeout = Keyword.get(suggestions, :timeout, 5000)
1626 limit = Keyword.get(suggestions, :limit, 23)
1628 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1630 user = user.nickname
1634 |> String.replace("{{host}}", host)
1635 |> String.replace("{{user}}", user)
1637 with {:ok, %{status: 200, body: body}} <-
1642 recv_timeout: timeout,
1646 {:ok, data} <- Jason.decode(body) do
1649 |> Enum.slice(0, limit)
1654 case User.get_or_fetch(x["acct"]) do
1655 {:ok, %User{id: id}} -> id
1661 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1664 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1670 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1677 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1678 with %Activity{} = activity <- Activity.get_by_id(status_id),
1679 true <- Visibility.visible_for_user?(activity, user) do
1683 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1693 def reports(%{assigns: %{user: user}} = conn, params) do
1694 case CommonAPI.report(user, params) do
1697 |> put_view(ReportView)
1698 |> try_render("report.json", %{activity: activity})
1702 |> put_status(:bad_request)
1703 |> json(%{error: err})
1707 def try_render(conn, target, params)
1708 when is_binary(target) do
1709 res = render(conn, target, params)
1714 |> json(%{error: "Can't display this activity"})
1720 def try_render(conn, _, _) do
1723 |> json(%{error: "Can't display this activity"})
1726 defp present?(nil), do: false
1727 defp present?(false), do: false
1728 defp present?(_), do: true