1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
12 alias Pleroma.Notification
14 alias Pleroma.Object.Fetcher
15 alias Pleroma.Pagination
17 alias Pleroma.ScheduledActivity
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.ActivityPub.Visibility
23 alias Pleroma.Web.CommonAPI
24 alias Pleroma.Web.MastodonAPI.AccountView
25 alias Pleroma.Web.MastodonAPI.AppView
26 alias Pleroma.Web.MastodonAPI.FilterView
27 alias Pleroma.Web.MastodonAPI.ListView
28 alias Pleroma.Web.MastodonAPI.MastodonAPI
29 alias Pleroma.Web.MastodonAPI.MastodonView
30 alias Pleroma.Web.MastodonAPI.NotificationView
31 alias Pleroma.Web.MastodonAPI.ReportView
32 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
33 alias Pleroma.Web.MastodonAPI.StatusView
34 alias Pleroma.Web.MediaProxy
35 alias Pleroma.Web.OAuth.App
36 alias Pleroma.Web.OAuth.Authorization
37 alias Pleroma.Web.OAuth.Token
39 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
44 @httpoison Application.get_env(:pleroma, :httpoison)
45 @local_mastodon_name "Mastodon-Local"
47 action_fallback(:errors)
49 def create_app(conn, params) do
50 scopes = oauth_scopes(params, ["read"])
54 |> Map.drop(["scope", "scopes"])
55 |> Map.put("scopes", scopes)
57 with cs <- App.register_changeset(%App{}, app_attrs),
58 false <- cs.changes[:client_name] == @local_mastodon_name,
59 {:ok, app} <- Repo.insert(cs) do
62 |> render("show.json", %{app: app})
71 value_function \\ fn x -> {:ok, x} end
73 if Map.has_key?(params, params_field) do
74 case value_function.(params[params_field]) do
75 {:ok, new_value} -> Map.put(map, map_field, new_value)
83 def update_credentials(%{assigns: %{user: user}} = conn, params) do
88 |> add_if_present(params, "display_name", :name)
89 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
90 |> add_if_present(params, "avatar", :avatar, fn value ->
91 with %Plug.Upload{} <- value,
92 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
101 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
102 |> add_if_present(params, "header", :banner, fn value ->
103 with %Plug.Upload{} <- value,
104 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
111 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
113 with changeset <- User.update_changeset(user, user_params),
114 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
115 {:ok, user} <- User.update_and_set_cache(changeset) do
116 if original_user != user do
117 CommonAPI.update(user)
120 json(conn, AccountView.render("account.json", %{user: user, for: user}))
125 |> json(%{error: "Invalid request"})
129 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
130 account = AccountView.render("account.json", %{user: user, for: user})
134 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
135 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
138 |> render("short.json", %{app: app})
142 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
143 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
144 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
145 account = AccountView.render("account.json", %{user: user, for: for_user})
151 |> json(%{error: "Can't find user"})
155 @mastodon_api_level "2.5.0"
157 def masto_instance(conn, _params) do
158 instance = Config.get(:instance)
162 title: Keyword.get(instance, :name),
163 description: Keyword.get(instance, :description),
164 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
165 email: Keyword.get(instance, :email),
167 streaming_api: Pleroma.Web.Endpoint.websocket_url()
169 stats: Stats.get_stats(),
170 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
172 registrations: Pleroma.Config.get([:instance, :registrations_open]),
173 # Extra (not present in Mastodon):
174 max_toot_chars: Keyword.get(instance, :limit)
180 def peers(conn, _params) do
181 json(conn, Stats.get_peers())
184 defp mastodonized_emoji do
185 Pleroma.Emoji.get_all()
186 |> Enum.map(fn {shortcode, relative_url, tags} ->
187 url = to_string(URI.merge(Web.base_url(), relative_url))
190 "shortcode" => shortcode,
192 "visible_in_picker" => true,
199 def custom_emojis(conn, _params) do
200 mastodon_emoji = mastodonized_emoji()
201 json(conn, mastodon_emoji)
204 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
207 |> Map.drop(["since_id", "max_id", "min_id"])
210 last = List.last(activities)
217 |> Map.get("limit", "20")
218 |> String.to_integer()
221 if length(activities) <= limit do
227 |> Enum.at(limit * -1)
231 {next_url, prev_url} =
235 Pleroma.Web.Endpoint,
238 Map.merge(params, %{max_id: max_id})
241 Pleroma.Web.Endpoint,
244 Map.merge(params, %{min_id: min_id})
250 Pleroma.Web.Endpoint,
252 Map.merge(params, %{max_id: max_id})
255 Pleroma.Web.Endpoint,
257 Map.merge(params, %{min_id: min_id})
263 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
269 def home_timeline(%{assigns: %{user: user}} = conn, params) do
272 |> Map.put("type", ["Create", "Announce"])
273 |> Map.put("blocking_user", user)
274 |> Map.put("muting_user", user)
275 |> Map.put("user", user)
278 [user.ap_id | user.following]
279 |> ActivityPub.fetch_activities(params)
280 |> ActivityPub.contain_timeline(user)
283 user = Repo.preload(user, :bookmarks)
286 |> add_link_headers(:home_timeline, activities)
287 |> put_view(StatusView)
288 |> render("index.json", %{activities: activities, for: user, as: :activity})
291 def public_timeline(%{assigns: %{user: user}} = conn, params) do
292 local_only = params["local"] in [true, "True", "true", "1"]
296 |> Map.put("type", ["Create", "Announce"])
297 |> Map.put("local_only", local_only)
298 |> Map.put("blocking_user", user)
299 |> Map.put("muting_user", user)
300 |> ActivityPub.fetch_public_activities()
303 user = Repo.preload(user, :bookmarks)
306 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
307 |> put_view(StatusView)
308 |> render("index.json", %{activities: activities, for: user, as: :activity})
311 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
312 with %User{} = user <- User.get_cached_by_id(params["id"]),
313 reading_user <- Repo.preload(reading_user, :bookmarks) do
314 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
317 |> add_link_headers(:user_statuses, activities, params["id"])
318 |> put_view(StatusView)
319 |> render("index.json", %{
320 activities: activities,
327 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
330 |> Map.put("type", "Create")
331 |> Map.put("blocking_user", user)
332 |> Map.put("user", user)
333 |> Map.put(:visibility, "direct")
337 |> ActivityPub.fetch_activities_query(params)
338 |> Pagination.fetch_paginated(params)
340 user = Repo.preload(user, :bookmarks)
343 |> add_link_headers(:dm_timeline, activities)
344 |> put_view(StatusView)
345 |> render("index.json", %{activities: activities, for: user, as: :activity})
348 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
349 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
350 true <- Visibility.visible_for_user?(activity, user) do
352 |> put_view(StatusView)
353 |> try_render("status.json", %{activity: activity, for: user})
357 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
358 with %Activity{} = activity <- Activity.get_by_id(id),
360 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
361 "blocking_user" => user,
365 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
367 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
368 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
374 activities: grouped_activities[true] || [],
378 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
383 activities: grouped_activities[false] || [],
387 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
394 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
395 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
397 |> add_link_headers(:scheduled_statuses, scheduled_activities)
398 |> put_view(ScheduledActivityView)
399 |> render("index.json", %{scheduled_activities: scheduled_activities})
403 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
404 with %ScheduledActivity{} = scheduled_activity <-
405 ScheduledActivity.get(user, scheduled_activity_id) do
407 |> put_view(ScheduledActivityView)
408 |> render("show.json", %{scheduled_activity: scheduled_activity})
410 _ -> {:error, :not_found}
414 def update_scheduled_status(
415 %{assigns: %{user: user}} = conn,
416 %{"id" => scheduled_activity_id} = params
418 with %ScheduledActivity{} = scheduled_activity <-
419 ScheduledActivity.get(user, scheduled_activity_id),
420 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
422 |> put_view(ScheduledActivityView)
423 |> render("show.json", %{scheduled_activity: scheduled_activity})
425 nil -> {:error, :not_found}
430 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
431 with %ScheduledActivity{} = scheduled_activity <-
432 ScheduledActivity.get(user, scheduled_activity_id),
433 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
435 |> put_view(ScheduledActivityView)
436 |> render("show.json", %{scheduled_activity: scheduled_activity})
438 nil -> {:error, :not_found}
443 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
444 when length(media_ids) > 0 do
447 |> Map.put("status", ".")
449 post_status(conn, params)
452 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
455 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
458 case get_req_header(conn, "idempotency-key") do
460 _ -> Ecto.UUID.generate()
463 scheduled_at = params["scheduled_at"]
465 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
466 with {:ok, scheduled_activity} <-
467 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
469 |> put_view(ScheduledActivityView)
470 |> render("show.json", %{scheduled_activity: scheduled_activity})
473 params = Map.drop(params, ["scheduled_at"])
476 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
477 CommonAPI.post(user, params)
481 |> put_view(StatusView)
482 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
486 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
487 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
493 |> json(%{error: "Can't delete this post"})
497 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
498 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
499 %Activity{} = announce <- Activity.normalize(announce.data) do
501 |> put_view(StatusView)
502 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
506 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
507 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
508 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
510 |> put_view(StatusView)
511 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
515 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
524 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
526 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
528 |> put_view(StatusView)
529 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
533 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
534 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
536 |> put_view(StatusView)
537 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
541 |> put_resp_content_type("application/json")
542 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
546 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
547 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
549 |> put_view(StatusView)
550 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
554 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
555 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
556 %Object{} = object <- Object.normalize(activity),
557 %User{} = user <- User.get_cached_by_nickname(user.nickname),
558 true <- Visibility.visible_for_user?(activity, user),
559 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
560 user = Repo.preload(user, :bookmarks)
563 |> put_view(StatusView)
564 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
568 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
569 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
570 %Object{} = object <- Object.normalize(activity),
571 %User{} = user <- User.get_cached_by_nickname(user.nickname),
572 true <- Visibility.visible_for_user?(activity, user),
573 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
574 user = Repo.preload(user, :bookmarks)
577 |> put_view(StatusView)
578 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
582 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
583 activity = Activity.get_by_id(id)
585 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
587 |> put_view(StatusView)
588 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
592 |> put_resp_content_type("application/json")
593 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
597 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
598 activity = Activity.get_by_id(id)
600 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
602 |> put_view(StatusView)
603 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
607 def notifications(%{assigns: %{user: user}} = conn, params) do
608 notifications = MastodonAPI.get_notifications(user, params)
611 |> add_link_headers(:notifications, notifications)
612 |> put_view(NotificationView)
613 |> render("index.json", %{notifications: notifications, for: user})
616 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
617 with {:ok, notification} <- Notification.get(user, id) do
619 |> put_view(NotificationView)
620 |> render("show.json", %{notification: notification, for: user})
624 |> put_resp_content_type("application/json")
625 |> send_resp(403, Jason.encode!(%{"error" => reason}))
629 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
630 Notification.clear(user)
634 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
635 with {:ok, _notif} <- Notification.dismiss(user, id) do
640 |> put_resp_content_type("application/json")
641 |> send_resp(403, Jason.encode!(%{"error" => reason}))
645 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
646 Notification.destroy_multiple(user, ids)
650 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
652 q = from(u in User, where: u.id in ^id)
653 targets = Repo.all(q)
656 |> put_view(AccountView)
657 |> render("relationships.json", %{user: user, targets: targets})
660 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
661 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
663 def update_media(%{assigns: %{user: user}} = conn, data) do
664 with %Object{} = object <- Repo.get(Object, data["id"]),
665 true <- Object.authorize_mutation(object, user),
666 true <- is_binary(data["description"]),
667 description <- data["description"] do
668 new_data = %{object.data | "name" => description}
672 |> Object.change(%{data: new_data})
675 attachment_data = Map.put(new_data, "id", object.id)
678 |> put_view(StatusView)
679 |> render("attachment.json", %{attachment: attachment_data})
683 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
684 with {:ok, object} <-
687 actor: User.ap_id(user),
688 description: Map.get(data, "description")
690 attachment_data = Map.put(object.data, "id", object.id)
693 |> put_view(StatusView)
694 |> render("attachment.json", %{attachment: attachment_data})
698 def favourited_by(conn, %{"id" => id}) do
699 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
700 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
701 q = from(u in User, where: u.ap_id in ^likes)
705 |> put_view(AccountView)
706 |> render(AccountView, "accounts.json", %{users: users, as: :user})
712 def reblogged_by(conn, %{"id" => id}) do
713 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
714 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
715 q = from(u in User, where: u.ap_id in ^announces)
719 |> put_view(AccountView)
720 |> render("accounts.json", %{users: users, as: :user})
726 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
727 local_only = params["local"] in [true, "True", "true", "1"]
730 [params["tag"], params["any"]]
734 |> Enum.map(&String.downcase(&1))
739 |> Enum.map(&String.downcase(&1))
744 |> Enum.map(&String.downcase(&1))
748 |> Map.put("type", "Create")
749 |> Map.put("local_only", local_only)
750 |> Map.put("blocking_user", user)
751 |> Map.put("muting_user", user)
752 |> Map.put("tag", tags)
753 |> Map.put("tag_all", tag_all)
754 |> Map.put("tag_reject", tag_reject)
755 |> ActivityPub.fetch_public_activities()
759 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
760 |> put_view(StatusView)
761 |> render("index.json", %{activities: activities, for: user, as: :activity})
764 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
765 with %User{} = user <- User.get_cached_by_id(id),
766 followers <- MastodonAPI.get_followers(user, params) do
769 for_user && user.id == for_user.id -> followers
770 user.info.hide_followers -> []
775 |> add_link_headers(:followers, followers, user)
776 |> put_view(AccountView)
777 |> render("accounts.json", %{users: followers, as: :user})
781 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
782 with %User{} = user <- User.get_cached_by_id(id),
783 followers <- MastodonAPI.get_friends(user, params) do
786 for_user && user.id == for_user.id -> followers
787 user.info.hide_follows -> []
792 |> add_link_headers(:following, followers, user)
793 |> put_view(AccountView)
794 |> render("accounts.json", %{users: followers, as: :user})
798 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
799 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
801 |> put_view(AccountView)
802 |> render("accounts.json", %{users: follow_requests, as: :user})
806 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
807 with %User{} = follower <- User.get_cached_by_id(id),
808 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
810 |> put_view(AccountView)
811 |> render("relationship.json", %{user: followed, target: follower})
815 |> put_resp_content_type("application/json")
816 |> send_resp(403, Jason.encode!(%{"error" => message}))
820 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
821 with %User{} = follower <- User.get_cached_by_id(id),
822 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
824 |> put_view(AccountView)
825 |> render("relationship.json", %{user: followed, target: follower})
829 |> put_resp_content_type("application/json")
830 |> send_resp(403, Jason.encode!(%{"error" => message}))
834 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
835 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
836 {_, true} <- {:followed, follower.id != followed.id},
837 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
839 |> put_view(AccountView)
840 |> render("relationship.json", %{user: follower, target: followed})
847 |> put_resp_content_type("application/json")
848 |> send_resp(403, Jason.encode!(%{"error" => message}))
852 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
853 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
854 {_, true} <- {:followed, follower.id != followed.id},
855 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
857 |> put_view(AccountView)
858 |> render("account.json", %{user: followed, for: follower})
865 |> put_resp_content_type("application/json")
866 |> send_resp(403, Jason.encode!(%{"error" => message}))
870 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
871 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
872 {_, true} <- {:followed, follower.id != followed.id},
873 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
875 |> put_view(AccountView)
876 |> render("relationship.json", %{user: follower, target: followed})
886 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
887 with %User{} = muted <- User.get_cached_by_id(id),
888 {:ok, muter} <- User.mute(muter, muted) do
890 |> put_view(AccountView)
891 |> render("relationship.json", %{user: muter, target: muted})
895 |> put_resp_content_type("application/json")
896 |> send_resp(403, Jason.encode!(%{"error" => message}))
900 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
901 with %User{} = muted <- User.get_cached_by_id(id),
902 {:ok, muter} <- User.unmute(muter, muted) do
904 |> put_view(AccountView)
905 |> render("relationship.json", %{user: muter, target: muted})
909 |> put_resp_content_type("application/json")
910 |> send_resp(403, Jason.encode!(%{"error" => message}))
914 def mutes(%{assigns: %{user: user}} = conn, _) do
915 with muted_accounts <- User.muted_users(user) do
916 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
921 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
922 with %User{} = blocked <- User.get_cached_by_id(id),
923 {:ok, blocker} <- User.block(blocker, blocked),
924 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
926 |> put_view(AccountView)
927 |> render("relationship.json", %{user: blocker, target: blocked})
931 |> put_resp_content_type("application/json")
932 |> send_resp(403, Jason.encode!(%{"error" => message}))
936 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
937 with %User{} = blocked <- User.get_cached_by_id(id),
938 {:ok, blocker} <- User.unblock(blocker, blocked),
939 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
941 |> put_view(AccountView)
942 |> render("relationship.json", %{user: blocker, target: blocked})
946 |> put_resp_content_type("application/json")
947 |> send_resp(403, Jason.encode!(%{"error" => message}))
951 def blocks(%{assigns: %{user: user}} = conn, _) do
952 with blocked_accounts <- User.blocked_users(user) do
953 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
958 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
959 json(conn, info.domain_blocks || [])
962 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
963 User.block_domain(blocker, domain)
967 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
968 User.unblock_domain(blocker, domain)
972 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
973 with %User{} = subscription_target <- User.get_cached_by_id(id),
974 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: user, target: subscription_target})
981 |> put_resp_content_type("application/json")
982 |> send_resp(403, Jason.encode!(%{"error" => message}))
986 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
987 with %User{} = subscription_target <- User.get_cached_by_id(id),
988 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
990 |> put_view(AccountView)
991 |> render("relationship.json", %{user: user, target: subscription_target})
995 |> put_resp_content_type("application/json")
996 |> send_resp(403, Jason.encode!(%{"error" => message}))
1000 def status_search(user, query) do
1002 if Regex.match?(~r/https?:/, query) do
1003 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1004 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1005 true <- Visibility.visible_for_user?(activity, user) do
1014 [a, o] in Activity.with_preloaded_object(Activity),
1015 where: fragment("?->>'type' = 'Create'", a.data),
1016 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1019 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1024 order_by: [desc: :id]
1027 Repo.all(q) ++ fetched
1030 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1031 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1033 statuses = status_search(user, query)
1035 tags_path = Web.base_url() <> "/tag/"
1041 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1042 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1043 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1046 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1048 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1055 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1056 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1058 statuses = status_search(user, query)
1064 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1065 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1068 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1070 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1077 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1078 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1080 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1085 def favourites(%{assigns: %{user: user}} = conn, params) do
1088 |> Map.put("type", "Create")
1089 |> Map.put("favorited_by", user.ap_id)
1090 |> Map.put("blocking_user", user)
1093 ActivityPub.fetch_activities([], params)
1096 user = Repo.preload(user, :bookmarks)
1099 |> add_link_headers(:favourites, activities)
1100 |> put_view(StatusView)
1101 |> render("index.json", %{activities: activities, for: user, as: :activity})
1104 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1105 with %User{} = user <- User.get_by_id(id),
1106 false <- user.info.hide_favorites do
1109 |> Map.put("type", "Create")
1110 |> Map.put("favorited_by", user.ap_id)
1111 |> Map.put("blocking_user", for_user)
1115 ["https://www.w3.org/ns/activitystreams#Public"] ++
1116 [for_user.ap_id | for_user.following]
1118 ["https://www.w3.org/ns/activitystreams#Public"]
1123 |> ActivityPub.fetch_activities(params)
1127 |> add_link_headers(:favourites, activities)
1128 |> put_view(StatusView)
1129 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1132 {:error, :not_found}
1137 |> json(%{error: "Can't get favorites"})
1141 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1142 user = User.get_cached_by_id(user.id)
1143 user = Repo.preload(user, :bookmarks)
1146 Bookmark.for_user_query(user.id)
1147 |> Pagination.fetch_paginated(params)
1151 |> Enum.map(fn b -> b.activity end)
1154 |> add_link_headers(:bookmarks, bookmarks)
1155 |> put_view(StatusView)
1156 |> render("index.json", %{activities: activities, for: user, as: :activity})
1159 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1160 lists = Pleroma.List.for_user(user, opts)
1161 res = ListView.render("lists.json", lists: lists)
1165 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1166 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1167 res = ListView.render("list.json", list: list)
1173 |> json(%{error: "Record not found"})
1177 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1178 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1179 res = ListView.render("lists.json", lists: lists)
1183 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1184 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1185 {:ok, _list} <- Pleroma.List.delete(list) do
1193 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1194 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1195 res = ListView.render("list.json", list: list)
1200 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1202 |> Enum.each(fn account_id ->
1203 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1204 %User{} = followed <- User.get_cached_by_id(account_id) do
1205 Pleroma.List.follow(list, followed)
1212 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1214 |> Enum.each(fn account_id ->
1215 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1216 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1217 Pleroma.List.unfollow(list, followed)
1224 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1225 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1226 {:ok, users} = Pleroma.List.get_following(list) do
1228 |> put_view(AccountView)
1229 |> render("accounts.json", %{users: users, as: :user})
1233 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1234 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1235 {:ok, list} <- Pleroma.List.rename(list, title) do
1236 res = ListView.render("list.json", list: list)
1244 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1245 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1248 |> Map.put("type", "Create")
1249 |> Map.put("blocking_user", user)
1250 |> Map.put("muting_user", user)
1252 # we must filter the following list for the user to avoid leaking statuses the user
1253 # does not actually have permission to see (for more info, peruse security issue #270).
1256 |> Enum.filter(fn x -> x in user.following end)
1257 |> ActivityPub.fetch_activities_bounded(following, params)
1260 user = Repo.preload(user, :bookmarks)
1263 |> put_view(StatusView)
1264 |> render("index.json", %{activities: activities, for: user, as: :activity})
1269 |> json(%{error: "Error."})
1273 def index(%{assigns: %{user: user}} = conn, _params) do
1274 token = get_session(conn, :oauth_token)
1277 mastodon_emoji = mastodonized_emoji()
1279 limit = Config.get([:instance, :limit])
1282 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1284 flavour = get_user_flavour(user)
1289 streaming_api_base_url:
1290 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1291 access_token: token,
1293 domain: Pleroma.Web.Endpoint.host(),
1296 unfollow_modal: false,
1299 auto_play_gif: false,
1300 display_sensitive_media: false,
1301 reduce_motion: false,
1302 max_toot_chars: limit,
1303 mascot: "/images/pleroma-fox-tan-smol.png"
1306 delete_others_notice: present?(user.info.is_moderator),
1307 admin: present?(user.info.is_admin)
1311 default_privacy: user.info.default_scope,
1312 default_sensitive: false,
1313 allow_content_types: Config.get([:instance, :allowed_post_formats])
1315 media_attachments: %{
1316 accept_content_types: [
1332 user.info.settings ||
1362 push_subscription: nil,
1364 custom_emojis: mastodon_emoji,
1370 |> put_layout(false)
1371 |> put_view(MastodonView)
1372 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1375 |> put_session(:return_to, conn.request_path)
1376 |> redirect(to: "/web/login")
1380 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1381 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1383 with changeset <- Ecto.Changeset.change(user),
1384 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1385 {:ok, _user} <- User.update_and_set_cache(changeset) do
1390 |> put_resp_content_type("application/json")
1391 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1395 @supported_flavours ["glitch", "vanilla"]
1397 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1398 when flavour in @supported_flavours do
1399 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1401 with changeset <- Ecto.Changeset.change(user),
1402 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1403 {:ok, user} <- User.update_and_set_cache(changeset),
1404 flavour <- user.info.flavour do
1409 |> put_resp_content_type("application/json")
1410 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1414 def set_flavour(conn, _params) do
1417 |> json(%{error: "Unsupported flavour"})
1420 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1421 json(conn, get_user_flavour(user))
1424 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1428 defp get_user_flavour(_) do
1432 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1433 redirect(conn, to: local_mastodon_root_path(conn))
1436 @doc "Local Mastodon FE login init action"
1437 def login(conn, %{"code" => auth_token}) do
1438 with {:ok, app} <- get_or_make_app(),
1439 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1440 {:ok, token} <- Token.exchange_token(app, auth) do
1442 |> put_session(:oauth_token, token.token)
1443 |> redirect(to: local_mastodon_root_path(conn))
1447 @doc "Local Mastodon FE callback action"
1448 def login(conn, _) do
1449 with {:ok, app} <- get_or_make_app() do
1454 response_type: "code",
1455 client_id: app.client_id,
1457 scope: Enum.join(app.scopes, " ")
1460 redirect(conn, to: path)
1464 defp local_mastodon_root_path(conn) do
1465 case get_session(conn, :return_to) do
1467 mastodon_api_path(conn, :index, ["getting-started"])
1470 delete_session(conn, :return_to)
1475 defp get_or_make_app do
1476 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1477 scopes = ["read", "write", "follow", "push"]
1479 with %App{} = app <- Repo.get_by(App, find_attrs) do
1481 if app.scopes == scopes do
1485 |> Ecto.Changeset.change(%{scopes: scopes})
1493 App.register_changeset(
1495 Map.put(find_attrs, :scopes, scopes)
1502 def logout(conn, _) do
1505 |> redirect(to: "/")
1508 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1509 Logger.debug("Unimplemented, returning unmodified relationship")
1511 with %User{} = target <- User.get_cached_by_id(id) do
1513 |> put_view(AccountView)
1514 |> render("relationship.json", %{user: user, target: target})
1518 def empty_array(conn, _) do
1519 Logger.debug("Unimplemented, returning an empty array")
1523 def empty_object(conn, _) do
1524 Logger.debug("Unimplemented, returning an empty object")
1528 def get_filters(%{assigns: %{user: user}} = conn, _) do
1529 filters = Filter.get_filters(user)
1530 res = FilterView.render("filters.json", filters: filters)
1535 %{assigns: %{user: user}} = conn,
1536 %{"phrase" => phrase, "context" => context} = params
1542 hide: Map.get(params, "irreversible", nil),
1543 whole_word: Map.get(params, "boolean", true)
1547 {:ok, response} = Filter.create(query)
1548 res = FilterView.render("filter.json", filter: response)
1552 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1553 filter = Filter.get(filter_id, user)
1554 res = FilterView.render("filter.json", filter: filter)
1559 %{assigns: %{user: user}} = conn,
1560 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1564 filter_id: filter_id,
1567 hide: Map.get(params, "irreversible", nil),
1568 whole_word: Map.get(params, "boolean", true)
1572 {:ok, response} = Filter.update(query)
1573 res = FilterView.render("filter.json", filter: response)
1577 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1580 filter_id: filter_id
1583 {:ok, _} = Filter.delete(query)
1589 def errors(conn, {:error, %Changeset{} = changeset}) do
1592 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1593 |> Enum.map_join(", ", fn {_k, v} -> v end)
1597 |> json(%{error: error_message})
1600 def errors(conn, {:error, :not_found}) do
1603 |> json(%{error: "Record not found"})
1606 def errors(conn, _) do
1609 |> json("Something went wrong")
1612 def suggestions(%{assigns: %{user: user}} = conn, _) do
1613 suggestions = Config.get(:suggestions)
1615 if Keyword.get(suggestions, :enabled, false) do
1616 api = Keyword.get(suggestions, :third_party_engine, "")
1617 timeout = Keyword.get(suggestions, :timeout, 5000)
1618 limit = Keyword.get(suggestions, :limit, 23)
1620 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1622 user = user.nickname
1626 |> String.replace("{{host}}", host)
1627 |> String.replace("{{user}}", user)
1629 with {:ok, %{status: 200, body: body}} <-
1634 recv_timeout: timeout,
1638 {:ok, data} <- Jason.decode(body) do
1641 |> Enum.slice(0, limit)
1646 case User.get_or_fetch(x["acct"]) do
1653 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1656 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1662 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1669 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1670 with %Activity{} = activity <- Activity.get_by_id(status_id),
1671 true <- Visibility.visible_for_user?(activity, user) do
1675 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1685 def reports(%{assigns: %{user: user}} = conn, params) do
1686 case CommonAPI.report(user, params) do
1689 |> put_view(ReportView)
1690 |> try_render("report.json", %{activity: activity})
1694 |> put_status(:bad_request)
1695 |> json(%{error: err})
1699 def try_render(conn, target, params)
1700 when is_binary(target) do
1701 res = render(conn, target, params)
1706 |> json(%{error: "Can't display this activity"})
1712 def try_render(conn, _, _) do
1715 |> json(%{error: "Can't display this activity"})
1718 defp present?(nil), do: false
1719 defp present?(false), do: false
1720 defp present?(_), do: true