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, tags} ->
185 url = to_string(URI.merge(Web.base_url(), relative_url))
188 "shortcode" => shortcode,
190 "visible_in_picker" => true,
192 "tags" => String.split(tags, ",")
197 def custom_emojis(conn, _params) do
198 mastodon_emoji = mastodonized_emoji()
199 json(conn, mastodon_emoji)
202 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
205 |> Map.drop(["since_id", "max_id"])
208 last = List.last(activities)
209 first = List.first(activities)
215 {next_url, prev_url} =
219 Pleroma.Web.Endpoint,
222 Map.merge(params, %{max_id: min})
225 Pleroma.Web.Endpoint,
228 Map.merge(params, %{since_id: max})
234 Pleroma.Web.Endpoint,
236 Map.merge(params, %{max_id: min})
239 Pleroma.Web.Endpoint,
241 Map.merge(params, %{since_id: max})
247 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
253 def home_timeline(%{assigns: %{user: user}} = conn, params) do
256 |> Map.put("type", ["Create", "Announce"])
257 |> Map.put("blocking_user", user)
258 |> Map.put("muting_user", user)
259 |> Map.put("user", user)
262 [user.ap_id | user.following]
263 |> ActivityPub.fetch_activities(params)
264 |> ActivityPub.contain_timeline(user)
268 |> add_link_headers(:home_timeline, activities)
269 |> put_view(StatusView)
270 |> render("index.json", %{activities: activities, for: user, as: :activity})
273 def public_timeline(%{assigns: %{user: user}} = conn, params) do
274 local_only = params["local"] in [true, "True", "true", "1"]
278 |> Map.put("type", ["Create", "Announce"])
279 |> Map.put("local_only", local_only)
280 |> Map.put("blocking_user", user)
281 |> Map.put("muting_user", user)
282 |> ActivityPub.fetch_public_activities()
286 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
287 |> put_view(StatusView)
288 |> render("index.json", %{activities: activities, for: user, as: :activity})
291 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
292 with %User{} = user <- User.get_by_id(params["id"]) do
293 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
296 |> add_link_headers(:user_statuses, activities, params["id"])
297 |> put_view(StatusView)
298 |> render("index.json", %{
299 activities: activities,
306 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
309 |> Map.put("type", "Create")
310 |> Map.put("blocking_user", user)
311 |> Map.put("user", user)
312 |> Map.put(:visibility, "direct")
316 |> ActivityPub.fetch_activities_query(params)
320 |> add_link_headers(:dm_timeline, activities)
321 |> put_view(StatusView)
322 |> render("index.json", %{activities: activities, for: user, as: :activity})
325 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
326 with %Activity{} = activity <- Activity.get_by_id(id),
327 true <- Visibility.visible_for_user?(activity, user) do
329 |> put_view(StatusView)
330 |> try_render("status.json", %{activity: activity, for: user})
334 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
335 with %Activity{} = activity <- Activity.get_by_id(id),
337 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
338 "blocking_user" => user,
342 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
344 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
345 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
351 activities: grouped_activities[true] || [],
355 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
360 activities: grouped_activities[false] || [],
364 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
371 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
372 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
374 |> add_link_headers(:scheduled_statuses, scheduled_activities)
375 |> put_view(ScheduledActivityView)
376 |> render("index.json", %{scheduled_activities: scheduled_activities})
380 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
381 with %ScheduledActivity{} = scheduled_activity <-
382 ScheduledActivity.get(user, scheduled_activity_id) do
384 |> put_view(ScheduledActivityView)
385 |> render("show.json", %{scheduled_activity: scheduled_activity})
387 _ -> {:error, :not_found}
391 def update_scheduled_status(
392 %{assigns: %{user: user}} = conn,
393 %{"id" => scheduled_activity_id} = params
395 with %ScheduledActivity{} = scheduled_activity <-
396 ScheduledActivity.get(user, scheduled_activity_id),
397 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
399 |> put_view(ScheduledActivityView)
400 |> render("show.json", %{scheduled_activity: scheduled_activity})
402 nil -> {:error, :not_found}
407 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
408 with %ScheduledActivity{} = scheduled_activity <-
409 ScheduledActivity.get(user, scheduled_activity_id),
410 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
412 |> put_view(ScheduledActivityView)
413 |> render("show.json", %{scheduled_activity: scheduled_activity})
415 nil -> {:error, :not_found}
420 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
421 when length(media_ids) > 0 do
424 |> Map.put("status", ".")
426 post_status(conn, params)
429 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
432 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
435 case get_req_header(conn, "idempotency-key") do
437 _ -> Ecto.UUID.generate()
440 scheduled_at = params["scheduled_at"]
442 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
443 with {:ok, scheduled_activity} <-
444 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
446 |> put_view(ScheduledActivityView)
447 |> render("show.json", %{scheduled_activity: scheduled_activity})
450 params = Map.drop(params, ["scheduled_at"])
453 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
454 CommonAPI.post(user, params)
458 |> put_view(StatusView)
459 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
463 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
464 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
470 |> json(%{error: "Can't delete this post"})
474 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
475 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
477 |> put_view(StatusView)
478 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
482 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
483 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
484 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
486 |> put_view(StatusView)
487 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
491 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
492 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
493 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
495 |> put_view(StatusView)
496 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
500 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
501 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
502 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
504 |> put_view(StatusView)
505 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
509 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
510 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
512 |> put_view(StatusView)
513 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
517 |> put_resp_content_type("application/json")
518 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
522 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
523 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
525 |> put_view(StatusView)
526 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
530 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
531 with %Activity{} = activity <- Activity.get_by_id(id),
532 %User{} = user <- User.get_by_nickname(user.nickname),
533 true <- Visibility.visible_for_user?(activity, user),
534 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
536 |> put_view(StatusView)
537 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
541 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
542 with %Activity{} = activity <- Activity.get_by_id(id),
543 %User{} = user <- User.get_by_nickname(user.nickname),
544 true <- Visibility.visible_for_user?(activity, user),
545 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
547 |> put_view(StatusView)
548 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
552 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
553 activity = Activity.get_by_id(id)
555 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
557 |> put_view(StatusView)
558 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
562 |> put_resp_content_type("application/json")
563 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
567 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
568 activity = Activity.get_by_id(id)
570 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
577 def notifications(%{assigns: %{user: user}} = conn, params) do
578 notifications = MastodonAPI.get_notifications(user, params)
581 |> add_link_headers(:notifications, notifications)
582 |> put_view(NotificationView)
583 |> render("index.json", %{notifications: notifications, for: user})
586 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
587 with {:ok, notification} <- Notification.get(user, id) do
589 |> put_view(NotificationView)
590 |> render("show.json", %{notification: notification, for: user})
594 |> put_resp_content_type("application/json")
595 |> send_resp(403, Jason.encode!(%{"error" => reason}))
599 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
600 Notification.clear(user)
604 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
605 with {:ok, _notif} <- Notification.dismiss(user, id) do
610 |> put_resp_content_type("application/json")
611 |> send_resp(403, Jason.encode!(%{"error" => reason}))
615 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
617 q = from(u in User, where: u.id in ^id)
618 targets = Repo.all(q)
621 |> put_view(AccountView)
622 |> render("relationships.json", %{user: user, targets: targets})
625 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
626 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
628 def update_media(%{assigns: %{user: user}} = conn, data) do
629 with %Object{} = object <- Repo.get(Object, data["id"]),
630 true <- Object.authorize_mutation(object, user),
631 true <- is_binary(data["description"]),
632 description <- data["description"] do
633 new_data = %{object.data | "name" => description}
637 |> Object.change(%{data: new_data})
640 attachment_data = Map.put(new_data, "id", object.id)
643 |> put_view(StatusView)
644 |> render("attachment.json", %{attachment: attachment_data})
648 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
649 with {:ok, object} <-
652 actor: User.ap_id(user),
653 description: Map.get(data, "description")
655 attachment_data = Map.put(object.data, "id", object.id)
658 |> put_view(StatusView)
659 |> render("attachment.json", %{attachment: attachment_data})
663 def favourited_by(conn, %{"id" => id}) do
664 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
665 q = from(u in User, where: u.ap_id in ^likes)
669 |> put_view(AccountView)
670 |> render(AccountView, "accounts.json", %{users: users, as: :user})
676 def reblogged_by(conn, %{"id" => id}) do
677 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
678 q = from(u in User, where: u.ap_id in ^announces)
682 |> put_view(AccountView)
683 |> render("accounts.json", %{users: users, as: :user})
689 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
690 local_only = params["local"] in [true, "True", "true", "1"]
693 [params["tag"], params["any"]]
697 |> Enum.map(&String.downcase(&1))
702 |> Enum.map(&String.downcase(&1))
707 |> Enum.map(&String.downcase(&1))
711 |> Map.put("type", "Create")
712 |> Map.put("local_only", local_only)
713 |> Map.put("blocking_user", user)
714 |> Map.put("muting_user", user)
715 |> Map.put("tag", tags)
716 |> Map.put("tag_all", tag_all)
717 |> Map.put("tag_reject", tag_reject)
718 |> ActivityPub.fetch_public_activities()
722 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
723 |> put_view(StatusView)
724 |> render("index.json", %{activities: activities, for: user, as: :activity})
727 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
728 with %User{} = user <- User.get_by_id(id),
729 followers <- MastodonAPI.get_followers(user, params) do
732 for_user && user.id == for_user.id -> followers
733 user.info.hide_followers -> []
738 |> add_link_headers(:followers, followers, user)
739 |> put_view(AccountView)
740 |> render("accounts.json", %{users: followers, as: :user})
744 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
745 with %User{} = user <- User.get_by_id(id),
746 followers <- MastodonAPI.get_friends(user, params) do
749 for_user && user.id == for_user.id -> followers
750 user.info.hide_follows -> []
755 |> add_link_headers(:following, followers, user)
756 |> put_view(AccountView)
757 |> render("accounts.json", %{users: followers, as: :user})
761 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
762 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
764 |> put_view(AccountView)
765 |> render("accounts.json", %{users: follow_requests, as: :user})
769 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
770 with %User{} = follower <- User.get_by_id(id),
771 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
773 |> put_view(AccountView)
774 |> render("relationship.json", %{user: followed, target: follower})
778 |> put_resp_content_type("application/json")
779 |> send_resp(403, Jason.encode!(%{"error" => message}))
783 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
784 with %User{} = follower <- User.get_by_id(id),
785 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
787 |> put_view(AccountView)
788 |> render("relationship.json", %{user: followed, target: follower})
792 |> put_resp_content_type("application/json")
793 |> send_resp(403, Jason.encode!(%{"error" => message}))
797 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
798 with %User{} = followed <- User.get_by_id(id),
799 false <- User.following?(follower, followed),
800 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
802 |> put_view(AccountView)
803 |> render("relationship.json", %{user: follower, target: followed})
806 followed = User.get_cached_by_id(id)
809 case conn.params["reblogs"] do
810 true -> CommonAPI.show_reblogs(follower, followed)
811 false -> CommonAPI.hide_reblogs(follower, followed)
815 |> put_view(AccountView)
816 |> render("relationship.json", %{user: follower, target: followed})
820 |> put_resp_content_type("application/json")
821 |> send_resp(403, Jason.encode!(%{"error" => message}))
825 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
826 with %User{} = followed <- User.get_by_nickname(uri),
827 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
829 |> put_view(AccountView)
830 |> render("account.json", %{user: followed, for: follower})
834 |> put_resp_content_type("application/json")
835 |> send_resp(403, Jason.encode!(%{"error" => message}))
839 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
840 with %User{} = followed <- User.get_by_id(id),
841 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
843 |> put_view(AccountView)
844 |> render("relationship.json", %{user: follower, target: followed})
848 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
849 with %User{} = muted <- User.get_by_id(id),
850 {:ok, muter} <- User.mute(muter, muted) do
852 |> put_view(AccountView)
853 |> render("relationship.json", %{user: muter, target: muted})
857 |> put_resp_content_type("application/json")
858 |> send_resp(403, Jason.encode!(%{"error" => message}))
862 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
863 with %User{} = muted <- User.get_by_id(id),
864 {:ok, muter} <- User.unmute(muter, muted) do
866 |> put_view(AccountView)
867 |> render("relationship.json", %{user: muter, target: muted})
871 |> put_resp_content_type("application/json")
872 |> send_resp(403, Jason.encode!(%{"error" => message}))
876 def mutes(%{assigns: %{user: user}} = conn, _) do
877 with muted_accounts <- User.muted_users(user) do
878 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
883 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
884 with %User{} = blocked <- User.get_by_id(id),
885 {:ok, blocker} <- User.block(blocker, blocked),
886 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
888 |> put_view(AccountView)
889 |> render("relationship.json", %{user: blocker, target: blocked})
893 |> put_resp_content_type("application/json")
894 |> send_resp(403, Jason.encode!(%{"error" => message}))
898 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
899 with %User{} = blocked <- User.get_by_id(id),
900 {:ok, blocker} <- User.unblock(blocker, blocked),
901 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
903 |> put_view(AccountView)
904 |> render("relationship.json", %{user: blocker, target: blocked})
908 |> put_resp_content_type("application/json")
909 |> send_resp(403, Jason.encode!(%{"error" => message}))
913 def blocks(%{assigns: %{user: user}} = conn, _) do
914 with blocked_accounts <- User.blocked_users(user) do
915 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
920 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
921 json(conn, info.domain_blocks || [])
924 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
925 User.block_domain(blocker, domain)
929 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
930 User.unblock_domain(blocker, domain)
934 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
935 with %User{} = subscription_target <- User.get_cached_by_id(id),
936 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: user, target: subscription_target})
943 |> put_resp_content_type("application/json")
944 |> send_resp(403, Jason.encode!(%{"error" => message}))
948 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
949 with %User{} = subscription_target <- User.get_cached_by_id(id),
950 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
952 |> put_view(AccountView)
953 |> render("relationship.json", %{user: user, target: subscription_target})
957 |> put_resp_content_type("application/json")
958 |> send_resp(403, Jason.encode!(%{"error" => message}))
962 def status_search(user, query) do
964 if Regex.match?(~r/https?:/, query) do
965 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
966 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
967 true <- Visibility.visible_for_user?(activity, user) do
977 where: fragment("?->>'type' = 'Create'", a.data),
978 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
981 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
986 order_by: [desc: :id]
989 Repo.all(q) ++ fetched
992 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
993 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
995 statuses = status_search(user, query)
997 tags_path = Web.base_url() <> "/tag/"
1003 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1004 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1005 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1008 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1010 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1017 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1018 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1020 statuses = status_search(user, query)
1026 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1027 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1030 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1032 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1039 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1040 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1042 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1047 def favourites(%{assigns: %{user: user}} = conn, params) do
1050 |> Map.put("type", "Create")
1051 |> Map.put("favorited_by", user.ap_id)
1052 |> Map.put("blocking_user", user)
1055 ActivityPub.fetch_activities([], params)
1059 |> add_link_headers(:favourites, activities)
1060 |> put_view(StatusView)
1061 |> render("index.json", %{activities: activities, for: user, as: :activity})
1064 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1065 user = User.get_by_id(user.id)
1069 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1073 |> put_view(StatusView)
1074 |> render("index.json", %{activities: activities, for: user, as: :activity})
1077 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1078 lists = Pleroma.List.for_user(user, opts)
1079 res = ListView.render("lists.json", lists: lists)
1083 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1084 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1085 res = ListView.render("list.json", list: list)
1091 |> json(%{error: "Record not found"})
1095 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1096 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1097 res = ListView.render("lists.json", lists: lists)
1101 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1102 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1103 {:ok, _list} <- Pleroma.List.delete(list) do
1111 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1112 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1113 res = ListView.render("list.json", list: list)
1118 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1120 |> Enum.each(fn account_id ->
1121 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1122 %User{} = followed <- User.get_by_id(account_id) do
1123 Pleroma.List.follow(list, followed)
1130 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1132 |> Enum.each(fn account_id ->
1133 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1134 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1135 Pleroma.List.unfollow(list, followed)
1142 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1143 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1144 {:ok, users} = Pleroma.List.get_following(list) do
1146 |> put_view(AccountView)
1147 |> render("accounts.json", %{users: users, as: :user})
1151 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1152 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1153 {:ok, list} <- Pleroma.List.rename(list, title) do
1154 res = ListView.render("list.json", list: list)
1162 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1163 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1166 |> Map.put("type", "Create")
1167 |> Map.put("blocking_user", user)
1168 |> Map.put("muting_user", user)
1170 # we must filter the following list for the user to avoid leaking statuses the user
1171 # does not actually have permission to see (for more info, peruse security issue #270).
1174 |> Enum.filter(fn x -> x in user.following end)
1175 |> ActivityPub.fetch_activities_bounded(following, params)
1179 |> put_view(StatusView)
1180 |> render("index.json", %{activities: activities, for: user, as: :activity})
1185 |> json(%{error: "Error."})
1189 def index(%{assigns: %{user: user}} = conn, _params) do
1190 token = get_session(conn, :oauth_token)
1193 mastodon_emoji = mastodonized_emoji()
1195 limit = Config.get([:instance, :limit])
1198 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1200 flavour = get_user_flavour(user)
1205 streaming_api_base_url:
1206 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1207 access_token: token,
1209 domain: Pleroma.Web.Endpoint.host(),
1212 unfollow_modal: false,
1215 auto_play_gif: false,
1216 display_sensitive_media: false,
1217 reduce_motion: false,
1218 max_toot_chars: limit,
1219 mascot: "/images/pleroma-fox-tan-smol.png"
1222 delete_others_notice: present?(user.info.is_moderator),
1223 admin: present?(user.info.is_admin)
1227 default_privacy: user.info.default_scope,
1228 default_sensitive: false,
1229 allow_content_types: Config.get([:instance, :allowed_post_formats])
1231 media_attachments: %{
1232 accept_content_types: [
1248 user.info.settings ||
1278 push_subscription: nil,
1280 custom_emojis: mastodon_emoji,
1286 |> put_layout(false)
1287 |> put_view(MastodonView)
1288 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1291 |> put_session(:return_to, conn.request_path)
1292 |> redirect(to: "/web/login")
1296 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1297 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1299 with changeset <- Ecto.Changeset.change(user),
1300 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1301 {:ok, _user} <- User.update_and_set_cache(changeset) do
1306 |> put_resp_content_type("application/json")
1307 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1311 @supported_flavours ["glitch", "vanilla"]
1313 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1314 when flavour in @supported_flavours do
1315 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1317 with changeset <- Ecto.Changeset.change(user),
1318 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1319 {:ok, user} <- User.update_and_set_cache(changeset),
1320 flavour <- user.info.flavour do
1325 |> put_resp_content_type("application/json")
1326 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1330 def set_flavour(conn, _params) do
1333 |> json(%{error: "Unsupported flavour"})
1336 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1337 json(conn, get_user_flavour(user))
1340 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1344 defp get_user_flavour(_) do
1348 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1349 redirect(conn, to: local_mastodon_root_path(conn))
1352 @doc "Local Mastodon FE login init action"
1353 def login(conn, %{"code" => auth_token}) do
1354 with {:ok, app} <- get_or_make_app(),
1355 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1356 {:ok, token} <- Token.exchange_token(app, auth) do
1358 |> put_session(:oauth_token, token.token)
1359 |> redirect(to: local_mastodon_root_path(conn))
1363 @doc "Local Mastodon FE callback action"
1364 def login(conn, _) do
1365 with {:ok, app} <- get_or_make_app() do
1370 response_type: "code",
1371 client_id: app.client_id,
1373 scope: Enum.join(app.scopes, " ")
1376 redirect(conn, to: path)
1380 defp local_mastodon_root_path(conn) do
1381 case get_session(conn, :return_to) do
1383 mastodon_api_path(conn, :index, ["getting-started"])
1386 delete_session(conn, :return_to)
1391 defp get_or_make_app do
1392 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1393 scopes = ["read", "write", "follow", "push"]
1395 with %App{} = app <- Repo.get_by(App, find_attrs) do
1397 if app.scopes == scopes do
1401 |> Ecto.Changeset.change(%{scopes: scopes})
1409 App.register_changeset(
1411 Map.put(find_attrs, :scopes, scopes)
1418 def logout(conn, _) do
1421 |> redirect(to: "/")
1424 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1425 Logger.debug("Unimplemented, returning unmodified relationship")
1427 with %User{} = target <- User.get_by_id(id) do
1429 |> put_view(AccountView)
1430 |> render("relationship.json", %{user: user, target: target})
1434 def empty_array(conn, _) do
1435 Logger.debug("Unimplemented, returning an empty array")
1439 def empty_object(conn, _) do
1440 Logger.debug("Unimplemented, returning an empty object")
1444 def get_filters(%{assigns: %{user: user}} = conn, _) do
1445 filters = Filter.get_filters(user)
1446 res = FilterView.render("filters.json", filters: filters)
1451 %{assigns: %{user: user}} = conn,
1452 %{"phrase" => phrase, "context" => context} = params
1458 hide: Map.get(params, "irreversible", nil),
1459 whole_word: Map.get(params, "boolean", true)
1463 {:ok, response} = Filter.create(query)
1464 res = FilterView.render("filter.json", filter: response)
1468 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1469 filter = Filter.get(filter_id, user)
1470 res = FilterView.render("filter.json", filter: filter)
1475 %{assigns: %{user: user}} = conn,
1476 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1480 filter_id: filter_id,
1483 hide: Map.get(params, "irreversible", nil),
1484 whole_word: Map.get(params, "boolean", true)
1488 {:ok, response} = Filter.update(query)
1489 res = FilterView.render("filter.json", filter: response)
1493 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1496 filter_id: filter_id
1499 {:ok, _} = Filter.delete(query)
1505 def errors(conn, {:error, %Changeset{} = changeset}) do
1508 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1509 |> Enum.map_join(", ", fn {_k, v} -> v end)
1513 |> json(%{error: error_message})
1516 def errors(conn, {:error, :not_found}) do
1519 |> json(%{error: "Record not found"})
1522 def errors(conn, _) do
1525 |> json("Something went wrong")
1528 def suggestions(%{assigns: %{user: user}} = conn, _) do
1529 suggestions = Config.get(:suggestions)
1531 if Keyword.get(suggestions, :enabled, false) do
1532 api = Keyword.get(suggestions, :third_party_engine, "")
1533 timeout = Keyword.get(suggestions, :timeout, 5000)
1534 limit = Keyword.get(suggestions, :limit, 23)
1536 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1538 user = user.nickname
1542 |> String.replace("{{host}}", host)
1543 |> String.replace("{{user}}", user)
1545 with {:ok, %{status: 200, body: body}} <-
1550 recv_timeout: timeout,
1554 {:ok, data} <- Jason.decode(body) do
1557 |> Enum.slice(0, limit)
1562 case User.get_or_fetch(x["acct"]) do
1569 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1572 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1578 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1585 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1586 with %Activity{} = activity <- Activity.get_by_id(status_id),
1587 true <- Visibility.visible_for_user?(activity, user) do
1591 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1601 def reports(%{assigns: %{user: user}} = conn, params) do
1602 case CommonAPI.report(user, params) do
1605 |> put_view(ReportView)
1606 |> try_render("report.json", %{activity: activity})
1610 |> put_status(:bad_request)
1611 |> json(%{error: err})
1615 def try_render(conn, target, params)
1616 when is_binary(target) do
1617 res = render(conn, target, params)
1622 |> json(%{error: "Can't display this activity"})
1628 def try_render(conn, _, _) do
1631 |> json(%{error: "Can't display this activity"})
1634 defp present?(nil), do: false
1635 defp present?(false), do: false
1636 defp present?(_), do: true