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
13 alias Pleroma.Object.Fetcher
14 alias Pleroma.Pagination
16 alias Pleroma.ScheduledActivity
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.AppView
25 alias Pleroma.Web.MastodonAPI.FilterView
26 alias Pleroma.Web.MastodonAPI.ListView
27 alias Pleroma.Web.MastodonAPI.MastodonAPI
28 alias Pleroma.Web.MastodonAPI.MastodonView
29 alias Pleroma.Web.MastodonAPI.NotificationView
30 alias Pleroma.Web.MastodonAPI.ReportView
31 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
32 alias Pleroma.Web.MastodonAPI.StatusView
33 alias Pleroma.Web.MediaProxy
34 alias Pleroma.Web.OAuth.App
35 alias Pleroma.Web.OAuth.Authorization
36 alias Pleroma.Web.OAuth.Token
38 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
43 @httpoison Application.get_env(:pleroma, :httpoison)
44 @local_mastodon_name "Mastodon-Local"
46 action_fallback(:errors)
48 def create_app(conn, params) do
49 scopes = oauth_scopes(params, ["read"])
53 |> Map.drop(["scope", "scopes"])
54 |> Map.put("scopes", scopes)
56 with cs <- App.register_changeset(%App{}, app_attrs),
57 false <- cs.changes[:client_name] == @local_mastodon_name,
58 {:ok, app} <- Repo.insert(cs) do
61 |> render("show.json", %{app: app})
70 value_function \\ fn x -> {:ok, x} end
72 if Map.has_key?(params, params_field) do
73 case value_function.(params[params_field]) do
74 {:ok, new_value} -> Map.put(map, map_field, new_value)
82 def update_credentials(%{assigns: %{user: user}} = conn, params) do
87 |> add_if_present(params, "display_name", :name)
88 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
89 |> add_if_present(params, "avatar", :avatar, fn value ->
90 with %Plug.Upload{} <- value,
91 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
100 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
101 |> add_if_present(params, "header", :banner, fn value ->
102 with %Plug.Upload{} <- value,
103 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
110 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
112 with changeset <- User.update_changeset(user, user_params),
113 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
114 {:ok, user} <- User.update_and_set_cache(changeset) do
115 if original_user != user do
116 CommonAPI.update(user)
119 json(conn, AccountView.render("account.json", %{user: user, for: user}))
124 |> json(%{error: "Invalid request"})
128 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
129 account = AccountView.render("account.json", %{user: user, for: user})
133 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
134 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
137 |> render("short.json", %{app: app})
141 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
142 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
143 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
144 account = AccountView.render("account.json", %{user: user, for: for_user})
150 |> json(%{error: "Can't find user"})
154 @mastodon_api_level "2.5.0"
156 def masto_instance(conn, _params) do
157 instance = Config.get(:instance)
161 title: Keyword.get(instance, :name),
162 description: Keyword.get(instance, :description),
163 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
164 email: Keyword.get(instance, :email),
166 streaming_api: Pleroma.Web.Endpoint.websocket_url()
168 stats: Stats.get_stats(),
169 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
171 registrations: Pleroma.Config.get([:instance, :registrations_open]),
172 # Extra (not present in Mastodon):
173 max_toot_chars: Keyword.get(instance, :limit)
179 def peers(conn, _params) do
180 json(conn, Stats.get_peers())
183 defp mastodonized_emoji do
184 Pleroma.Emoji.get_all()
185 |> Enum.map(fn {shortcode, relative_url, tags} ->
186 url = to_string(URI.merge(Web.base_url(), relative_url))
189 "shortcode" => shortcode,
191 "visible_in_picker" => true,
193 "tags" => String.split(tags, ",")
198 def custom_emojis(conn, _params) do
199 mastodon_emoji = mastodonized_emoji()
200 json(conn, mastodon_emoji)
203 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
206 |> Map.drop(["since_id", "max_id", "min_id"])
209 last = List.last(activities)
216 |> Map.get("limit", "20")
217 |> String.to_integer()
220 if length(activities) <= limit do
226 |> Enum.at(limit * -1)
230 {next_url, prev_url} =
234 Pleroma.Web.Endpoint,
237 Map.merge(params, %{max_id: max_id})
240 Pleroma.Web.Endpoint,
243 Map.merge(params, %{min_id: min_id})
249 Pleroma.Web.Endpoint,
251 Map.merge(params, %{max_id: max_id})
254 Pleroma.Web.Endpoint,
256 Map.merge(params, %{min_id: min_id})
262 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
268 def home_timeline(%{assigns: %{user: user}} = conn, params) do
271 |> Map.put("type", ["Create", "Announce"])
272 |> Map.put("blocking_user", user)
273 |> Map.put("muting_user", user)
274 |> Map.put("user", user)
277 [user.ap_id | user.following]
278 |> ActivityPub.fetch_activities(params)
279 |> ActivityPub.contain_timeline(user)
283 |> add_link_headers(:home_timeline, activities)
284 |> put_view(StatusView)
285 |> render("index.json", %{activities: activities, for: user, as: :activity})
288 def public_timeline(%{assigns: %{user: user}} = conn, params) do
289 local_only = params["local"] in [true, "True", "true", "1"]
293 |> Map.put("type", ["Create", "Announce"])
294 |> Map.put("local_only", local_only)
295 |> Map.put("blocking_user", user)
296 |> Map.put("muting_user", user)
297 |> ActivityPub.fetch_public_activities()
301 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
302 |> put_view(StatusView)
303 |> render("index.json", %{activities: activities, for: user, as: :activity})
306 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
307 with %User{} = user <- User.get_by_id(params["id"]) do
308 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
311 |> add_link_headers(:user_statuses, activities, params["id"])
312 |> put_view(StatusView)
313 |> render("index.json", %{
314 activities: activities,
321 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
324 |> Map.put("type", "Create")
325 |> Map.put("blocking_user", user)
326 |> Map.put("user", user)
327 |> Map.put(:visibility, "direct")
331 |> ActivityPub.fetch_activities_query(params)
332 |> Pagination.fetch_paginated(params)
335 |> add_link_headers(:dm_timeline, activities)
336 |> put_view(StatusView)
337 |> render("index.json", %{activities: activities, for: user, as: :activity})
340 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
341 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
342 true <- Visibility.visible_for_user?(activity, user) do
344 |> put_view(StatusView)
345 |> try_render("status.json", %{activity: activity, for: user})
349 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
350 with %Activity{} = activity <- Activity.get_by_id(id),
352 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
353 "blocking_user" => user,
357 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
359 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
360 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
366 activities: grouped_activities[true] || [],
370 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
375 activities: grouped_activities[false] || [],
379 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
386 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
387 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
389 |> add_link_headers(:scheduled_statuses, scheduled_activities)
390 |> put_view(ScheduledActivityView)
391 |> render("index.json", %{scheduled_activities: scheduled_activities})
395 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
396 with %ScheduledActivity{} = scheduled_activity <-
397 ScheduledActivity.get(user, scheduled_activity_id) do
399 |> put_view(ScheduledActivityView)
400 |> render("show.json", %{scheduled_activity: scheduled_activity})
402 _ -> {:error, :not_found}
406 def update_scheduled_status(
407 %{assigns: %{user: user}} = conn,
408 %{"id" => scheduled_activity_id} = params
410 with %ScheduledActivity{} = scheduled_activity <-
411 ScheduledActivity.get(user, scheduled_activity_id),
412 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
414 |> put_view(ScheduledActivityView)
415 |> render("show.json", %{scheduled_activity: scheduled_activity})
417 nil -> {:error, :not_found}
422 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id),
425 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
427 |> put_view(ScheduledActivityView)
428 |> render("show.json", %{scheduled_activity: scheduled_activity})
430 nil -> {:error, :not_found}
435 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
436 when length(media_ids) > 0 do
439 |> Map.put("status", ".")
441 post_status(conn, params)
444 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
447 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
450 case get_req_header(conn, "idempotency-key") do
452 _ -> Ecto.UUID.generate()
455 scheduled_at = params["scheduled_at"]
457 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
458 with {:ok, scheduled_activity} <-
459 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
461 |> put_view(ScheduledActivityView)
462 |> render("show.json", %{scheduled_activity: scheduled_activity})
465 params = Map.drop(params, ["scheduled_at"])
468 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
469 CommonAPI.post(user, params)
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
479 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
485 |> json(%{error: "Can't delete this post"})
489 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
490 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
491 %Activity{} = announce <- Activity.normalize(announce.data) do
493 |> put_view(StatusView)
494 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
498 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
499 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
500 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
502 |> put_view(StatusView)
503 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
507 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
508 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
509 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
511 |> put_view(StatusView)
512 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
516 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
517 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
518 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
525 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
526 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
528 |> put_view(StatusView)
529 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
533 |> put_resp_content_type("application/json")
534 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
538 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
539 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
541 |> put_view(StatusView)
542 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
546 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
547 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
548 %Object{} = object <- Object.normalize(activity),
549 %User{} = user <- User.get_by_nickname(user.nickname),
550 true <- Visibility.visible_for_user?(activity, user),
551 {:ok, user} <- User.bookmark(user, object.data["id"]) do
553 |> put_view(StatusView)
554 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
558 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
559 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
560 %Object{} = object <- Object.normalize(activity),
561 %User{} = user <- User.get_by_nickname(user.nickname),
562 true <- Visibility.visible_for_user?(activity, user),
563 {:ok, user} <- User.unbookmark(user, object.data["id"]) do
565 |> put_view(StatusView)
566 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
570 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
571 activity = Activity.get_by_id(id)
573 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
575 |> put_view(StatusView)
576 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
580 |> put_resp_content_type("application/json")
581 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
585 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
586 activity = Activity.get_by_id(id)
588 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
590 |> put_view(StatusView)
591 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
595 def notifications(%{assigns: %{user: user}} = conn, params) do
596 notifications = MastodonAPI.get_notifications(user, params)
599 |> add_link_headers(:notifications, notifications)
600 |> put_view(NotificationView)
601 |> render("index.json", %{notifications: notifications, for: user})
604 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
605 with {:ok, notification} <- Notification.get(user, id) do
607 |> put_view(NotificationView)
608 |> render("show.json", %{notification: notification, for: user})
612 |> put_resp_content_type("application/json")
613 |> send_resp(403, Jason.encode!(%{"error" => reason}))
617 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
618 Notification.clear(user)
622 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
623 with {:ok, _notif} <- Notification.dismiss(user, id) do
628 |> put_resp_content_type("application/json")
629 |> send_resp(403, Jason.encode!(%{"error" => reason}))
633 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
634 Notification.destroy_multiple(user, ids)
638 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
640 q = from(u in User, where: u.id in ^id)
641 targets = Repo.all(q)
644 |> put_view(AccountView)
645 |> render("relationships.json", %{user: user, targets: targets})
648 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
649 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
651 def update_media(%{assigns: %{user: user}} = conn, data) do
652 with %Object{} = object <- Repo.get(Object, data["id"]),
653 true <- Object.authorize_mutation(object, user),
654 true <- is_binary(data["description"]),
655 description <- data["description"] do
656 new_data = %{object.data | "name" => description}
660 |> Object.change(%{data: new_data})
663 attachment_data = Map.put(new_data, "id", object.id)
666 |> put_view(StatusView)
667 |> render("attachment.json", %{attachment: attachment_data})
671 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
672 with {:ok, object} <-
675 actor: User.ap_id(user),
676 description: Map.get(data, "description")
678 attachment_data = Map.put(object.data, "id", object.id)
681 |> put_view(StatusView)
682 |> render("attachment.json", %{attachment: attachment_data})
686 def favourited_by(conn, %{"id" => id}) do
687 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
688 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
689 q = from(u in User, where: u.ap_id in ^likes)
693 |> put_view(AccountView)
694 |> render(AccountView, "accounts.json", %{users: users, as: :user})
700 def reblogged_by(conn, %{"id" => id}) do
701 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
702 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
703 q = from(u in User, where: u.ap_id in ^announces)
707 |> put_view(AccountView)
708 |> render("accounts.json", %{users: users, as: :user})
714 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
715 local_only = params["local"] in [true, "True", "true", "1"]
718 [params["tag"], params["any"]]
722 |> Enum.map(&String.downcase(&1))
727 |> Enum.map(&String.downcase(&1))
732 |> Enum.map(&String.downcase(&1))
736 |> Map.put("type", "Create")
737 |> Map.put("local_only", local_only)
738 |> Map.put("blocking_user", user)
739 |> Map.put("muting_user", user)
740 |> Map.put("tag", tags)
741 |> Map.put("tag_all", tag_all)
742 |> Map.put("tag_reject", tag_reject)
743 |> ActivityPub.fetch_public_activities()
747 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
748 |> put_view(StatusView)
749 |> render("index.json", %{activities: activities, for: user, as: :activity})
752 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
753 with %User{} = user <- User.get_by_id(id),
754 followers <- MastodonAPI.get_followers(user, params) do
757 for_user && user.id == for_user.id -> followers
758 user.info.hide_followers -> []
763 |> add_link_headers(:followers, followers, user)
764 |> put_view(AccountView)
765 |> render("accounts.json", %{users: followers, as: :user})
769 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
770 with %User{} = user <- User.get_by_id(id),
771 followers <- MastodonAPI.get_friends(user, params) do
774 for_user && user.id == for_user.id -> followers
775 user.info.hide_follows -> []
780 |> add_link_headers(:following, followers, user)
781 |> put_view(AccountView)
782 |> render("accounts.json", %{users: followers, as: :user})
786 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
787 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
789 |> put_view(AccountView)
790 |> render("accounts.json", %{users: follow_requests, as: :user})
794 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
795 with %User{} = follower <- User.get_by_id(id),
796 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
798 |> put_view(AccountView)
799 |> render("relationship.json", %{user: followed, target: follower})
803 |> put_resp_content_type("application/json")
804 |> send_resp(403, Jason.encode!(%{"error" => message}))
808 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
809 with %User{} = follower <- User.get_by_id(id),
810 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
812 |> put_view(AccountView)
813 |> render("relationship.json", %{user: followed, target: follower})
817 |> put_resp_content_type("application/json")
818 |> send_resp(403, Jason.encode!(%{"error" => message}))
822 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
823 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
824 {_, true} <- {:followed, follower.id != followed.id},
825 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
827 |> put_view(AccountView)
828 |> render("relationship.json", %{user: follower, target: followed})
835 |> put_resp_content_type("application/json")
836 |> send_resp(403, Jason.encode!(%{"error" => message}))
840 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
841 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
842 {_, true} <- {:followed, follower.id != followed.id},
843 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
845 |> put_view(AccountView)
846 |> render("account.json", %{user: followed, for: follower})
853 |> put_resp_content_type("application/json")
854 |> send_resp(403, Jason.encode!(%{"error" => message}))
858 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
859 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
860 {_, true} <- {:followed, follower.id != followed.id},
861 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
863 |> put_view(AccountView)
864 |> render("relationship.json", %{user: follower, target: followed})
874 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
875 with %User{} = muted <- User.get_by_id(id),
876 {:ok, muter} <- User.mute(muter, muted) do
878 |> put_view(AccountView)
879 |> render("relationship.json", %{user: muter, target: muted})
883 |> put_resp_content_type("application/json")
884 |> send_resp(403, Jason.encode!(%{"error" => message}))
888 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
889 with %User{} = muted <- User.get_by_id(id),
890 {:ok, muter} <- User.unmute(muter, muted) do
892 |> put_view(AccountView)
893 |> render("relationship.json", %{user: muter, target: muted})
897 |> put_resp_content_type("application/json")
898 |> send_resp(403, Jason.encode!(%{"error" => message}))
902 def mutes(%{assigns: %{user: user}} = conn, _) do
903 with muted_accounts <- User.muted_users(user) do
904 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
909 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
910 with %User{} = blocked <- User.get_by_id(id),
911 {:ok, blocker} <- User.block(blocker, blocked),
912 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
914 |> put_view(AccountView)
915 |> render("relationship.json", %{user: blocker, target: blocked})
919 |> put_resp_content_type("application/json")
920 |> send_resp(403, Jason.encode!(%{"error" => message}))
924 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
925 with %User{} = blocked <- User.get_by_id(id),
926 {:ok, blocker} <- User.unblock(blocker, blocked),
927 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
929 |> put_view(AccountView)
930 |> render("relationship.json", %{user: blocker, target: blocked})
934 |> put_resp_content_type("application/json")
935 |> send_resp(403, Jason.encode!(%{"error" => message}))
939 def blocks(%{assigns: %{user: user}} = conn, _) do
940 with blocked_accounts <- User.blocked_users(user) do
941 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
946 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
947 json(conn, info.domain_blocks || [])
950 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
951 User.block_domain(blocker, domain)
955 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
956 User.unblock_domain(blocker, domain)
960 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
961 with %User{} = subscription_target <- User.get_cached_by_id(id),
962 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
964 |> put_view(AccountView)
965 |> render("relationship.json", %{user: user, target: subscription_target})
969 |> put_resp_content_type("application/json")
970 |> send_resp(403, Jason.encode!(%{"error" => message}))
974 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
975 with %User{} = subscription_target <- User.get_cached_by_id(id),
976 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
978 |> put_view(AccountView)
979 |> render("relationship.json", %{user: user, target: subscription_target})
983 |> put_resp_content_type("application/json")
984 |> send_resp(403, Jason.encode!(%{"error" => message}))
988 def status_search(user, query) do
990 if Regex.match?(~r/https?:/, query) do
991 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
992 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
993 true <- Visibility.visible_for_user?(activity, user) do
1002 [a, o] in Activity.with_preloaded_object(Activity),
1003 where: fragment("?->>'type' = 'Create'", a.data),
1004 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1007 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1012 order_by: [desc: :id]
1015 Repo.all(q) ++ fetched
1018 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1019 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1021 statuses = status_search(user, query)
1023 tags_path = Web.base_url() <> "/tag/"
1029 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1030 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1031 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1034 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1036 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1043 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1044 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1046 statuses = status_search(user, query)
1052 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1053 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1056 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1058 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1065 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1066 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1068 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1073 def favourites(%{assigns: %{user: user}} = conn, params) do
1076 |> Map.put("type", "Create")
1077 |> Map.put("favorited_by", user.ap_id)
1078 |> Map.put("blocking_user", user)
1081 ActivityPub.fetch_activities([], params)
1085 |> add_link_headers(:favourites, activities)
1086 |> put_view(StatusView)
1087 |> render("index.json", %{activities: activities, for: user, as: :activity})
1090 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1091 user = User.get_by_id(user.id)
1095 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1099 |> put_view(StatusView)
1100 |> render("index.json", %{activities: activities, for: user, as: :activity})
1103 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1104 lists = Pleroma.List.for_user(user, opts)
1105 res = ListView.render("lists.json", lists: lists)
1109 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1110 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1111 res = ListView.render("list.json", list: list)
1117 |> json(%{error: "Record not found"})
1121 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1122 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1123 res = ListView.render("lists.json", lists: lists)
1127 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1128 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1129 {:ok, _list} <- Pleroma.List.delete(list) do
1137 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1138 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1139 res = ListView.render("list.json", list: list)
1144 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1146 |> Enum.each(fn account_id ->
1147 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1148 %User{} = followed <- User.get_by_id(account_id) do
1149 Pleroma.List.follow(list, followed)
1156 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1158 |> Enum.each(fn account_id ->
1159 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1160 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1161 Pleroma.List.unfollow(list, followed)
1168 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1169 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1170 {:ok, users} = Pleroma.List.get_following(list) do
1172 |> put_view(AccountView)
1173 |> render("accounts.json", %{users: users, as: :user})
1177 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1178 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1179 {:ok, list} <- Pleroma.List.rename(list, title) do
1180 res = ListView.render("list.json", list: list)
1188 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1189 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1192 |> Map.put("type", "Create")
1193 |> Map.put("blocking_user", user)
1194 |> Map.put("muting_user", user)
1196 # we must filter the following list for the user to avoid leaking statuses the user
1197 # does not actually have permission to see (for more info, peruse security issue #270).
1200 |> Enum.filter(fn x -> x in user.following end)
1201 |> ActivityPub.fetch_activities_bounded(following, params)
1205 |> put_view(StatusView)
1206 |> render("index.json", %{activities: activities, for: user, as: :activity})
1211 |> json(%{error: "Error."})
1215 def index(%{assigns: %{user: user}} = conn, _params) do
1216 token = get_session(conn, :oauth_token)
1219 mastodon_emoji = mastodonized_emoji()
1221 limit = Config.get([:instance, :limit])
1224 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1226 flavour = get_user_flavour(user)
1231 streaming_api_base_url:
1232 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1233 access_token: token,
1235 domain: Pleroma.Web.Endpoint.host(),
1238 unfollow_modal: false,
1241 auto_play_gif: false,
1242 display_sensitive_media: false,
1243 reduce_motion: false,
1244 max_toot_chars: limit,
1245 mascot: "/images/pleroma-fox-tan-smol.png"
1248 delete_others_notice: present?(user.info.is_moderator),
1249 admin: present?(user.info.is_admin)
1253 default_privacy: user.info.default_scope,
1254 default_sensitive: false,
1255 allow_content_types: Config.get([:instance, :allowed_post_formats])
1257 media_attachments: %{
1258 accept_content_types: [
1274 user.info.settings ||
1304 push_subscription: nil,
1306 custom_emojis: mastodon_emoji,
1312 |> put_layout(false)
1313 |> put_view(MastodonView)
1314 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1317 |> put_session(:return_to, conn.request_path)
1318 |> redirect(to: "/web/login")
1322 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1323 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1325 with changeset <- Ecto.Changeset.change(user),
1326 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1327 {:ok, _user} <- User.update_and_set_cache(changeset) do
1332 |> put_resp_content_type("application/json")
1333 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1337 @supported_flavours ["glitch", "vanilla"]
1339 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1340 when flavour in @supported_flavours do
1341 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1343 with changeset <- Ecto.Changeset.change(user),
1344 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1345 {:ok, user} <- User.update_and_set_cache(changeset),
1346 flavour <- user.info.flavour do
1351 |> put_resp_content_type("application/json")
1352 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1356 def set_flavour(conn, _params) do
1359 |> json(%{error: "Unsupported flavour"})
1362 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1363 json(conn, get_user_flavour(user))
1366 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1370 defp get_user_flavour(_) do
1374 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1375 redirect(conn, to: local_mastodon_root_path(conn))
1378 @doc "Local Mastodon FE login init action"
1379 def login(conn, %{"code" => auth_token}) do
1380 with {:ok, app} <- get_or_make_app(),
1381 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1382 {:ok, token} <- Token.exchange_token(app, auth) do
1384 |> put_session(:oauth_token, token.token)
1385 |> redirect(to: local_mastodon_root_path(conn))
1389 @doc "Local Mastodon FE callback action"
1390 def login(conn, _) do
1391 with {:ok, app} <- get_or_make_app() do
1396 response_type: "code",
1397 client_id: app.client_id,
1399 scope: Enum.join(app.scopes, " ")
1402 redirect(conn, to: path)
1406 defp local_mastodon_root_path(conn) do
1407 case get_session(conn, :return_to) do
1409 mastodon_api_path(conn, :index, ["getting-started"])
1412 delete_session(conn, :return_to)
1417 defp get_or_make_app do
1418 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1419 scopes = ["read", "write", "follow", "push"]
1421 with %App{} = app <- Repo.get_by(App, find_attrs) do
1423 if app.scopes == scopes do
1427 |> Ecto.Changeset.change(%{scopes: scopes})
1435 App.register_changeset(
1437 Map.put(find_attrs, :scopes, scopes)
1444 def logout(conn, _) do
1447 |> redirect(to: "/")
1450 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1451 Logger.debug("Unimplemented, returning unmodified relationship")
1453 with %User{} = target <- User.get_by_id(id) do
1455 |> put_view(AccountView)
1456 |> render("relationship.json", %{user: user, target: target})
1460 def empty_array(conn, _) do
1461 Logger.debug("Unimplemented, returning an empty array")
1465 def empty_object(conn, _) do
1466 Logger.debug("Unimplemented, returning an empty object")
1470 def get_filters(%{assigns: %{user: user}} = conn, _) do
1471 filters = Filter.get_filters(user)
1472 res = FilterView.render("filters.json", filters: filters)
1477 %{assigns: %{user: user}} = conn,
1478 %{"phrase" => phrase, "context" => context} = params
1484 hide: Map.get(params, "irreversible", nil),
1485 whole_word: Map.get(params, "boolean", true)
1489 {:ok, response} = Filter.create(query)
1490 res = FilterView.render("filter.json", filter: response)
1494 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1495 filter = Filter.get(filter_id, user)
1496 res = FilterView.render("filter.json", filter: filter)
1501 %{assigns: %{user: user}} = conn,
1502 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1506 filter_id: filter_id,
1509 hide: Map.get(params, "irreversible", nil),
1510 whole_word: Map.get(params, "boolean", true)
1514 {:ok, response} = Filter.update(query)
1515 res = FilterView.render("filter.json", filter: response)
1519 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1522 filter_id: filter_id
1525 {:ok, _} = Filter.delete(query)
1531 def errors(conn, {:error, %Changeset{} = changeset}) do
1534 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1535 |> Enum.map_join(", ", fn {_k, v} -> v end)
1539 |> json(%{error: error_message})
1542 def errors(conn, {:error, :not_found}) do
1545 |> json(%{error: "Record not found"})
1548 def errors(conn, _) do
1551 |> json("Something went wrong")
1554 def suggestions(%{assigns: %{user: user}} = conn, _) do
1555 suggestions = Config.get(:suggestions)
1557 if Keyword.get(suggestions, :enabled, false) do
1558 api = Keyword.get(suggestions, :third_party_engine, "")
1559 timeout = Keyword.get(suggestions, :timeout, 5000)
1560 limit = Keyword.get(suggestions, :limit, 23)
1562 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1564 user = user.nickname
1568 |> String.replace("{{host}}", host)
1569 |> String.replace("{{user}}", user)
1571 with {:ok, %{status: 200, body: body}} <-
1576 recv_timeout: timeout,
1580 {:ok, data} <- Jason.decode(body) do
1583 |> Enum.slice(0, limit)
1588 case User.get_or_fetch(x["acct"]) do
1595 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1598 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1604 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1611 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1612 with %Activity{} = activity <- Activity.get_by_id(status_id),
1613 true <- Visibility.visible_for_user?(activity, user) do
1617 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1627 def reports(%{assigns: %{user: user}} = conn, params) do
1628 case CommonAPI.report(user, params) do
1631 |> put_view(ReportView)
1632 |> try_render("report.json", %{activity: activity})
1636 |> put_status(:bad_request)
1637 |> json(%{error: err})
1641 def try_render(conn, target, params)
1642 when is_binary(target) do
1643 res = render(conn, target, params)
1648 |> json(%{error: "Can't display this activity"})
1654 def try_render(conn, _, _) do
1657 |> json(%{error: "Can't display this activity"})
1660 defp present?(nil), do: false
1661 defp present?(false), do: false
1662 defp present?(_), do: true