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 destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
616 Notification.destroy_multiple(user, ids)
620 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
622 q = from(u in User, where: u.id in ^id)
623 targets = Repo.all(q)
626 |> put_view(AccountView)
627 |> render("relationships.json", %{user: user, targets: targets})
630 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
631 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
633 def update_media(%{assigns: %{user: user}} = conn, data) do
634 with %Object{} = object <- Repo.get(Object, data["id"]),
635 true <- Object.authorize_mutation(object, user),
636 true <- is_binary(data["description"]),
637 description <- data["description"] do
638 new_data = %{object.data | "name" => description}
642 |> Object.change(%{data: new_data})
645 attachment_data = Map.put(new_data, "id", object.id)
648 |> put_view(StatusView)
649 |> render("attachment.json", %{attachment: attachment_data})
653 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
654 with {:ok, object} <-
657 actor: User.ap_id(user),
658 description: Map.get(data, "description")
660 attachment_data = Map.put(object.data, "id", object.id)
663 |> put_view(StatusView)
664 |> render("attachment.json", %{attachment: attachment_data})
668 def favourited_by(conn, %{"id" => id}) do
669 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
670 q = from(u in User, where: u.ap_id in ^likes)
674 |> put_view(AccountView)
675 |> render(AccountView, "accounts.json", %{users: users, as: :user})
681 def reblogged_by(conn, %{"id" => id}) do
682 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
683 q = from(u in User, where: u.ap_id in ^announces)
687 |> put_view(AccountView)
688 |> render("accounts.json", %{users: users, as: :user})
694 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
695 local_only = params["local"] in [true, "True", "true", "1"]
698 [params["tag"], params["any"]]
702 |> Enum.map(&String.downcase(&1))
707 |> Enum.map(&String.downcase(&1))
712 |> Enum.map(&String.downcase(&1))
716 |> Map.put("type", "Create")
717 |> Map.put("local_only", local_only)
718 |> Map.put("blocking_user", user)
719 |> Map.put("muting_user", user)
720 |> Map.put("tag", tags)
721 |> Map.put("tag_all", tag_all)
722 |> Map.put("tag_reject", tag_reject)
723 |> ActivityPub.fetch_public_activities()
727 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
728 |> put_view(StatusView)
729 |> render("index.json", %{activities: activities, for: user, as: :activity})
732 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
733 with %User{} = user <- User.get_by_id(id),
734 followers <- MastodonAPI.get_followers(user, params) do
737 for_user && user.id == for_user.id -> followers
738 user.info.hide_followers -> []
743 |> add_link_headers(:followers, followers, user)
744 |> put_view(AccountView)
745 |> render("accounts.json", %{users: followers, as: :user})
749 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
750 with %User{} = user <- User.get_by_id(id),
751 followers <- MastodonAPI.get_friends(user, params) do
754 for_user && user.id == for_user.id -> followers
755 user.info.hide_follows -> []
760 |> add_link_headers(:following, followers, user)
761 |> put_view(AccountView)
762 |> render("accounts.json", %{users: followers, as: :user})
766 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
767 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
769 |> put_view(AccountView)
770 |> render("accounts.json", %{users: follow_requests, as: :user})
774 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
775 with %User{} = follower <- User.get_by_id(id),
776 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
778 |> put_view(AccountView)
779 |> render("relationship.json", %{user: followed, target: follower})
783 |> put_resp_content_type("application/json")
784 |> send_resp(403, Jason.encode!(%{"error" => message}))
788 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
789 with %User{} = follower <- User.get_by_id(id),
790 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
792 |> put_view(AccountView)
793 |> render("relationship.json", %{user: followed, target: follower})
797 |> put_resp_content_type("application/json")
798 |> send_resp(403, Jason.encode!(%{"error" => message}))
802 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
803 with %User{} = followed <- User.get_by_id(id),
804 false <- User.following?(follower, followed),
805 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
807 |> put_view(AccountView)
808 |> render("relationship.json", %{user: follower, target: followed})
811 followed = User.get_cached_by_id(id)
814 case conn.params["reblogs"] do
815 true -> CommonAPI.show_reblogs(follower, followed)
816 false -> CommonAPI.hide_reblogs(follower, followed)
820 |> put_view(AccountView)
821 |> render("relationship.json", %{user: follower, target: followed})
825 |> put_resp_content_type("application/json")
826 |> send_resp(403, Jason.encode!(%{"error" => message}))
830 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
831 with %User{} = followed <- User.get_by_nickname(uri),
832 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
834 |> put_view(AccountView)
835 |> render("account.json", %{user: followed, for: follower})
839 |> put_resp_content_type("application/json")
840 |> send_resp(403, Jason.encode!(%{"error" => message}))
844 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
845 with %User{} = followed <- User.get_by_id(id),
846 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
848 |> put_view(AccountView)
849 |> render("relationship.json", %{user: follower, target: followed})
853 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
854 with %User{} = muted <- User.get_by_id(id),
855 {:ok, muter} <- User.mute(muter, muted) do
857 |> put_view(AccountView)
858 |> render("relationship.json", %{user: muter, target: muted})
862 |> put_resp_content_type("application/json")
863 |> send_resp(403, Jason.encode!(%{"error" => message}))
867 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
868 with %User{} = muted <- User.get_by_id(id),
869 {:ok, muter} <- User.unmute(muter, muted) do
871 |> put_view(AccountView)
872 |> render("relationship.json", %{user: muter, target: muted})
876 |> put_resp_content_type("application/json")
877 |> send_resp(403, Jason.encode!(%{"error" => message}))
881 def mutes(%{assigns: %{user: user}} = conn, _) do
882 with muted_accounts <- User.muted_users(user) do
883 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
888 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
889 with %User{} = blocked <- User.get_by_id(id),
890 {:ok, blocker} <- User.block(blocker, blocked),
891 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
893 |> put_view(AccountView)
894 |> render("relationship.json", %{user: blocker, target: blocked})
898 |> put_resp_content_type("application/json")
899 |> send_resp(403, Jason.encode!(%{"error" => message}))
903 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
904 with %User{} = blocked <- User.get_by_id(id),
905 {:ok, blocker} <- User.unblock(blocker, blocked),
906 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
908 |> put_view(AccountView)
909 |> render("relationship.json", %{user: blocker, target: blocked})
913 |> put_resp_content_type("application/json")
914 |> send_resp(403, Jason.encode!(%{"error" => message}))
918 def blocks(%{assigns: %{user: user}} = conn, _) do
919 with blocked_accounts <- User.blocked_users(user) do
920 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
925 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
926 json(conn, info.domain_blocks || [])
929 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
930 User.block_domain(blocker, domain)
934 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
935 User.unblock_domain(blocker, domain)
939 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
940 with %User{} = subscription_target <- User.get_cached_by_id(id),
941 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
943 |> put_view(AccountView)
944 |> render("relationship.json", %{user: user, target: subscription_target})
948 |> put_resp_content_type("application/json")
949 |> send_resp(403, Jason.encode!(%{"error" => message}))
953 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
954 with %User{} = subscription_target <- User.get_cached_by_id(id),
955 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
957 |> put_view(AccountView)
958 |> render("relationship.json", %{user: user, target: subscription_target})
962 |> put_resp_content_type("application/json")
963 |> send_resp(403, Jason.encode!(%{"error" => message}))
967 def status_search(user, query) do
969 if Regex.match?(~r/https?:/, query) do
970 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
971 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
972 true <- Visibility.visible_for_user?(activity, user) do
982 where: fragment("?->>'type' = 'Create'", a.data),
983 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
986 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
991 order_by: [desc: :id]
994 Repo.all(q) ++ fetched
997 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
998 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1000 statuses = status_search(user, query)
1002 tags_path = Web.base_url() <> "/tag/"
1008 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1009 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1010 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1013 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1015 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1022 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1023 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1025 statuses = status_search(user, query)
1031 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1032 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1035 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1037 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1044 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1045 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1047 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1052 def favourites(%{assigns: %{user: user}} = conn, params) do
1055 |> Map.put("type", "Create")
1056 |> Map.put("favorited_by", user.ap_id)
1057 |> Map.put("blocking_user", user)
1060 ActivityPub.fetch_activities([], params)
1064 |> add_link_headers(:favourites, activities)
1065 |> put_view(StatusView)
1066 |> render("index.json", %{activities: activities, for: user, as: :activity})
1069 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1070 user = User.get_by_id(user.id)
1074 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1078 |> put_view(StatusView)
1079 |> render("index.json", %{activities: activities, for: user, as: :activity})
1082 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1083 lists = Pleroma.List.for_user(user, opts)
1084 res = ListView.render("lists.json", lists: lists)
1088 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1089 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1090 res = ListView.render("list.json", list: list)
1096 |> json(%{error: "Record not found"})
1100 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1101 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1102 res = ListView.render("lists.json", lists: lists)
1106 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1107 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1108 {:ok, _list} <- Pleroma.List.delete(list) do
1116 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1117 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1118 res = ListView.render("list.json", list: list)
1123 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1125 |> Enum.each(fn account_id ->
1126 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1127 %User{} = followed <- User.get_by_id(account_id) do
1128 Pleroma.List.follow(list, followed)
1135 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1137 |> Enum.each(fn account_id ->
1138 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1139 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1140 Pleroma.List.unfollow(list, followed)
1147 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1148 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1149 {:ok, users} = Pleroma.List.get_following(list) do
1151 |> put_view(AccountView)
1152 |> render("accounts.json", %{users: users, as: :user})
1156 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1157 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1158 {:ok, list} <- Pleroma.List.rename(list, title) do
1159 res = ListView.render("list.json", list: list)
1167 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1168 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1171 |> Map.put("type", "Create")
1172 |> Map.put("blocking_user", user)
1173 |> Map.put("muting_user", user)
1175 # we must filter the following list for the user to avoid leaking statuses the user
1176 # does not actually have permission to see (for more info, peruse security issue #270).
1179 |> Enum.filter(fn x -> x in user.following end)
1180 |> ActivityPub.fetch_activities_bounded(following, params)
1184 |> put_view(StatusView)
1185 |> render("index.json", %{activities: activities, for: user, as: :activity})
1190 |> json(%{error: "Error."})
1194 def index(%{assigns: %{user: user}} = conn, _params) do
1195 token = get_session(conn, :oauth_token)
1198 mastodon_emoji = mastodonized_emoji()
1200 limit = Config.get([:instance, :limit])
1203 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1205 flavour = get_user_flavour(user)
1210 streaming_api_base_url:
1211 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1212 access_token: token,
1214 domain: Pleroma.Web.Endpoint.host(),
1217 unfollow_modal: false,
1220 auto_play_gif: false,
1221 display_sensitive_media: false,
1222 reduce_motion: false,
1223 max_toot_chars: limit,
1224 mascot: "/images/pleroma-fox-tan-smol.png"
1227 delete_others_notice: present?(user.info.is_moderator),
1228 admin: present?(user.info.is_admin)
1232 default_privacy: user.info.default_scope,
1233 default_sensitive: false,
1234 allow_content_types: Config.get([:instance, :allowed_post_formats])
1236 media_attachments: %{
1237 accept_content_types: [
1253 user.info.settings ||
1283 push_subscription: nil,
1285 custom_emojis: mastodon_emoji,
1291 |> put_layout(false)
1292 |> put_view(MastodonView)
1293 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1296 |> put_session(:return_to, conn.request_path)
1297 |> redirect(to: "/web/login")
1301 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1302 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1304 with changeset <- Ecto.Changeset.change(user),
1305 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1306 {:ok, _user} <- User.update_and_set_cache(changeset) do
1311 |> put_resp_content_type("application/json")
1312 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1316 @supported_flavours ["glitch", "vanilla"]
1318 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1319 when flavour in @supported_flavours do
1320 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1322 with changeset <- Ecto.Changeset.change(user),
1323 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1324 {:ok, user} <- User.update_and_set_cache(changeset),
1325 flavour <- user.info.flavour do
1330 |> put_resp_content_type("application/json")
1331 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1335 def set_flavour(conn, _params) do
1338 |> json(%{error: "Unsupported flavour"})
1341 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1342 json(conn, get_user_flavour(user))
1345 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1349 defp get_user_flavour(_) do
1353 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1354 redirect(conn, to: local_mastodon_root_path(conn))
1357 @doc "Local Mastodon FE login init action"
1358 def login(conn, %{"code" => auth_token}) do
1359 with {:ok, app} <- get_or_make_app(),
1360 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1361 {:ok, token} <- Token.exchange_token(app, auth) do
1363 |> put_session(:oauth_token, token.token)
1364 |> redirect(to: local_mastodon_root_path(conn))
1368 @doc "Local Mastodon FE callback action"
1369 def login(conn, _) do
1370 with {:ok, app} <- get_or_make_app() do
1375 response_type: "code",
1376 client_id: app.client_id,
1378 scope: Enum.join(app.scopes, " ")
1381 redirect(conn, to: path)
1385 defp local_mastodon_root_path(conn) do
1386 case get_session(conn, :return_to) do
1388 mastodon_api_path(conn, :index, ["getting-started"])
1391 delete_session(conn, :return_to)
1396 defp get_or_make_app do
1397 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1398 scopes = ["read", "write", "follow", "push"]
1400 with %App{} = app <- Repo.get_by(App, find_attrs) do
1402 if app.scopes == scopes do
1406 |> Ecto.Changeset.change(%{scopes: scopes})
1414 App.register_changeset(
1416 Map.put(find_attrs, :scopes, scopes)
1423 def logout(conn, _) do
1426 |> redirect(to: "/")
1429 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1430 Logger.debug("Unimplemented, returning unmodified relationship")
1432 with %User{} = target <- User.get_by_id(id) do
1434 |> put_view(AccountView)
1435 |> render("relationship.json", %{user: user, target: target})
1439 def empty_array(conn, _) do
1440 Logger.debug("Unimplemented, returning an empty array")
1444 def empty_object(conn, _) do
1445 Logger.debug("Unimplemented, returning an empty object")
1449 def get_filters(%{assigns: %{user: user}} = conn, _) do
1450 filters = Filter.get_filters(user)
1451 res = FilterView.render("filters.json", filters: filters)
1456 %{assigns: %{user: user}} = conn,
1457 %{"phrase" => phrase, "context" => context} = params
1463 hide: Map.get(params, "irreversible", nil),
1464 whole_word: Map.get(params, "boolean", true)
1468 {:ok, response} = Filter.create(query)
1469 res = FilterView.render("filter.json", filter: response)
1473 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1474 filter = Filter.get(filter_id, user)
1475 res = FilterView.render("filter.json", filter: filter)
1480 %{assigns: %{user: user}} = conn,
1481 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1485 filter_id: filter_id,
1488 hide: Map.get(params, "irreversible", nil),
1489 whole_word: Map.get(params, "boolean", true)
1493 {:ok, response} = Filter.update(query)
1494 res = FilterView.render("filter.json", filter: response)
1498 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1501 filter_id: filter_id
1504 {:ok, _} = Filter.delete(query)
1510 def errors(conn, {:error, %Changeset{} = changeset}) do
1513 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1514 |> Enum.map_join(", ", fn {_k, v} -> v end)
1518 |> json(%{error: error_message})
1521 def errors(conn, {:error, :not_found}) do
1524 |> json(%{error: "Record not found"})
1527 def errors(conn, _) do
1530 |> json("Something went wrong")
1533 def suggestions(%{assigns: %{user: user}} = conn, _) do
1534 suggestions = Config.get(:suggestions)
1536 if Keyword.get(suggestions, :enabled, false) do
1537 api = Keyword.get(suggestions, :third_party_engine, "")
1538 timeout = Keyword.get(suggestions, :timeout, 5000)
1539 limit = Keyword.get(suggestions, :limit, 23)
1541 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1543 user = user.nickname
1547 |> String.replace("{{host}}", host)
1548 |> String.replace("{{user}}", user)
1550 with {:ok, %{status: 200, body: body}} <-
1555 recv_timeout: timeout,
1559 {:ok, data} <- Jason.decode(body) do
1562 |> Enum.slice(0, limit)
1567 case User.get_or_fetch(x["acct"]) do
1574 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1577 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1583 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1590 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1591 with %Activity{} = activity <- Activity.get_by_id(status_id),
1592 true <- Visibility.visible_for_user?(activity, user) do
1596 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1606 def reports(%{assigns: %{user: user}} = conn, params) do
1607 case CommonAPI.report(user, params) do
1610 |> put_view(ReportView)
1611 |> try_render("report.json", %{activity: activity})
1615 |> put_status(:bad_request)
1616 |> json(%{error: err})
1620 def try_render(conn, target, params)
1621 when is_binary(target) do
1622 res = render(conn, target, params)
1627 |> json(%{error: "Can't display this activity"})
1633 def try_render(conn, _, _) do
1636 |> json(%{error: "Can't display this activity"})
1639 defp present?(nil), do: false
1640 defp present?(false), do: false
1641 defp present?(_), do: true