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
11 alias Pleroma.Notification
14 alias Pleroma.ScheduledActivity
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Visibility
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.AppView
23 alias Pleroma.Web.MastodonAPI.FilterView
24 alias Pleroma.Web.MastodonAPI.ListView
25 alias Pleroma.Web.MastodonAPI.MastodonAPI
26 alias Pleroma.Web.MastodonAPI.MastodonView
27 alias Pleroma.Web.MastodonAPI.NotificationView
28 alias Pleroma.Web.MastodonAPI.ReportView
29 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
30 alias Pleroma.Web.MastodonAPI.StatusView
31 alias Pleroma.Web.MediaProxy
32 alias Pleroma.Web.OAuth.App
33 alias Pleroma.Web.OAuth.Authorization
34 alias Pleroma.Web.OAuth.Token
36 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
41 @httpoison Application.get_env(:pleroma, :httpoison)
42 @local_mastodon_name "Mastodon-Local"
44 action_fallback(:errors)
46 def create_app(conn, params) do
47 scopes = oauth_scopes(params, ["read"])
51 |> Map.drop(["scope", "scopes"])
52 |> Map.put("scopes", scopes)
54 with cs <- App.register_changeset(%App{}, app_attrs),
55 false <- cs.changes[:client_name] == @local_mastodon_name,
56 {:ok, app} <- Repo.insert(cs) do
59 |> render("show.json", %{app: app})
68 value_function \\ fn x -> {:ok, x} end
70 if Map.has_key?(params, params_field) do
71 case value_function.(params[params_field]) do
72 {:ok, new_value} -> Map.put(map, map_field, new_value)
80 def update_credentials(%{assigns: %{user: user}} = conn, params) do
85 |> add_if_present(params, "display_name", :name)
86 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
87 |> add_if_present(params, "avatar", :avatar, fn value ->
88 with %Plug.Upload{} <- value,
89 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
98 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
99 |> add_if_present(params, "header", :banner, fn value ->
100 with %Plug.Upload{} <- value,
101 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
108 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
110 with changeset <- User.update_changeset(user, user_params),
111 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
112 {:ok, user} <- User.update_and_set_cache(changeset) do
113 if original_user != user do
114 CommonAPI.update(user)
117 json(conn, AccountView.render("account.json", %{user: user, for: user}))
122 |> json(%{error: "Invalid request"})
126 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
127 account = AccountView.render("account.json", %{user: user, for: user})
131 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
132 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
135 |> render("short.json", %{app: app})
139 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
140 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
141 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
142 account = AccountView.render("account.json", %{user: user, for: for_user})
148 |> json(%{error: "Can't find user"})
152 @mastodon_api_level "2.5.0"
154 def masto_instance(conn, _params) do
155 instance = Config.get(:instance)
159 title: Keyword.get(instance, :name),
160 description: Keyword.get(instance, :description),
161 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
162 email: Keyword.get(instance, :email),
164 streaming_api: Pleroma.Web.Endpoint.websocket_url()
166 stats: Stats.get_stats(),
167 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
169 registrations: Pleroma.Config.get([:instance, :registrations_open]),
170 # Extra (not present in Mastodon):
171 max_toot_chars: Keyword.get(instance, :limit)
177 def peers(conn, _params) do
178 json(conn, Stats.get_peers())
181 defp mastodonized_emoji do
182 Pleroma.Emoji.get_all()
183 |> Enum.map(fn {shortcode, relative_url} ->
184 url = to_string(URI.merge(Web.base_url(), relative_url))
187 "shortcode" => shortcode,
189 "visible_in_picker" => true,
195 def custom_emojis(conn, _params) do
196 mastodon_emoji = mastodonized_emoji()
197 json(conn, mastodon_emoji)
200 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
203 |> Map.drop(["since_id", "max_id"])
206 last = List.last(activities)
207 first = List.first(activities)
213 {next_url, prev_url} =
217 Pleroma.Web.Endpoint,
220 Map.merge(params, %{max_id: min})
223 Pleroma.Web.Endpoint,
226 Map.merge(params, %{since_id: max})
232 Pleroma.Web.Endpoint,
234 Map.merge(params, %{max_id: min})
237 Pleroma.Web.Endpoint,
239 Map.merge(params, %{since_id: max})
245 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
251 def home_timeline(%{assigns: %{user: user}} = conn, params) do
254 |> Map.put("type", ["Create", "Announce"])
255 |> Map.put("blocking_user", user)
256 |> Map.put("muting_user", user)
257 |> Map.put("user", user)
260 [user.ap_id | user.following]
261 |> ActivityPub.fetch_activities(params)
262 |> ActivityPub.contain_timeline(user)
266 |> add_link_headers(:home_timeline, activities)
267 |> put_view(StatusView)
268 |> render("index.json", %{activities: activities, for: user, as: :activity})
271 def public_timeline(%{assigns: %{user: user}} = conn, params) do
272 local_only = params["local"] in [true, "True", "true", "1"]
276 |> Map.put("type", ["Create", "Announce"])
277 |> Map.put("local_only", local_only)
278 |> Map.put("blocking_user", user)
279 |> Map.put("muting_user", user)
280 |> ActivityPub.fetch_public_activities()
284 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
285 |> put_view(StatusView)
286 |> render("index.json", %{activities: activities, for: user, as: :activity})
289 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
290 with %User{} = user <- User.get_by_id(params["id"]) do
291 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
294 |> add_link_headers(:user_statuses, activities, params["id"])
295 |> put_view(StatusView)
296 |> render("index.json", %{
297 activities: activities,
304 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
307 |> Map.put("type", "Create")
308 |> Map.put("blocking_user", user)
309 |> Map.put("user", user)
310 |> Map.put(:visibility, "direct")
314 |> ActivityPub.fetch_activities_query(params)
318 |> add_link_headers(:dm_timeline, activities)
319 |> put_view(StatusView)
320 |> render("index.json", %{activities: activities, for: user, as: :activity})
323 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
324 with %Activity{} = activity <- Activity.get_by_id(id),
325 true <- Visibility.visible_for_user?(activity, user) do
327 |> put_view(StatusView)
328 |> try_render("status.json", %{activity: activity, for: user})
332 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
333 with %Activity{} = activity <- Activity.get_by_id(id),
335 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
336 "blocking_user" => user,
340 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
342 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
343 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
349 activities: grouped_activities[true] || [],
353 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
358 activities: grouped_activities[false] || [],
362 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
369 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
370 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
372 |> add_link_headers(:scheduled_statuses, scheduled_activities)
373 |> put_view(ScheduledActivityView)
374 |> render("index.json", %{scheduled_activities: scheduled_activities})
378 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
379 with %ScheduledActivity{} = scheduled_activity <-
380 ScheduledActivity.get(user, scheduled_activity_id) do
382 |> put_view(ScheduledActivityView)
383 |> render("show.json", %{scheduled_activity: scheduled_activity})
385 _ -> {:error, :not_found}
389 def update_scheduled_status(
390 %{assigns: %{user: user}} = conn,
391 %{"id" => scheduled_activity_id} = params
393 with %ScheduledActivity{} = scheduled_activity <-
394 ScheduledActivity.get(user, scheduled_activity_id),
395 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
397 |> put_view(ScheduledActivityView)
398 |> render("show.json", %{scheduled_activity: scheduled_activity})
400 nil -> {:error, :not_found}
405 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
406 with %ScheduledActivity{} = scheduled_activity <-
407 ScheduledActivity.get(user, scheduled_activity_id),
408 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
410 |> put_view(ScheduledActivityView)
411 |> render("show.json", %{scheduled_activity: scheduled_activity})
413 nil -> {:error, :not_found}
418 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
419 when length(media_ids) > 0 do
422 |> Map.put("status", ".")
424 post_status(conn, params)
427 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
430 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
433 case get_req_header(conn, "idempotency-key") do
435 _ -> Ecto.UUID.generate()
438 scheduled_at = params["scheduled_at"]
440 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
441 {:ok, scheduled_activity} =
442 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
443 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at})
447 |> put_view(ScheduledActivityView)
448 |> 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, :not_found}) do
1480 |> json(%{error: "Record not found"})
1483 def errors(conn, _) do
1486 |> json("Something went wrong")
1489 def suggestions(%{assigns: %{user: user}} = conn, _) do
1490 suggestions = Config.get(:suggestions)
1492 if Keyword.get(suggestions, :enabled, false) do
1493 api = Keyword.get(suggestions, :third_party_engine, "")
1494 timeout = Keyword.get(suggestions, :timeout, 5000)
1495 limit = Keyword.get(suggestions, :limit, 23)
1497 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1499 user = user.nickname
1503 |> String.replace("{{host}}", host)
1504 |> String.replace("{{user}}", user)
1506 with {:ok, %{status: 200, body: body}} <-
1511 recv_timeout: timeout,
1515 {:ok, data} <- Jason.decode(body) do
1518 |> Enum.slice(0, limit)
1523 case User.get_or_fetch(x["acct"]) do
1530 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1533 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1539 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1546 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1547 with %Activity{} = activity <- Activity.get_by_id(status_id),
1548 true <- Visibility.visible_for_user?(activity, user) do
1552 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1562 def reports(%{assigns: %{user: user}} = conn, params) do
1563 case CommonAPI.report(user, params) do
1566 |> put_view(ReportView)
1567 |> try_render("report.json", %{activity: activity})
1571 |> put_status(:bad_request)
1572 |> json(%{error: err})
1576 def try_render(conn, target, params)
1577 when is_binary(target) do
1578 res = render(conn, target, params)
1583 |> json(%{error: "Can't display this activity"})
1589 def try_render(conn, _, _) do
1592 |> json(%{error: "Can't display this activity"})
1595 defp present?(nil), do: false
1596 defp present?(false), do: false
1597 defp present?(_), do: true