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 status_search(user, query) do
936 if Regex.match?(~r/https?:/, query) do
937 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
938 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
939 true <- Visibility.visible_for_user?(activity, user) do
949 where: fragment("?->>'type' = 'Create'", a.data),
950 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
953 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
958 order_by: [desc: :id]
961 Repo.all(q) ++ fetched
964 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
965 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
967 statuses = status_search(user, query)
969 tags_path = Web.base_url() <> "/tag/"
975 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
976 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
977 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
980 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
982 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
989 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
990 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
992 statuses = status_search(user, query)
998 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
999 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1002 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1004 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1011 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1012 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1014 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1019 def favourites(%{assigns: %{user: user}} = conn, params) do
1022 |> Map.put("type", "Create")
1023 |> Map.put("favorited_by", user.ap_id)
1024 |> Map.put("blocking_user", user)
1027 ActivityPub.fetch_activities([], params)
1031 |> add_link_headers(:favourites, activities)
1032 |> put_view(StatusView)
1033 |> render("index.json", %{activities: activities, for: user, as: :activity})
1036 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1037 user = User.get_by_id(user.id)
1041 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1045 |> put_view(StatusView)
1046 |> render("index.json", %{activities: activities, for: user, as: :activity})
1049 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1050 lists = Pleroma.List.for_user(user, opts)
1051 res = ListView.render("lists.json", lists: lists)
1055 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1056 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1057 res = ListView.render("list.json", list: list)
1063 |> json(%{error: "Record not found"})
1067 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1068 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1069 res = ListView.render("lists.json", lists: lists)
1073 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1074 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1075 {:ok, _list} <- Pleroma.List.delete(list) do
1083 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1084 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1085 res = ListView.render("list.json", list: list)
1090 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1092 |> Enum.each(fn account_id ->
1093 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1094 %User{} = followed <- User.get_by_id(account_id) do
1095 Pleroma.List.follow(list, followed)
1102 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1104 |> Enum.each(fn account_id ->
1105 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1106 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1107 Pleroma.List.unfollow(list, followed)
1114 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1115 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1116 {:ok, users} = Pleroma.List.get_following(list) do
1118 |> put_view(AccountView)
1119 |> render("accounts.json", %{users: users, as: :user})
1123 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1124 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1125 {:ok, list} <- Pleroma.List.rename(list, title) do
1126 res = ListView.render("list.json", list: list)
1134 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1135 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1138 |> Map.put("type", "Create")
1139 |> Map.put("blocking_user", user)
1140 |> Map.put("muting_user", user)
1142 # we must filter the following list for the user to avoid leaking statuses the user
1143 # does not actually have permission to see (for more info, peruse security issue #270).
1146 |> Enum.filter(fn x -> x in user.following end)
1147 |> ActivityPub.fetch_activities_bounded(following, params)
1151 |> put_view(StatusView)
1152 |> render("index.json", %{activities: activities, for: user, as: :activity})
1157 |> json(%{error: "Error."})
1161 def index(%{assigns: %{user: user}} = conn, _params) do
1162 token = get_session(conn, :oauth_token)
1165 mastodon_emoji = mastodonized_emoji()
1167 limit = Config.get([:instance, :limit])
1170 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1172 flavour = get_user_flavour(user)
1177 streaming_api_base_url:
1178 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1179 access_token: token,
1181 domain: Pleroma.Web.Endpoint.host(),
1184 unfollow_modal: false,
1187 auto_play_gif: false,
1188 display_sensitive_media: false,
1189 reduce_motion: false,
1190 max_toot_chars: limit,
1191 mascot: "/images/pleroma-fox-tan-smol.png"
1194 delete_others_notice: present?(user.info.is_moderator),
1195 admin: present?(user.info.is_admin)
1199 default_privacy: user.info.default_scope,
1200 default_sensitive: false,
1201 allow_content_types: Config.get([:instance, :allowed_post_formats])
1203 media_attachments: %{
1204 accept_content_types: [
1220 user.info.settings ||
1250 push_subscription: nil,
1252 custom_emojis: mastodon_emoji,
1258 |> put_layout(false)
1259 |> put_view(MastodonView)
1260 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1263 |> put_session(:return_to, conn.request_path)
1264 |> redirect(to: "/web/login")
1268 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1269 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1271 with changeset <- Ecto.Changeset.change(user),
1272 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1273 {:ok, _user} <- User.update_and_set_cache(changeset) do
1278 |> put_resp_content_type("application/json")
1279 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1283 @supported_flavours ["glitch", "vanilla"]
1285 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1286 when flavour in @supported_flavours do
1287 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1289 with changeset <- Ecto.Changeset.change(user),
1290 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1291 {:ok, user} <- User.update_and_set_cache(changeset),
1292 flavour <- user.info.flavour do
1297 |> put_resp_content_type("application/json")
1298 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1302 def set_flavour(conn, _params) do
1305 |> json(%{error: "Unsupported flavour"})
1308 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1309 json(conn, get_user_flavour(user))
1312 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1316 defp get_user_flavour(_) do
1320 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1321 redirect(conn, to: local_mastodon_root_path(conn))
1324 @doc "Local Mastodon FE login init action"
1325 def login(conn, %{"code" => auth_token}) do
1326 with {:ok, app} <- get_or_make_app(),
1327 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1328 {:ok, token} <- Token.exchange_token(app, auth) do
1330 |> put_session(:oauth_token, token.token)
1331 |> redirect(to: local_mastodon_root_path(conn))
1335 @doc "Local Mastodon FE callback action"
1336 def login(conn, _) do
1337 with {:ok, app} <- get_or_make_app() do
1342 response_type: "code",
1343 client_id: app.client_id,
1345 scope: Enum.join(app.scopes, " ")
1348 redirect(conn, to: path)
1352 defp local_mastodon_root_path(conn) do
1353 case get_session(conn, :return_to) do
1355 mastodon_api_path(conn, :index, ["getting-started"])
1358 delete_session(conn, :return_to)
1363 defp get_or_make_app do
1364 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1365 scopes = ["read", "write", "follow", "push"]
1367 with %App{} = app <- Repo.get_by(App, find_attrs) do
1369 if app.scopes == scopes do
1373 |> Ecto.Changeset.change(%{scopes: scopes})
1381 App.register_changeset(
1383 Map.put(find_attrs, :scopes, scopes)
1390 def logout(conn, _) do
1393 |> redirect(to: "/")
1396 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1397 Logger.debug("Unimplemented, returning unmodified relationship")
1399 with %User{} = target <- User.get_by_id(id) do
1401 |> put_view(AccountView)
1402 |> render("relationship.json", %{user: user, target: target})
1406 def empty_array(conn, _) do
1407 Logger.debug("Unimplemented, returning an empty array")
1411 def empty_object(conn, _) do
1412 Logger.debug("Unimplemented, returning an empty object")
1416 def get_filters(%{assigns: %{user: user}} = conn, _) do
1417 filters = Filter.get_filters(user)
1418 res = FilterView.render("filters.json", filters: filters)
1423 %{assigns: %{user: user}} = conn,
1424 %{"phrase" => phrase, "context" => context} = params
1430 hide: Map.get(params, "irreversible", nil),
1431 whole_word: Map.get(params, "boolean", true)
1435 {:ok, response} = Filter.create(query)
1436 res = FilterView.render("filter.json", filter: response)
1440 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1441 filter = Filter.get(filter_id, user)
1442 res = FilterView.render("filter.json", filter: filter)
1447 %{assigns: %{user: user}} = conn,
1448 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1452 filter_id: filter_id,
1455 hide: Map.get(params, "irreversible", nil),
1456 whole_word: Map.get(params, "boolean", true)
1460 {:ok, response} = Filter.update(query)
1461 res = FilterView.render("filter.json", filter: response)
1465 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1468 filter_id: filter_id
1471 {:ok, _} = Filter.delete(query)
1477 def errors(conn, {:error, %Changeset{} = changeset}) do
1480 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1481 |> Enum.map_join(", ", fn {_k, v} -> v end)
1485 |> json(%{error: error_message})
1488 def errors(conn, {:error, :not_found}) do
1491 |> json(%{error: "Record not found"})
1494 def errors(conn, _) do
1497 |> json("Something went wrong")
1500 def suggestions(%{assigns: %{user: user}} = conn, _) do
1501 suggestions = Config.get(:suggestions)
1503 if Keyword.get(suggestions, :enabled, false) do
1504 api = Keyword.get(suggestions, :third_party_engine, "")
1505 timeout = Keyword.get(suggestions, :timeout, 5000)
1506 limit = Keyword.get(suggestions, :limit, 23)
1508 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1510 user = user.nickname
1514 |> String.replace("{{host}}", host)
1515 |> String.replace("{{user}}", user)
1517 with {:ok, %{status: 200, body: body}} <-
1522 recv_timeout: timeout,
1526 {:ok, data} <- Jason.decode(body) do
1529 |> Enum.slice(0, limit)
1534 case User.get_or_fetch(x["acct"]) do
1541 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1544 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1550 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1557 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1558 with %Activity{} = activity <- Activity.get_by_id(status_id),
1559 true <- Visibility.visible_for_user?(activity, user) do
1563 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1573 def reports(%{assigns: %{user: user}} = conn, params) do
1574 case CommonAPI.report(user, params) do
1577 |> put_view(ReportView)
1578 |> try_render("report.json", %{activity: activity})
1582 |> put_status(:bad_request)
1583 |> json(%{error: err})
1587 def try_render(conn, target, params)
1588 when is_binary(target) do
1589 res = render(conn, target, params)
1594 |> json(%{error: "Can't display this activity"})
1600 def try_render(conn, _, _) do
1603 |> json(%{error: "Can't display this activity"})
1606 defp present?(nil), do: false
1607 defp present?(false), do: false
1608 defp present?(_), do: true