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 {:ok, scheduled_activity} <-
394 ScheduledActivity.update(user, scheduled_activity_id, params) do
396 |> put_view(ScheduledActivityView)
397 |> render("show.json", %{scheduled_activity: scheduled_activity})
401 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
402 with {:ok, %ScheduledActivity{}} <- ScheduledActivity.delete(user, scheduled_activity_id) do
408 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
409 when length(media_ids) > 0 do
412 |> Map.put("status", ".")
414 post_status(conn, params)
417 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
420 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
423 case get_req_header(conn, "idempotency-key") do
425 _ -> Ecto.UUID.generate()
429 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
432 |> put_view(StatusView)
433 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
436 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
437 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
443 |> json(%{error: "Can't delete this post"})
447 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
448 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
450 |> put_view(StatusView)
451 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
455 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
456 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
457 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
459 |> put_view(StatusView)
460 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
464 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
465 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
466 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
468 |> put_view(StatusView)
469 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
473 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
474 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
475 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
477 |> put_view(StatusView)
478 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
482 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
483 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
485 |> put_view(StatusView)
486 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
490 |> put_resp_content_type("application/json")
491 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
495 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
496 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
498 |> put_view(StatusView)
499 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
503 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
504 with %Activity{} = activity <- Activity.get_by_id(id),
505 %User{} = user <- User.get_by_nickname(user.nickname),
506 true <- Visibility.visible_for_user?(activity, user),
507 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
509 |> put_view(StatusView)
510 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
514 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
515 with %Activity{} = activity <- Activity.get_by_id(id),
516 %User{} = user <- User.get_by_nickname(user.nickname),
517 true <- Visibility.visible_for_user?(activity, user),
518 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
525 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
526 activity = Activity.get_by_id(id)
528 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
530 |> put_view(StatusView)
531 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
535 |> put_resp_content_type("application/json")
536 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
540 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
541 activity = Activity.get_by_id(id)
543 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
545 |> put_view(StatusView)
546 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
550 def notifications(%{assigns: %{user: user}} = conn, params) do
551 notifications = MastodonAPI.get_notifications(user, params)
554 |> add_link_headers(:notifications, notifications)
555 |> put_view(NotificationView)
556 |> render("index.json", %{notifications: notifications, for: user})
559 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
560 with {:ok, notification} <- Notification.get(user, id) do
562 |> put_view(NotificationView)
563 |> render("show.json", %{notification: notification, for: user})
567 |> put_resp_content_type("application/json")
568 |> send_resp(403, Jason.encode!(%{"error" => reason}))
572 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
573 Notification.clear(user)
577 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
578 with {:ok, _notif} <- Notification.dismiss(user, id) do
583 |> put_resp_content_type("application/json")
584 |> send_resp(403, Jason.encode!(%{"error" => reason}))
588 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
590 q = from(u in User, where: u.id in ^id)
591 targets = Repo.all(q)
594 |> put_view(AccountView)
595 |> render("relationships.json", %{user: user, targets: targets})
598 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
599 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
601 def update_media(%{assigns: %{user: user}} = conn, data) do
602 with %Object{} = object <- Repo.get(Object, data["id"]),
603 true <- Object.authorize_mutation(object, user),
604 true <- is_binary(data["description"]),
605 description <- data["description"] do
606 new_data = %{object.data | "name" => description}
610 |> Object.change(%{data: new_data})
613 attachment_data = Map.put(new_data, "id", object.id)
616 |> put_view(StatusView)
617 |> render("attachment.json", %{attachment: attachment_data})
621 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
622 with {:ok, object} <-
625 actor: User.ap_id(user),
626 description: Map.get(data, "description")
628 attachment_data = Map.put(object.data, "id", object.id)
631 |> put_view(StatusView)
632 |> render("attachment.json", %{attachment: attachment_data})
636 def favourited_by(conn, %{"id" => id}) do
637 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
638 q = from(u in User, where: u.ap_id in ^likes)
642 |> put_view(AccountView)
643 |> render(AccountView, "accounts.json", %{users: users, as: :user})
649 def reblogged_by(conn, %{"id" => id}) do
650 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
651 q = from(u in User, where: u.ap_id in ^announces)
655 |> put_view(AccountView)
656 |> render("accounts.json", %{users: users, as: :user})
662 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
663 local_only = params["local"] in [true, "True", "true", "1"]
666 [params["tag"], params["any"]]
670 |> Enum.map(&String.downcase(&1))
675 |> Enum.map(&String.downcase(&1))
680 |> Enum.map(&String.downcase(&1))
684 |> Map.put("type", "Create")
685 |> Map.put("local_only", local_only)
686 |> Map.put("blocking_user", user)
687 |> Map.put("muting_user", user)
688 |> Map.put("tag", tags)
689 |> Map.put("tag_all", tag_all)
690 |> Map.put("tag_reject", tag_reject)
691 |> ActivityPub.fetch_public_activities()
695 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
696 |> put_view(StatusView)
697 |> render("index.json", %{activities: activities, for: user, as: :activity})
700 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
701 with %User{} = user <- User.get_by_id(id),
702 followers <- MastodonAPI.get_followers(user, params) do
705 for_user && user.id == for_user.id -> followers
706 user.info.hide_followers -> []
711 |> add_link_headers(:followers, followers, user)
712 |> put_view(AccountView)
713 |> render("accounts.json", %{users: followers, as: :user})
717 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
718 with %User{} = user <- User.get_by_id(id),
719 followers <- MastodonAPI.get_friends(user, params) do
722 for_user && user.id == for_user.id -> followers
723 user.info.hide_follows -> []
728 |> add_link_headers(:following, followers, user)
729 |> put_view(AccountView)
730 |> render("accounts.json", %{users: followers, as: :user})
734 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
735 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
737 |> put_view(AccountView)
738 |> render("accounts.json", %{users: follow_requests, as: :user})
742 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
743 with %User{} = follower <- User.get_by_id(id),
744 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
746 |> put_view(AccountView)
747 |> render("relationship.json", %{user: followed, target: follower})
751 |> put_resp_content_type("application/json")
752 |> send_resp(403, Jason.encode!(%{"error" => message}))
756 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
757 with %User{} = follower <- User.get_by_id(id),
758 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
760 |> put_view(AccountView)
761 |> render("relationship.json", %{user: followed, target: follower})
765 |> put_resp_content_type("application/json")
766 |> send_resp(403, Jason.encode!(%{"error" => message}))
770 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
771 with %User{} = followed <- User.get_by_id(id),
772 false <- User.following?(follower, followed),
773 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
775 |> put_view(AccountView)
776 |> render("relationship.json", %{user: follower, target: followed})
779 followed = User.get_cached_by_id(id)
782 case conn.params["reblogs"] do
783 true -> CommonAPI.show_reblogs(follower, followed)
784 false -> CommonAPI.hide_reblogs(follower, followed)
788 |> put_view(AccountView)
789 |> render("relationship.json", %{user: follower, target: followed})
793 |> put_resp_content_type("application/json")
794 |> send_resp(403, Jason.encode!(%{"error" => message}))
798 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
799 with %User{} = followed <- User.get_by_nickname(uri),
800 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
802 |> put_view(AccountView)
803 |> render("account.json", %{user: followed, for: follower})
807 |> put_resp_content_type("application/json")
808 |> send_resp(403, Jason.encode!(%{"error" => message}))
812 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
813 with %User{} = followed <- User.get_by_id(id),
814 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
816 |> put_view(AccountView)
817 |> render("relationship.json", %{user: follower, target: followed})
821 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
822 with %User{} = muted <- User.get_by_id(id),
823 {:ok, muter} <- User.mute(muter, muted) do
825 |> put_view(AccountView)
826 |> render("relationship.json", %{user: muter, target: muted})
830 |> put_resp_content_type("application/json")
831 |> send_resp(403, Jason.encode!(%{"error" => message}))
835 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
836 with %User{} = muted <- User.get_by_id(id),
837 {:ok, muter} <- User.unmute(muter, muted) do
839 |> put_view(AccountView)
840 |> render("relationship.json", %{user: muter, target: muted})
844 |> put_resp_content_type("application/json")
845 |> send_resp(403, Jason.encode!(%{"error" => message}))
849 def mutes(%{assigns: %{user: user}} = conn, _) do
850 with muted_accounts <- User.muted_users(user) do
851 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
856 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
857 with %User{} = blocked <- User.get_by_id(id),
858 {:ok, blocker} <- User.block(blocker, blocked),
859 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
861 |> put_view(AccountView)
862 |> render("relationship.json", %{user: blocker, target: blocked})
866 |> put_resp_content_type("application/json")
867 |> send_resp(403, Jason.encode!(%{"error" => message}))
871 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
872 with %User{} = blocked <- User.get_by_id(id),
873 {:ok, blocker} <- User.unblock(blocker, blocked),
874 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
876 |> put_view(AccountView)
877 |> render("relationship.json", %{user: blocker, target: blocked})
881 |> put_resp_content_type("application/json")
882 |> send_resp(403, Jason.encode!(%{"error" => message}))
886 def blocks(%{assigns: %{user: user}} = conn, _) do
887 with blocked_accounts <- User.blocked_users(user) do
888 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
893 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
894 json(conn, info.domain_blocks || [])
897 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
898 User.block_domain(blocker, domain)
902 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
903 User.unblock_domain(blocker, domain)
907 def status_search(user, query) do
909 if Regex.match?(~r/https?:/, query) do
910 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
911 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
912 true <- Visibility.visible_for_user?(activity, user) do
922 where: fragment("?->>'type' = 'Create'", a.data),
923 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
926 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
931 order_by: [desc: :id]
934 Repo.all(q) ++ fetched
937 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
938 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
940 statuses = status_search(user, query)
942 tags_path = Web.base_url() <> "/tag/"
948 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
949 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
950 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
953 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
955 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
962 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
963 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
965 statuses = status_search(user, query)
971 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
972 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
975 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
977 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
984 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
985 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
987 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
992 def favourites(%{assigns: %{user: user}} = conn, params) do
995 |> Map.put("type", "Create")
996 |> Map.put("favorited_by", user.ap_id)
997 |> Map.put("blocking_user", user)
1000 ActivityPub.fetch_activities([], params)
1004 |> add_link_headers(:favourites, activities)
1005 |> put_view(StatusView)
1006 |> render("index.json", %{activities: activities, for: user, as: :activity})
1009 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1010 user = User.get_by_id(user.id)
1014 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1018 |> put_view(StatusView)
1019 |> render("index.json", %{activities: activities, for: user, as: :activity})
1022 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1023 lists = Pleroma.List.for_user(user, opts)
1024 res = ListView.render("lists.json", lists: lists)
1028 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1029 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1030 res = ListView.render("list.json", list: list)
1036 |> json(%{error: "Record not found"})
1040 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1041 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1042 res = ListView.render("lists.json", lists: lists)
1046 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1047 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1048 {:ok, _list} <- Pleroma.List.delete(list) do
1056 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1057 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1058 res = ListView.render("list.json", list: list)
1063 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1065 |> Enum.each(fn account_id ->
1066 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1067 %User{} = followed <- User.get_by_id(account_id) do
1068 Pleroma.List.follow(list, followed)
1075 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1077 |> Enum.each(fn account_id ->
1078 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1079 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1080 Pleroma.List.unfollow(list, followed)
1087 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1088 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1089 {:ok, users} = Pleroma.List.get_following(list) do
1091 |> put_view(AccountView)
1092 |> render("accounts.json", %{users: users, as: :user})
1096 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1097 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1098 {:ok, list} <- Pleroma.List.rename(list, title) do
1099 res = ListView.render("list.json", list: list)
1107 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1108 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1111 |> Map.put("type", "Create")
1112 |> Map.put("blocking_user", user)
1113 |> Map.put("muting_user", user)
1115 # we must filter the following list for the user to avoid leaking statuses the user
1116 # does not actually have permission to see (for more info, peruse security issue #270).
1119 |> Enum.filter(fn x -> x in user.following end)
1120 |> ActivityPub.fetch_activities_bounded(following, params)
1124 |> put_view(StatusView)
1125 |> render("index.json", %{activities: activities, for: user, as: :activity})
1130 |> json(%{error: "Error."})
1134 def index(%{assigns: %{user: user}} = conn, _params) do
1135 token = get_session(conn, :oauth_token)
1138 mastodon_emoji = mastodonized_emoji()
1140 limit = Config.get([:instance, :limit])
1143 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1145 flavour = get_user_flavour(user)
1150 streaming_api_base_url:
1151 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1152 access_token: token,
1154 domain: Pleroma.Web.Endpoint.host(),
1157 unfollow_modal: false,
1160 auto_play_gif: false,
1161 display_sensitive_media: false,
1162 reduce_motion: false,
1163 max_toot_chars: limit,
1164 mascot: "/images/pleroma-fox-tan-smol.png"
1167 delete_others_notice: present?(user.info.is_moderator),
1168 admin: present?(user.info.is_admin)
1172 default_privacy: user.info.default_scope,
1173 default_sensitive: false,
1174 allow_content_types: Config.get([:instance, :allowed_post_formats])
1176 media_attachments: %{
1177 accept_content_types: [
1193 user.info.settings ||
1223 push_subscription: nil,
1225 custom_emojis: mastodon_emoji,
1231 |> put_layout(false)
1232 |> put_view(MastodonView)
1233 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1236 |> put_session(:return_to, conn.request_path)
1237 |> redirect(to: "/web/login")
1241 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1242 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1244 with changeset <- Ecto.Changeset.change(user),
1245 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1246 {:ok, _user} <- User.update_and_set_cache(changeset) do
1251 |> put_resp_content_type("application/json")
1252 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1256 @supported_flavours ["glitch", "vanilla"]
1258 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1259 when flavour in @supported_flavours do
1260 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1262 with changeset <- Ecto.Changeset.change(user),
1263 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1264 {:ok, user} <- User.update_and_set_cache(changeset),
1265 flavour <- user.info.flavour do
1270 |> put_resp_content_type("application/json")
1271 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1275 def set_flavour(conn, _params) do
1278 |> json(%{error: "Unsupported flavour"})
1281 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1282 json(conn, get_user_flavour(user))
1285 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1289 defp get_user_flavour(_) do
1293 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1294 redirect(conn, to: local_mastodon_root_path(conn))
1297 @doc "Local Mastodon FE login init action"
1298 def login(conn, %{"code" => auth_token}) do
1299 with {:ok, app} <- get_or_make_app(),
1300 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1301 {:ok, token} <- Token.exchange_token(app, auth) do
1303 |> put_session(:oauth_token, token.token)
1304 |> redirect(to: local_mastodon_root_path(conn))
1308 @doc "Local Mastodon FE callback action"
1309 def login(conn, _) do
1310 with {:ok, app} <- get_or_make_app() do
1315 response_type: "code",
1316 client_id: app.client_id,
1318 scope: Enum.join(app.scopes, " ")
1321 redirect(conn, to: path)
1325 defp local_mastodon_root_path(conn) do
1326 case get_session(conn, :return_to) do
1328 mastodon_api_path(conn, :index, ["getting-started"])
1331 delete_session(conn, :return_to)
1336 defp get_or_make_app do
1337 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1338 scopes = ["read", "write", "follow", "push"]
1340 with %App{} = app <- Repo.get_by(App, find_attrs) do
1342 if app.scopes == scopes do
1346 |> Ecto.Changeset.change(%{scopes: scopes})
1354 App.register_changeset(
1356 Map.put(find_attrs, :scopes, scopes)
1363 def logout(conn, _) do
1366 |> redirect(to: "/")
1369 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1370 Logger.debug("Unimplemented, returning unmodified relationship")
1372 with %User{} = target <- User.get_by_id(id) do
1374 |> put_view(AccountView)
1375 |> render("relationship.json", %{user: user, target: target})
1379 def empty_array(conn, _) do
1380 Logger.debug("Unimplemented, returning an empty array")
1384 def empty_object(conn, _) do
1385 Logger.debug("Unimplemented, returning an empty object")
1389 def get_filters(%{assigns: %{user: user}} = conn, _) do
1390 filters = Filter.get_filters(user)
1391 res = FilterView.render("filters.json", filters: filters)
1396 %{assigns: %{user: user}} = conn,
1397 %{"phrase" => phrase, "context" => context} = params
1403 hide: Map.get(params, "irreversible", nil),
1404 whole_word: Map.get(params, "boolean", true)
1408 {:ok, response} = Filter.create(query)
1409 res = FilterView.render("filter.json", filter: response)
1413 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1414 filter = Filter.get(filter_id, user)
1415 res = FilterView.render("filter.json", filter: filter)
1420 %{assigns: %{user: user}} = conn,
1421 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1425 filter_id: filter_id,
1428 hide: Map.get(params, "irreversible", nil),
1429 whole_word: Map.get(params, "boolean", true)
1433 {:ok, response} = Filter.update(query)
1434 res = FilterView.render("filter.json", filter: response)
1438 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1441 filter_id: filter_id
1444 {:ok, _} = Filter.delete(query)
1450 def errors(conn, {:error, :not_found}) do
1453 |> json(%{error: "Record not found"})
1456 def errors(conn, _) do
1459 |> json("Something went wrong")
1462 def suggestions(%{assigns: %{user: user}} = conn, _) do
1463 suggestions = Config.get(:suggestions)
1465 if Keyword.get(suggestions, :enabled, false) do
1466 api = Keyword.get(suggestions, :third_party_engine, "")
1467 timeout = Keyword.get(suggestions, :timeout, 5000)
1468 limit = Keyword.get(suggestions, :limit, 23)
1470 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1472 user = user.nickname
1476 |> String.replace("{{host}}", host)
1477 |> String.replace("{{user}}", user)
1479 with {:ok, %{status: 200, body: body}} <-
1484 recv_timeout: timeout,
1488 {:ok, data} <- Jason.decode(body) do
1491 |> Enum.slice(0, limit)
1496 case User.get_or_fetch(x["acct"]) do
1503 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1506 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1512 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1519 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1520 with %Activity{} = activity <- Activity.get_by_id(status_id),
1521 true <- Visibility.visible_for_user?(activity, user) do
1525 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1535 def reports(%{assigns: %{user: user}} = conn, params) do
1536 case CommonAPI.report(user, params) do
1539 |> put_view(ReportView)
1540 |> try_render("report.json", %{activity: activity})
1544 |> put_status(:bad_request)
1545 |> json(%{error: err})
1549 def try_render(conn, target, params)
1550 when is_binary(target) do
1551 res = render(conn, target, params)
1556 |> json(%{error: "Can't display this activity"})
1562 def try_render(conn, _, _) do
1565 |> json(%{error: "Can't display this activity"})
1568 defp present?(nil), do: false
1569 defp present?(false), do: false
1570 defp present?(_), do: true