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
15 alias Pleroma.ScheduledActivity
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.MastodonAPI.AccountView
23 alias Pleroma.Web.MastodonAPI.AppView
24 alias Pleroma.Web.MastodonAPI.FilterView
25 alias Pleroma.Web.MastodonAPI.ListView
26 alias Pleroma.Web.MastodonAPI.MastodonAPI
27 alias Pleroma.Web.MastodonAPI.MastodonView
28 alias Pleroma.Web.MastodonAPI.NotificationView
29 alias Pleroma.Web.MastodonAPI.ReportView
30 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
31 alias Pleroma.Web.MastodonAPI.StatusView
32 alias Pleroma.Web.MediaProxy
33 alias Pleroma.Web.OAuth.App
34 alias Pleroma.Web.OAuth.Authorization
35 alias Pleroma.Web.OAuth.Token
37 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
42 @httpoison Application.get_env(:pleroma, :httpoison)
43 @local_mastodon_name "Mastodon-Local"
45 action_fallback(:errors)
47 def create_app(conn, params) do
48 scopes = oauth_scopes(params, ["read"])
52 |> Map.drop(["scope", "scopes"])
53 |> Map.put("scopes", scopes)
55 with cs <- App.register_changeset(%App{}, app_attrs),
56 false <- cs.changes[:client_name] == @local_mastodon_name,
57 {:ok, app} <- Repo.insert(cs) do
60 |> render("show.json", %{app: app})
69 value_function \\ fn x -> {:ok, x} end
71 if Map.has_key?(params, params_field) do
72 case value_function.(params[params_field]) do
73 {:ok, new_value} -> Map.put(map, map_field, new_value)
81 def update_credentials(%{assigns: %{user: user}} = conn, params) do
86 |> add_if_present(params, "display_name", :name)
87 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
88 |> add_if_present(params, "avatar", :avatar, fn value ->
89 with %Plug.Upload{} <- value,
90 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
99 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
100 |> add_if_present(params, "header", :banner, fn value ->
101 with %Plug.Upload{} <- value,
102 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
109 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
111 with changeset <- User.update_changeset(user, user_params),
112 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
113 {:ok, user} <- User.update_and_set_cache(changeset) do
114 if original_user != user do
115 CommonAPI.update(user)
118 json(conn, AccountView.render("account.json", %{user: user, for: user}))
123 |> json(%{error: "Invalid request"})
127 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
128 account = AccountView.render("account.json", %{user: user, for: user})
132 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
133 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
136 |> render("short.json", %{app: app})
140 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
141 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
142 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
143 account = AccountView.render("account.json", %{user: user, for: for_user})
149 |> json(%{error: "Can't find user"})
153 @mastodon_api_level "2.5.0"
155 def masto_instance(conn, _params) do
156 instance = Config.get(:instance)
160 title: Keyword.get(instance, :name),
161 description: Keyword.get(instance, :description),
162 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
163 email: Keyword.get(instance, :email),
165 streaming_api: Pleroma.Web.Endpoint.websocket_url()
167 stats: Stats.get_stats(),
168 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
170 registrations: Pleroma.Config.get([:instance, :registrations_open]),
171 # Extra (not present in Mastodon):
172 max_toot_chars: Keyword.get(instance, :limit)
178 def peers(conn, _params) do
179 json(conn, Stats.get_peers())
182 defp mastodonized_emoji do
183 Pleroma.Emoji.get_all()
184 |> Enum.map(fn {shortcode, relative_url} ->
185 url = to_string(URI.merge(Web.base_url(), relative_url))
188 "shortcode" => shortcode,
190 "visible_in_picker" => true,
196 def custom_emojis(conn, _params) do
197 mastodon_emoji = mastodonized_emoji()
198 json(conn, mastodon_emoji)
201 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
204 |> Map.drop(["since_id", "max_id"])
207 last = List.last(activities)
208 first = List.first(activities)
214 {next_url, prev_url} =
218 Pleroma.Web.Endpoint,
221 Map.merge(params, %{max_id: min})
224 Pleroma.Web.Endpoint,
227 Map.merge(params, %{since_id: max})
233 Pleroma.Web.Endpoint,
235 Map.merge(params, %{max_id: min})
238 Pleroma.Web.Endpoint,
240 Map.merge(params, %{since_id: max})
246 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
252 def home_timeline(%{assigns: %{user: user}} = conn, params) do
255 |> Map.put("type", ["Create", "Announce"])
256 |> Map.put("blocking_user", user)
257 |> Map.put("muting_user", user)
258 |> Map.put("user", user)
261 [user.ap_id | user.following]
262 |> ActivityPub.fetch_activities(params)
263 |> ActivityPub.contain_timeline(user)
267 |> add_link_headers(:home_timeline, activities)
268 |> put_view(StatusView)
269 |> render("index.json", %{activities: activities, for: user, as: :activity})
272 def public_timeline(%{assigns: %{user: user}} = conn, params) do
273 local_only = params["local"] in [true, "True", "true", "1"]
277 |> Map.put("type", ["Create", "Announce"])
278 |> Map.put("local_only", local_only)
279 |> Map.put("blocking_user", user)
280 |> Map.put("muting_user", user)
281 |> ActivityPub.fetch_public_activities()
285 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
286 |> put_view(StatusView)
287 |> render("index.json", %{activities: activities, for: user, as: :activity})
290 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
291 with %User{} = user <- User.get_by_id(params["id"]) do
292 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
295 |> add_link_headers(:user_statuses, activities, params["id"])
296 |> put_view(StatusView)
297 |> render("index.json", %{
298 activities: activities,
305 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
308 |> Map.put("type", "Create")
309 |> Map.put("blocking_user", user)
310 |> Map.put("user", user)
311 |> Map.put(:visibility, "direct")
315 |> ActivityPub.fetch_activities_query(params)
319 |> add_link_headers(:dm_timeline, activities)
320 |> put_view(StatusView)
321 |> render("index.json", %{activities: activities, for: user, as: :activity})
324 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
325 with %Activity{} = activity <- Activity.get_by_id(id),
326 true <- Visibility.visible_for_user?(activity, user) do
328 |> put_view(StatusView)
329 |> try_render("status.json", %{activity: activity, for: user})
333 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
334 with %Activity{} = activity <- Activity.get_by_id(id),
336 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
337 "blocking_user" => user,
341 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
343 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
344 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
350 activities: grouped_activities[true] || [],
354 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
359 activities: grouped_activities[false] || [],
363 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
370 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
371 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
373 |> add_link_headers(:scheduled_statuses, scheduled_activities)
374 |> put_view(ScheduledActivityView)
375 |> render("index.json", %{scheduled_activities: scheduled_activities})
379 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
380 with %ScheduledActivity{} = scheduled_activity <-
381 ScheduledActivity.get(user, scheduled_activity_id) do
383 |> put_view(ScheduledActivityView)
384 |> render("show.json", %{scheduled_activity: scheduled_activity})
386 _ -> {:error, :not_found}
390 def update_scheduled_status(
391 %{assigns: %{user: user}} = conn,
392 %{"id" => scheduled_activity_id} = params
394 with %ScheduledActivity{} = scheduled_activity <-
395 ScheduledActivity.get(user, scheduled_activity_id),
396 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
398 |> put_view(ScheduledActivityView)
399 |> render("show.json", %{scheduled_activity: scheduled_activity})
401 nil -> {:error, :not_found}
406 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
407 with %ScheduledActivity{} = scheduled_activity <-
408 ScheduledActivity.get(user, scheduled_activity_id),
409 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
411 |> put_view(ScheduledActivityView)
412 |> render("show.json", %{scheduled_activity: scheduled_activity})
414 nil -> {:error, :not_found}
419 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
420 when length(media_ids) > 0 do
423 |> Map.put("status", ".")
425 post_status(conn, params)
428 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
431 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
434 case get_req_header(conn, "idempotency-key") do
436 _ -> Ecto.UUID.generate()
439 scheduled_at = params["scheduled_at"]
441 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
442 with {:ok, scheduled_activity} <-
443 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
445 |> put_view(ScheduledActivityView)
446 |> render("show.json", %{scheduled_activity: scheduled_activity})
449 params = Map.drop(params, ["scheduled_at"])
452 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
453 CommonAPI.post(user, params)
457 |> put_view(StatusView)
458 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
462 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
463 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
469 |> json(%{error: "Can't delete this post"})
473 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
474 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
476 |> put_view(StatusView)
477 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
481 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
482 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
483 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
485 |> put_view(StatusView)
486 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
490 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
491 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
492 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
494 |> put_view(StatusView)
495 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
499 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
500 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
501 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
503 |> put_view(StatusView)
504 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
508 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
509 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
511 |> put_view(StatusView)
512 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
516 |> put_resp_content_type("application/json")
517 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
521 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
522 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
524 |> put_view(StatusView)
525 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
529 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
530 with %Activity{} = activity <- Activity.get_by_id(id),
531 %User{} = user <- User.get_by_nickname(user.nickname),
532 true <- Visibility.visible_for_user?(activity, user),
533 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
535 |> put_view(StatusView)
536 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
540 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
541 with %Activity{} = activity <- Activity.get_by_id(id),
542 %User{} = user <- User.get_by_nickname(user.nickname),
543 true <- Visibility.visible_for_user?(activity, user),
544 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
546 |> put_view(StatusView)
547 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
551 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
552 activity = Activity.get_by_id(id)
554 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
556 |> put_view(StatusView)
557 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
561 |> put_resp_content_type("application/json")
562 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
566 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
567 activity = Activity.get_by_id(id)
569 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
571 |> put_view(StatusView)
572 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
576 def notifications(%{assigns: %{user: user}} = conn, params) do
577 notifications = MastodonAPI.get_notifications(user, params)
580 |> add_link_headers(:notifications, notifications)
581 |> put_view(NotificationView)
582 |> render("index.json", %{notifications: notifications, for: user})
585 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
586 with {:ok, notification} <- Notification.get(user, id) do
588 |> put_view(NotificationView)
589 |> render("show.json", %{notification: notification, for: user})
593 |> put_resp_content_type("application/json")
594 |> send_resp(403, Jason.encode!(%{"error" => reason}))
598 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
599 Notification.clear(user)
603 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
604 with {:ok, _notif} <- Notification.dismiss(user, id) do
609 |> put_resp_content_type("application/json")
610 |> send_resp(403, Jason.encode!(%{"error" => reason}))
614 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
616 q = from(u in User, where: u.id in ^id)
617 targets = Repo.all(q)
620 |> put_view(AccountView)
621 |> render("relationships.json", %{user: user, targets: targets})
624 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
625 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
627 def update_media(%{assigns: %{user: user}} = conn, data) do
628 with %Object{} = object <- Repo.get(Object, data["id"]),
629 true <- Object.authorize_mutation(object, user),
630 true <- is_binary(data["description"]),
631 description <- data["description"] do
632 new_data = %{object.data | "name" => description}
636 |> Object.change(%{data: new_data})
639 attachment_data = Map.put(new_data, "id", object.id)
642 |> put_view(StatusView)
643 |> render("attachment.json", %{attachment: attachment_data})
647 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
648 with {:ok, object} <-
651 actor: User.ap_id(user),
652 description: Map.get(data, "description")
654 attachment_data = Map.put(object.data, "id", object.id)
657 |> put_view(StatusView)
658 |> render("attachment.json", %{attachment: attachment_data})
662 def favourited_by(conn, %{"id" => id}) do
663 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
664 q = from(u in User, where: u.ap_id in ^likes)
668 |> put_view(AccountView)
669 |> render(AccountView, "accounts.json", %{users: users, as: :user})
675 def reblogged_by(conn, %{"id" => id}) do
676 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
677 q = from(u in User, where: u.ap_id in ^announces)
681 |> put_view(AccountView)
682 |> render("accounts.json", %{users: users, as: :user})
688 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
689 local_only = params["local"] in [true, "True", "true", "1"]
692 [params["tag"], params["any"]]
696 |> Enum.map(&String.downcase(&1))
701 |> Enum.map(&String.downcase(&1))
706 |> Enum.map(&String.downcase(&1))
710 |> Map.put("type", "Create")
711 |> Map.put("local_only", local_only)
712 |> Map.put("blocking_user", user)
713 |> Map.put("muting_user", user)
714 |> Map.put("tag", tags)
715 |> Map.put("tag_all", tag_all)
716 |> Map.put("tag_reject", tag_reject)
717 |> ActivityPub.fetch_public_activities()
721 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
722 |> put_view(StatusView)
723 |> render("index.json", %{activities: activities, for: user, as: :activity})
726 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
727 with %User{} = user <- User.get_by_id(id),
728 followers <- MastodonAPI.get_followers(user, params) do
731 for_user && user.id == for_user.id -> followers
732 user.info.hide_followers -> []
737 |> add_link_headers(:followers, followers, user)
738 |> put_view(AccountView)
739 |> render("accounts.json", %{users: followers, as: :user})
743 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
744 with %User{} = user <- User.get_by_id(id),
745 followers <- MastodonAPI.get_friends(user, params) do
748 for_user && user.id == for_user.id -> followers
749 user.info.hide_follows -> []
754 |> add_link_headers(:following, followers, user)
755 |> put_view(AccountView)
756 |> render("accounts.json", %{users: followers, as: :user})
760 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
761 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
763 |> put_view(AccountView)
764 |> render("accounts.json", %{users: follow_requests, as: :user})
768 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
769 with %User{} = follower <- User.get_by_id(id),
770 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
772 |> put_view(AccountView)
773 |> render("relationship.json", %{user: followed, target: follower})
777 |> put_resp_content_type("application/json")
778 |> send_resp(403, Jason.encode!(%{"error" => message}))
782 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
783 with %User{} = follower <- User.get_by_id(id),
784 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
786 |> put_view(AccountView)
787 |> render("relationship.json", %{user: followed, target: follower})
791 |> put_resp_content_type("application/json")
792 |> send_resp(403, Jason.encode!(%{"error" => message}))
796 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
797 with %User{} = followed <- User.get_by_id(id),
798 false <- User.following?(follower, followed),
799 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
801 |> put_view(AccountView)
802 |> render("relationship.json", %{user: follower, target: followed})
805 followed = User.get_cached_by_id(id)
808 case conn.params["reblogs"] do
809 true -> CommonAPI.show_reblogs(follower, followed)
810 false -> CommonAPI.hide_reblogs(follower, followed)
814 |> put_view(AccountView)
815 |> render("relationship.json", %{user: follower, target: followed})
819 |> put_resp_content_type("application/json")
820 |> send_resp(403, Jason.encode!(%{"error" => message}))
824 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
825 with %User{} = followed <- User.get_by_nickname(uri),
826 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
828 |> put_view(AccountView)
829 |> render("account.json", %{user: followed, for: follower})
833 |> put_resp_content_type("application/json")
834 |> send_resp(403, Jason.encode!(%{"error" => message}))
838 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
839 with %User{} = followed <- User.get_by_id(id),
840 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
842 |> put_view(AccountView)
843 |> render("relationship.json", %{user: follower, target: followed})
847 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
848 with %User{} = muted <- User.get_by_id(id),
849 {:ok, muter} <- User.mute(muter, muted) do
851 |> put_view(AccountView)
852 |> render("relationship.json", %{user: muter, target: muted})
856 |> put_resp_content_type("application/json")
857 |> send_resp(403, Jason.encode!(%{"error" => message}))
861 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
862 with %User{} = muted <- User.get_by_id(id),
863 {:ok, muter} <- User.unmute(muter, muted) do
865 |> put_view(AccountView)
866 |> render("relationship.json", %{user: muter, target: muted})
870 |> put_resp_content_type("application/json")
871 |> send_resp(403, Jason.encode!(%{"error" => message}))
875 def mutes(%{assigns: %{user: user}} = conn, _) do
876 with muted_accounts <- User.muted_users(user) do
877 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
882 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
883 with %User{} = blocked <- User.get_by_id(id),
884 {:ok, blocker} <- User.block(blocker, blocked),
885 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
887 |> put_view(AccountView)
888 |> render("relationship.json", %{user: blocker, target: blocked})
892 |> put_resp_content_type("application/json")
893 |> send_resp(403, Jason.encode!(%{"error" => message}))
897 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
898 with %User{} = blocked <- User.get_by_id(id),
899 {:ok, blocker} <- User.unblock(blocker, blocked),
900 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
902 |> put_view(AccountView)
903 |> render("relationship.json", %{user: blocker, target: blocked})
907 |> put_resp_content_type("application/json")
908 |> send_resp(403, Jason.encode!(%{"error" => message}))
912 def blocks(%{assigns: %{user: user}} = conn, _) do
913 with blocked_accounts <- User.blocked_users(user) do
914 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
919 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
920 json(conn, info.domain_blocks || [])
923 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
924 User.block_domain(blocker, domain)
928 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
929 User.unblock_domain(blocker, domain)
933 def status_search(user, query) do
935 if Regex.match?(~r/https?:/, query) do
936 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
937 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
938 true <- Visibility.visible_for_user?(activity, user) do
948 where: fragment("?->>'type' = 'Create'", a.data),
949 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
952 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
957 order_by: [desc: :id]
960 Repo.all(q) ++ fetched
963 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
964 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
966 statuses = status_search(user, query)
968 tags_path = Web.base_url() <> "/tag/"
974 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
975 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
976 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
979 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
981 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
988 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
989 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
991 statuses = status_search(user, query)
997 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
998 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1001 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1003 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1010 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1011 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1013 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1018 def favourites(%{assigns: %{user: user}} = conn, params) do
1021 |> Map.put("type", "Create")
1022 |> Map.put("favorited_by", user.ap_id)
1023 |> Map.put("blocking_user", user)
1026 ActivityPub.fetch_activities([], params)
1030 |> add_link_headers(:favourites, activities)
1031 |> put_view(StatusView)
1032 |> render("index.json", %{activities: activities, for: user, as: :activity})
1035 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1036 user = User.get_by_id(user.id)
1040 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1044 |> put_view(StatusView)
1045 |> render("index.json", %{activities: activities, for: user, as: :activity})
1048 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1049 lists = Pleroma.List.for_user(user, opts)
1050 res = ListView.render("lists.json", lists: lists)
1054 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1055 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1056 res = ListView.render("list.json", list: list)
1062 |> json(%{error: "Record not found"})
1066 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1067 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1068 res = ListView.render("lists.json", lists: lists)
1072 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1073 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1074 {:ok, _list} <- Pleroma.List.delete(list) do
1082 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1083 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1084 res = ListView.render("list.json", list: list)
1089 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1091 |> Enum.each(fn account_id ->
1092 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1093 %User{} = followed <- User.get_by_id(account_id) do
1094 Pleroma.List.follow(list, followed)
1101 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1103 |> Enum.each(fn account_id ->
1104 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1105 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1106 Pleroma.List.unfollow(list, followed)
1113 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1114 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1115 {:ok, users} = Pleroma.List.get_following(list) do
1117 |> put_view(AccountView)
1118 |> render("accounts.json", %{users: users, as: :user})
1122 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1123 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1124 {:ok, list} <- Pleroma.List.rename(list, title) do
1125 res = ListView.render("list.json", list: list)
1133 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1134 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1137 |> Map.put("type", "Create")
1138 |> Map.put("blocking_user", user)
1139 |> Map.put("muting_user", user)
1141 # we must filter the following list for the user to avoid leaking statuses the user
1142 # does not actually have permission to see (for more info, peruse security issue #270).
1145 |> Enum.filter(fn x -> x in user.following end)
1146 |> ActivityPub.fetch_activities_bounded(following, params)
1150 |> put_view(StatusView)
1151 |> render("index.json", %{activities: activities, for: user, as: :activity})
1156 |> json(%{error: "Error."})
1160 def index(%{assigns: %{user: user}} = conn, _params) do
1161 token = get_session(conn, :oauth_token)
1164 mastodon_emoji = mastodonized_emoji()
1166 limit = Config.get([:instance, :limit])
1169 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1171 flavour = get_user_flavour(user)
1176 streaming_api_base_url:
1177 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1178 access_token: token,
1180 domain: Pleroma.Web.Endpoint.host(),
1183 unfollow_modal: false,
1186 auto_play_gif: false,
1187 display_sensitive_media: false,
1188 reduce_motion: false,
1189 max_toot_chars: limit,
1190 mascot: "/images/pleroma-fox-tan-smol.png"
1193 delete_others_notice: present?(user.info.is_moderator),
1194 admin: present?(user.info.is_admin)
1198 default_privacy: user.info.default_scope,
1199 default_sensitive: false,
1200 allow_content_types: Config.get([:instance, :allowed_post_formats])
1202 media_attachments: %{
1203 accept_content_types: [
1219 user.info.settings ||
1249 push_subscription: nil,
1251 custom_emojis: mastodon_emoji,
1257 |> put_layout(false)
1258 |> put_view(MastodonView)
1259 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1262 |> put_session(:return_to, conn.request_path)
1263 |> redirect(to: "/web/login")
1267 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1268 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1270 with changeset <- Ecto.Changeset.change(user),
1271 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1272 {:ok, _user} <- User.update_and_set_cache(changeset) do
1277 |> put_resp_content_type("application/json")
1278 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1282 @supported_flavours ["glitch", "vanilla"]
1284 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1285 when flavour in @supported_flavours do
1286 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1288 with changeset <- Ecto.Changeset.change(user),
1289 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1290 {:ok, user} <- User.update_and_set_cache(changeset),
1291 flavour <- user.info.flavour do
1296 |> put_resp_content_type("application/json")
1297 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1301 def set_flavour(conn, _params) do
1304 |> json(%{error: "Unsupported flavour"})
1307 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1308 json(conn, get_user_flavour(user))
1311 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1315 defp get_user_flavour(_) do
1319 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1320 redirect(conn, to: local_mastodon_root_path(conn))
1323 @doc "Local Mastodon FE login init action"
1324 def login(conn, %{"code" => auth_token}) do
1325 with {:ok, app} <- get_or_make_app(),
1326 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1327 {:ok, token} <- Token.exchange_token(app, auth) do
1329 |> put_session(:oauth_token, token.token)
1330 |> redirect(to: local_mastodon_root_path(conn))
1334 @doc "Local Mastodon FE callback action"
1335 def login(conn, _) do
1336 with {:ok, app} <- get_or_make_app() do
1341 response_type: "code",
1342 client_id: app.client_id,
1344 scope: Enum.join(app.scopes, " ")
1347 redirect(conn, to: path)
1351 defp local_mastodon_root_path(conn) do
1352 case get_session(conn, :return_to) do
1354 mastodon_api_path(conn, :index, ["getting-started"])
1357 delete_session(conn, :return_to)
1362 defp get_or_make_app do
1363 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1364 scopes = ["read", "write", "follow", "push"]
1366 with %App{} = app <- Repo.get_by(App, find_attrs) do
1368 if app.scopes == scopes do
1372 |> Ecto.Changeset.change(%{scopes: scopes})
1380 App.register_changeset(
1382 Map.put(find_attrs, :scopes, scopes)
1389 def logout(conn, _) do
1392 |> redirect(to: "/")
1395 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1396 Logger.debug("Unimplemented, returning unmodified relationship")
1398 with %User{} = target <- User.get_by_id(id) do
1400 |> put_view(AccountView)
1401 |> render("relationship.json", %{user: user, target: target})
1405 def empty_array(conn, _) do
1406 Logger.debug("Unimplemented, returning an empty array")
1410 def empty_object(conn, _) do
1411 Logger.debug("Unimplemented, returning an empty object")
1415 def get_filters(%{assigns: %{user: user}} = conn, _) do
1416 filters = Filter.get_filters(user)
1417 res = FilterView.render("filters.json", filters: filters)
1422 %{assigns: %{user: user}} = conn,
1423 %{"phrase" => phrase, "context" => context} = params
1429 hide: Map.get(params, "irreversible", nil),
1430 whole_word: Map.get(params, "boolean", true)
1434 {:ok, response} = Filter.create(query)
1435 res = FilterView.render("filter.json", filter: response)
1439 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1440 filter = Filter.get(filter_id, user)
1441 res = FilterView.render("filter.json", filter: filter)
1446 %{assigns: %{user: user}} = conn,
1447 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1451 filter_id: filter_id,
1454 hide: Map.get(params, "irreversible", nil),
1455 whole_word: Map.get(params, "boolean", true)
1459 {:ok, response} = Filter.update(query)
1460 res = FilterView.render("filter.json", filter: response)
1464 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1467 filter_id: filter_id
1470 {:ok, _} = Filter.delete(query)
1476 def errors(conn, {:error, %Changeset{} = changeset}) do
1479 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1480 |> Enum.map_join(", ", fn {_k, v} -> v end)
1484 |> json(%{error: error_message})
1487 def errors(conn, {:error, :not_found}) do
1490 |> json(%{error: "Record not found"})
1493 def errors(conn, _) do
1496 |> json("Something went wrong")
1499 def suggestions(%{assigns: %{user: user}} = conn, _) do
1500 suggestions = Config.get(:suggestions)
1502 if Keyword.get(suggestions, :enabled, false) do
1503 api = Keyword.get(suggestions, :third_party_engine, "")
1504 timeout = Keyword.get(suggestions, :timeout, 5000)
1505 limit = Keyword.get(suggestions, :limit, 23)
1507 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1509 user = user.nickname
1513 |> String.replace("{{host}}", host)
1514 |> String.replace("{{user}}", user)
1516 with {:ok, %{status: 200, body: body}} <-
1521 recv_timeout: timeout,
1525 {:ok, data} <- Jason.decode(body) do
1528 |> Enum.slice(0, limit)
1533 case User.get_or_fetch(x["acct"]) do
1540 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1543 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1549 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1556 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1557 with %Activity{} = activity <- Activity.get_by_id(status_id),
1558 true <- Visibility.visible_for_user?(activity, user) do
1562 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1572 def reports(%{assigns: %{user: user}} = conn, params) do
1573 case CommonAPI.report(user, params) do
1576 |> put_view(ReportView)
1577 |> try_render("report.json", %{activity: activity})
1581 |> put_status(:bad_request)
1582 |> json(%{error: err})
1586 def try_render(conn, target, params)
1587 when is_binary(target) do
1588 res = render(conn, target, params)
1593 |> json(%{error: "Can't display this activity"})
1599 def try_render(conn, _, _) do
1602 |> json(%{error: "Can't display this activity"})
1605 defp present?(nil), do: false
1606 defp present?(false), do: false
1607 defp present?(_), do: true