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)
1097 |> add_link_headers(:favourites, activities)
1098 |> put_view(StatusView)
1099 |> render("index.json", %{activities: activities, for: user, as: :activity})
1102 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1103 with %User{} = user <- User.get_by_id(id),
1104 false <- user.info.hide_favorites do
1107 |> Map.put("type", "Create")
1108 |> Map.put("favorited_by", user.ap_id)
1109 |> Map.put("blocking_user", for_user)
1113 ["https://www.w3.org/ns/activitystreams#Public"] ++
1114 [for_user.ap_id | for_user.following]
1116 ["https://www.w3.org/ns/activitystreams#Public"]
1121 |> ActivityPub.fetch_activities(params)
1125 |> add_link_headers(:favourites, activities)
1126 |> put_view(StatusView)
1127 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1130 {:error, :not_found}
1135 |> json(%{error: "Can't get favorites"})
1139 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1140 user = User.get_cached_by_id(user.id)
1141 user = Repo.preload(user, :bookmarks)
1144 Bookmark.for_user_query(user.id)
1145 |> Pagination.fetch_paginated(params)
1149 |> Enum.map(fn b -> b.activity end)
1152 |> add_link_headers(:bookmarks, bookmarks)
1153 |> put_view(StatusView)
1154 |> render("index.json", %{activities: activities, for: user, as: :activity})
1157 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1158 lists = Pleroma.List.for_user(user, opts)
1159 res = ListView.render("lists.json", lists: lists)
1163 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1164 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1165 res = ListView.render("list.json", list: list)
1171 |> json(%{error: "Record not found"})
1175 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1176 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1177 res = ListView.render("lists.json", lists: lists)
1181 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1182 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1183 {:ok, _list} <- Pleroma.List.delete(list) do
1191 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1192 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1193 res = ListView.render("list.json", list: list)
1198 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1200 |> Enum.each(fn account_id ->
1201 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1202 %User{} = followed <- User.get_cached_by_id(account_id) do
1203 Pleroma.List.follow(list, followed)
1210 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1212 |> Enum.each(fn account_id ->
1213 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1214 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1215 Pleroma.List.unfollow(list, followed)
1222 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1223 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1224 {:ok, users} = Pleroma.List.get_following(list) do
1226 |> put_view(AccountView)
1227 |> render("accounts.json", %{users: users, as: :user})
1231 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1232 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1233 {:ok, list} <- Pleroma.List.rename(list, title) do
1234 res = ListView.render("list.json", list: list)
1242 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1243 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1246 |> Map.put("type", "Create")
1247 |> Map.put("blocking_user", user)
1248 |> Map.put("muting_user", user)
1250 # we must filter the following list for the user to avoid leaking statuses the user
1251 # does not actually have permission to see (for more info, peruse security issue #270).
1254 |> Enum.filter(fn x -> x in user.following end)
1255 |> ActivityPub.fetch_activities_bounded(following, params)
1258 user = Repo.preload(user, :bookmarks)
1261 |> put_view(StatusView)
1262 |> render("index.json", %{activities: activities, for: user, as: :activity})
1267 |> json(%{error: "Error."})
1271 def index(%{assigns: %{user: user}} = conn, _params) do
1272 token = get_session(conn, :oauth_token)
1275 mastodon_emoji = mastodonized_emoji()
1277 limit = Config.get([:instance, :limit])
1280 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1282 flavour = get_user_flavour(user)
1287 streaming_api_base_url:
1288 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1289 access_token: token,
1291 domain: Pleroma.Web.Endpoint.host(),
1294 unfollow_modal: false,
1297 auto_play_gif: false,
1298 display_sensitive_media: false,
1299 reduce_motion: false,
1300 max_toot_chars: limit,
1301 mascot: "/images/pleroma-fox-tan-smol.png"
1304 delete_others_notice: present?(user.info.is_moderator),
1305 admin: present?(user.info.is_admin)
1309 default_privacy: user.info.default_scope,
1310 default_sensitive: false,
1311 allow_content_types: Config.get([:instance, :allowed_post_formats])
1313 media_attachments: %{
1314 accept_content_types: [
1330 user.info.settings ||
1360 push_subscription: nil,
1362 custom_emojis: mastodon_emoji,
1368 |> put_layout(false)
1369 |> put_view(MastodonView)
1370 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1373 |> put_session(:return_to, conn.request_path)
1374 |> redirect(to: "/web/login")
1378 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1379 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1381 with changeset <- Ecto.Changeset.change(user),
1382 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1383 {:ok, _user} <- User.update_and_set_cache(changeset) do
1388 |> put_resp_content_type("application/json")
1389 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1393 @supported_flavours ["glitch", "vanilla"]
1395 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1396 when flavour in @supported_flavours do
1397 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1399 with changeset <- Ecto.Changeset.change(user),
1400 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1401 {:ok, user} <- User.update_and_set_cache(changeset),
1402 flavour <- user.info.flavour do
1407 |> put_resp_content_type("application/json")
1408 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1412 def set_flavour(conn, _params) do
1415 |> json(%{error: "Unsupported flavour"})
1418 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1419 json(conn, get_user_flavour(user))
1422 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1426 defp get_user_flavour(_) do
1430 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1431 redirect(conn, to: local_mastodon_root_path(conn))
1434 @doc "Local Mastodon FE login init action"
1435 def login(conn, %{"code" => auth_token}) do
1436 with {:ok, app} <- get_or_make_app(),
1437 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1438 {:ok, token} <- Token.exchange_token(app, auth) do
1440 |> put_session(:oauth_token, token.token)
1441 |> redirect(to: local_mastodon_root_path(conn))
1445 @doc "Local Mastodon FE callback action"
1446 def login(conn, _) do
1447 with {:ok, app} <- get_or_make_app() do
1452 response_type: "code",
1453 client_id: app.client_id,
1455 scope: Enum.join(app.scopes, " ")
1458 redirect(conn, to: path)
1462 defp local_mastodon_root_path(conn) do
1463 case get_session(conn, :return_to) do
1465 mastodon_api_path(conn, :index, ["getting-started"])
1468 delete_session(conn, :return_to)
1473 defp get_or_make_app do
1474 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1475 scopes = ["read", "write", "follow", "push"]
1477 with %App{} = app <- Repo.get_by(App, find_attrs) do
1479 if app.scopes == scopes do
1483 |> Ecto.Changeset.change(%{scopes: scopes})
1491 App.register_changeset(
1493 Map.put(find_attrs, :scopes, scopes)
1500 def logout(conn, _) do
1503 |> redirect(to: "/")
1506 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1507 Logger.debug("Unimplemented, returning unmodified relationship")
1509 with %User{} = target <- User.get_cached_by_id(id) do
1511 |> put_view(AccountView)
1512 |> render("relationship.json", %{user: user, target: target})
1516 def empty_array(conn, _) do
1517 Logger.debug("Unimplemented, returning an empty array")
1521 def empty_object(conn, _) do
1522 Logger.debug("Unimplemented, returning an empty object")
1526 def get_filters(%{assigns: %{user: user}} = conn, _) do
1527 filters = Filter.get_filters(user)
1528 res = FilterView.render("filters.json", filters: filters)
1533 %{assigns: %{user: user}} = conn,
1534 %{"phrase" => phrase, "context" => context} = params
1540 hide: Map.get(params, "irreversible", nil),
1541 whole_word: Map.get(params, "boolean", true)
1545 {:ok, response} = Filter.create(query)
1546 res = FilterView.render("filter.json", filter: response)
1550 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1551 filter = Filter.get(filter_id, user)
1552 res = FilterView.render("filter.json", filter: filter)
1557 %{assigns: %{user: user}} = conn,
1558 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1562 filter_id: filter_id,
1565 hide: Map.get(params, "irreversible", nil),
1566 whole_word: Map.get(params, "boolean", true)
1570 {:ok, response} = Filter.update(query)
1571 res = FilterView.render("filter.json", filter: response)
1575 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1578 filter_id: filter_id
1581 {:ok, _} = Filter.delete(query)
1587 def errors(conn, {:error, %Changeset{} = changeset}) do
1590 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1591 |> Enum.map_join(", ", fn {_k, v} -> v end)
1595 |> json(%{error: error_message})
1598 def errors(conn, {:error, :not_found}) do
1601 |> json(%{error: "Record not found"})
1604 def errors(conn, _) do
1607 |> json("Something went wrong")
1610 def suggestions(%{assigns: %{user: user}} = conn, _) do
1611 suggestions = Config.get(:suggestions)
1613 if Keyword.get(suggestions, :enabled, false) do
1614 api = Keyword.get(suggestions, :third_party_engine, "")
1615 timeout = Keyword.get(suggestions, :timeout, 5000)
1616 limit = Keyword.get(suggestions, :limit, 23)
1618 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1620 user = user.nickname
1624 |> String.replace("{{host}}", host)
1625 |> String.replace("{{user}}", user)
1627 with {:ok, %{status: 200, body: body}} <-
1632 recv_timeout: timeout,
1636 {:ok, data} <- Jason.decode(body) do
1639 |> Enum.slice(0, limit)
1644 case User.get_or_fetch(x["acct"]) do
1651 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1654 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1660 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1667 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1668 with %Activity{} = activity <- Activity.get_by_id(status_id),
1669 true <- Visibility.visible_for_user?(activity, user) do
1673 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1683 def reports(%{assigns: %{user: user}} = conn, params) do
1684 case CommonAPI.report(user, params) do
1687 |> put_view(ReportView)
1688 |> try_render("report.json", %{activity: activity})
1692 |> put_status(:bad_request)
1693 |> json(%{error: err})
1697 def try_render(conn, target, params)
1698 when is_binary(target) do
1699 res = render(conn, target, params)
1704 |> json(%{error: "Can't display this activity"})
1710 def try_render(conn, _, _) do
1713 |> json(%{error: "Can't display this activity"})
1716 defp present?(nil), do: false
1717 defp present?(false), do: false
1718 defp present?(_), do: true