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.Conversation.Participation
13 alias Pleroma.Notification
15 alias Pleroma.Object.Fetcher
16 alias Pleroma.Pagination
18 alias Pleroma.ScheduledActivity
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Visibility
24 alias Pleroma.Web.CommonAPI
25 alias Pleroma.Web.MastodonAPI.AccountView
26 alias Pleroma.Web.MastodonAPI.AppView
27 alias Pleroma.Web.MastodonAPI.ConversationView
28 alias Pleroma.Web.MastodonAPI.FilterView
29 alias Pleroma.Web.MastodonAPI.ListView
30 alias Pleroma.Web.MastodonAPI.MastodonAPI
31 alias Pleroma.Web.MastodonAPI.MastodonView
32 alias Pleroma.Web.MastodonAPI.NotificationView
33 alias Pleroma.Web.MastodonAPI.ReportView
34 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
35 alias Pleroma.Web.MastodonAPI.StatusView
36 alias Pleroma.Web.MediaProxy
37 alias Pleroma.Web.OAuth.App
38 alias Pleroma.Web.OAuth.Authorization
39 alias Pleroma.Web.OAuth.Token
41 alias Pleroma.Web.ControllerHelper
46 @httpoison Application.get_env(:pleroma, :httpoison)
47 @local_mastodon_name "Mastodon-Local"
49 action_fallback(:errors)
51 def create_app(conn, params) do
52 scopes = ControllerHelper.oauth_scopes(params, ["read"])
56 |> Map.drop(["scope", "scopes"])
57 |> Map.put("scopes", scopes)
59 with cs <- App.register_changeset(%App{}, app_attrs),
60 false <- cs.changes[:client_name] == @local_mastodon_name,
61 {:ok, app} <- Repo.insert(cs) do
64 |> render("show.json", %{app: app})
73 value_function \\ fn x -> {:ok, x} end
75 if Map.has_key?(params, params_field) do
76 case value_function.(params[params_field]) do
77 {:ok, new_value} -> Map.put(map, map_field, new_value)
85 def update_credentials(%{assigns: %{user: user}} = conn, params) do
90 |> add_if_present(params, "display_name", :name)
91 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
92 |> add_if_present(params, "avatar", :avatar, fn value ->
93 with %Plug.Upload{} <- value,
94 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
102 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
103 |> Enum.reduce(%{}, fn key, acc ->
104 add_if_present(acc, params, to_string(key), key, fn value ->
105 {:ok, ControllerHelper.truthy_param?(value)}
108 |> add_if_present(params, "default_scope", :default_scope)
109 |> add_if_present(params, "header", :banner, fn value ->
110 with %Plug.Upload{} <- value,
111 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
118 info_cng = User.Info.profile_update(user.info, info_params)
120 with changeset <- User.update_changeset(user, user_params),
121 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
122 {:ok, user} <- User.update_and_set_cache(changeset) do
123 if original_user != user do
124 CommonAPI.update(user)
127 json(conn, AccountView.render("account.json", %{user: user, for: user}))
132 |> json(%{error: "Invalid request"})
136 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
137 account = AccountView.render("account.json", %{user: user, for: user})
141 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
142 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
145 |> render("short.json", %{app: app})
149 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
150 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
151 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
152 account = AccountView.render("account.json", %{user: user, for: for_user})
158 |> json(%{error: "Can't find user"})
162 @mastodon_api_level "2.5.0"
164 def masto_instance(conn, _params) do
165 instance = Config.get(:instance)
169 title: Keyword.get(instance, :name),
170 description: Keyword.get(instance, :description),
171 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
172 email: Keyword.get(instance, :email),
174 streaming_api: Pleroma.Web.Endpoint.websocket_url()
176 stats: Stats.get_stats(),
177 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
179 registrations: Pleroma.Config.get([:instance, :registrations_open]),
180 # Extra (not present in Mastodon):
181 max_toot_chars: Keyword.get(instance, :limit)
187 def peers(conn, _params) do
188 json(conn, Stats.get_peers())
191 defp mastodonized_emoji do
192 Pleroma.Emoji.get_all()
193 |> Enum.map(fn {shortcode, relative_url, tags} ->
194 url = to_string(URI.merge(Web.base_url(), relative_url))
197 "shortcode" => shortcode,
199 "visible_in_picker" => true,
206 def custom_emojis(conn, _params) do
207 mastodon_emoji = mastodonized_emoji()
208 json(conn, mastodon_emoji)
211 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
214 |> Map.drop(["since_id", "max_id", "min_id"])
217 last = List.last(activities)
224 |> Map.get("limit", "20")
225 |> String.to_integer()
228 if length(activities) <= limit do
234 |> Enum.at(limit * -1)
238 {next_url, prev_url} =
242 Pleroma.Web.Endpoint,
245 Map.merge(params, %{max_id: max_id})
248 Pleroma.Web.Endpoint,
251 Map.merge(params, %{min_id: min_id})
257 Pleroma.Web.Endpoint,
259 Map.merge(params, %{max_id: max_id})
262 Pleroma.Web.Endpoint,
264 Map.merge(params, %{min_id: min_id})
270 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
276 def home_timeline(%{assigns: %{user: user}} = conn, params) do
279 |> Map.put("type", ["Create", "Announce"])
280 |> Map.put("blocking_user", user)
281 |> Map.put("muting_user", user)
282 |> Map.put("user", user)
285 [user.ap_id | user.following]
286 |> ActivityPub.fetch_activities(params)
287 |> ActivityPub.contain_timeline(user)
290 user = Repo.preload(user, bookmarks: :activity)
293 |> add_link_headers(:home_timeline, activities)
294 |> put_view(StatusView)
295 |> render("index.json", %{activities: activities, for: user, as: :activity})
298 def public_timeline(%{assigns: %{user: user}} = conn, params) do
299 local_only = params["local"] in [true, "True", "true", "1"]
303 |> Map.put("type", ["Create", "Announce"])
304 |> Map.put("local_only", local_only)
305 |> Map.put("blocking_user", user)
306 |> Map.put("muting_user", user)
307 |> ActivityPub.fetch_public_activities()
310 user = Repo.preload(user, bookmarks: :activity)
313 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
314 |> put_view(StatusView)
315 |> render("index.json", %{activities: activities, for: user, as: :activity})
318 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
319 with %User{} = user <- User.get_cached_by_id(params["id"]),
320 reading_user <- Repo.preload(reading_user, :bookmarks) do
321 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
324 |> add_link_headers(:user_statuses, activities, params["id"])
325 |> put_view(StatusView)
326 |> render("index.json", %{
327 activities: activities,
334 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
337 |> Map.put("type", "Create")
338 |> Map.put("blocking_user", user)
339 |> Map.put("user", user)
340 |> Map.put(:visibility, "direct")
344 |> ActivityPub.fetch_activities_query(params)
345 |> Pagination.fetch_paginated(params)
347 user = Repo.preload(user, bookmarks: :activity)
350 |> add_link_headers(:dm_timeline, activities)
351 |> put_view(StatusView)
352 |> render("index.json", %{activities: activities, for: user, as: :activity})
355 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
356 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
357 true <- Visibility.visible_for_user?(activity, user) do
358 user = Repo.preload(user, bookmarks: :activity)
361 |> put_view(StatusView)
362 |> try_render("status.json", %{activity: activity, for: user})
366 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
367 with %Activity{} = activity <- Activity.get_by_id(id),
369 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
370 "blocking_user" => user,
374 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
376 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
377 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
383 activities: grouped_activities[true] || [],
387 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
392 activities: grouped_activities[false] || [],
396 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
403 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
404 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
406 |> add_link_headers(:scheduled_statuses, scheduled_activities)
407 |> put_view(ScheduledActivityView)
408 |> render("index.json", %{scheduled_activities: scheduled_activities})
412 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
413 with %ScheduledActivity{} = scheduled_activity <-
414 ScheduledActivity.get(user, scheduled_activity_id) do
416 |> put_view(ScheduledActivityView)
417 |> render("show.json", %{scheduled_activity: scheduled_activity})
419 _ -> {:error, :not_found}
423 def update_scheduled_status(
424 %{assigns: %{user: user}} = conn,
425 %{"id" => scheduled_activity_id} = params
427 with %ScheduledActivity{} = scheduled_activity <-
428 ScheduledActivity.get(user, scheduled_activity_id),
429 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
431 |> put_view(ScheduledActivityView)
432 |> render("show.json", %{scheduled_activity: scheduled_activity})
434 nil -> {:error, :not_found}
439 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
440 with %ScheduledActivity{} = scheduled_activity <-
441 ScheduledActivity.get(user, scheduled_activity_id),
442 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
444 |> put_view(ScheduledActivityView)
445 |> render("show.json", %{scheduled_activity: scheduled_activity})
447 nil -> {:error, :not_found}
452 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
453 when length(media_ids) > 0 do
456 |> Map.put("status", ".")
458 post_status(conn, params)
461 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
464 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
467 case get_req_header(conn, "idempotency-key") do
469 _ -> Ecto.UUID.generate()
472 scheduled_at = params["scheduled_at"]
474 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
475 with {:ok, scheduled_activity} <-
476 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
478 |> put_view(ScheduledActivityView)
479 |> render("show.json", %{scheduled_activity: scheduled_activity})
482 params = Map.drop(params, ["scheduled_at"])
485 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
486 CommonAPI.post(user, params)
490 |> put_view(StatusView)
491 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
495 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
496 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
502 |> json(%{error: "Can't delete this post"})
506 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
507 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
508 %Activity{} = announce <- Activity.normalize(announce.data) do
509 user = Repo.preload(user, bookmarks: :activity)
512 |> put_view(StatusView)
513 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
517 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
518 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
519 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
520 user = Repo.preload(user, bookmarks: :activity)
523 |> put_view(StatusView)
524 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
528 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
529 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
530 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
532 |> put_view(StatusView)
533 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
537 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
538 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
539 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
541 |> put_view(StatusView)
542 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
546 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
547 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
549 |> put_view(StatusView)
550 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
554 |> put_resp_content_type("application/json")
555 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
559 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
560 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
562 |> put_view(StatusView)
563 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
567 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
568 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
569 %User{} = user <- User.get_cached_by_nickname(user.nickname),
570 true <- Visibility.visible_for_user?(activity, user),
571 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
572 user = Repo.preload(user, bookmarks: :activity)
575 |> put_view(StatusView)
576 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
580 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
581 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
582 %User{} = user <- User.get_cached_by_nickname(user.nickname),
583 true <- Visibility.visible_for_user?(activity, user),
584 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
585 user = Repo.preload(user, bookmarks: :activity)
588 |> put_view(StatusView)
589 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
593 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
594 activity = Activity.get_by_id(id)
596 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
598 |> put_view(StatusView)
599 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
603 |> put_resp_content_type("application/json")
604 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
608 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
609 activity = Activity.get_by_id(id)
611 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
613 |> put_view(StatusView)
614 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
618 def notifications(%{assigns: %{user: user}} = conn, params) do
619 notifications = MastodonAPI.get_notifications(user, params)
622 |> add_link_headers(:notifications, notifications)
623 |> put_view(NotificationView)
624 |> render("index.json", %{notifications: notifications, for: user})
627 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
628 with {:ok, notification} <- Notification.get(user, id) do
630 |> put_view(NotificationView)
631 |> render("show.json", %{notification: notification, for: user})
635 |> put_resp_content_type("application/json")
636 |> send_resp(403, Jason.encode!(%{"error" => reason}))
640 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
641 Notification.clear(user)
645 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
646 with {:ok, _notif} <- Notification.dismiss(user, id) do
651 |> put_resp_content_type("application/json")
652 |> send_resp(403, Jason.encode!(%{"error" => reason}))
656 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
657 Notification.destroy_multiple(user, ids)
661 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
663 q = from(u in User, where: u.id in ^id)
664 targets = Repo.all(q)
667 |> put_view(AccountView)
668 |> render("relationships.json", %{user: user, targets: targets})
671 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
672 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
674 def update_media(%{assigns: %{user: user}} = conn, data) do
675 with %Object{} = object <- Repo.get(Object, data["id"]),
676 true <- Object.authorize_mutation(object, user),
677 true <- is_binary(data["description"]),
678 description <- data["description"] do
679 new_data = %{object.data | "name" => description}
683 |> Object.change(%{data: new_data})
686 attachment_data = Map.put(new_data, "id", object.id)
689 |> put_view(StatusView)
690 |> render("attachment.json", %{attachment: attachment_data})
694 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
695 with {:ok, object} <-
698 actor: User.ap_id(user),
699 description: Map.get(data, "description")
701 attachment_data = Map.put(object.data, "id", object.id)
704 |> put_view(StatusView)
705 |> render("attachment.json", %{attachment: attachment_data})
709 def favourited_by(conn, %{"id" => id}) do
710 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
711 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
712 q = from(u in User, where: u.ap_id in ^likes)
716 |> put_view(AccountView)
717 |> render(AccountView, "accounts.json", %{users: users, as: :user})
723 def reblogged_by(conn, %{"id" => id}) do
724 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
725 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
726 q = from(u in User, where: u.ap_id in ^announces)
730 |> put_view(AccountView)
731 |> render("accounts.json", %{users: users, as: :user})
737 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
738 local_only = params["local"] in [true, "True", "true", "1"]
741 [params["tag"], params["any"]]
745 |> Enum.map(&String.downcase(&1))
750 |> Enum.map(&String.downcase(&1))
755 |> Enum.map(&String.downcase(&1))
759 |> Map.put("type", "Create")
760 |> Map.put("local_only", local_only)
761 |> Map.put("blocking_user", user)
762 |> Map.put("muting_user", user)
763 |> Map.put("tag", tags)
764 |> Map.put("tag_all", tag_all)
765 |> Map.put("tag_reject", tag_reject)
766 |> ActivityPub.fetch_public_activities()
770 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
771 |> put_view(StatusView)
772 |> render("index.json", %{activities: activities, for: user, as: :activity})
775 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
776 with %User{} = user <- User.get_cached_by_id(id),
777 followers <- MastodonAPI.get_followers(user, params) do
780 for_user && user.id == for_user.id -> followers
781 user.info.hide_followers -> []
786 |> add_link_headers(:followers, followers, user)
787 |> put_view(AccountView)
788 |> render("accounts.json", %{users: followers, as: :user})
792 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
793 with %User{} = user <- User.get_cached_by_id(id),
794 followers <- MastodonAPI.get_friends(user, params) do
797 for_user && user.id == for_user.id -> followers
798 user.info.hide_follows -> []
803 |> add_link_headers(:following, followers, user)
804 |> put_view(AccountView)
805 |> render("accounts.json", %{users: followers, as: :user})
809 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
810 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
812 |> put_view(AccountView)
813 |> render("accounts.json", %{users: follow_requests, as: :user})
817 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
818 with %User{} = follower <- User.get_cached_by_id(id),
819 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
821 |> put_view(AccountView)
822 |> render("relationship.json", %{user: followed, target: follower})
826 |> put_resp_content_type("application/json")
827 |> send_resp(403, Jason.encode!(%{"error" => message}))
831 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
832 with %User{} = follower <- User.get_cached_by_id(id),
833 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
835 |> put_view(AccountView)
836 |> render("relationship.json", %{user: followed, target: follower})
840 |> put_resp_content_type("application/json")
841 |> send_resp(403, Jason.encode!(%{"error" => message}))
845 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
846 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
847 {_, true} <- {:followed, follower.id != followed.id},
848 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
850 |> put_view(AccountView)
851 |> render("relationship.json", %{user: follower, target: followed})
858 |> put_resp_content_type("application/json")
859 |> send_resp(403, Jason.encode!(%{"error" => message}))
863 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
864 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
865 {_, true} <- {:followed, follower.id != followed.id},
866 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
868 |> put_view(AccountView)
869 |> render("account.json", %{user: followed, for: follower})
876 |> put_resp_content_type("application/json")
877 |> send_resp(403, Jason.encode!(%{"error" => message}))
881 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
882 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
883 {_, true} <- {:followed, follower.id != followed.id},
884 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
886 |> put_view(AccountView)
887 |> render("relationship.json", %{user: follower, target: followed})
897 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
898 with %User{} = muted <- User.get_cached_by_id(id),
899 {:ok, muter} <- User.mute(muter, muted) do
901 |> put_view(AccountView)
902 |> render("relationship.json", %{user: muter, target: muted})
906 |> put_resp_content_type("application/json")
907 |> send_resp(403, Jason.encode!(%{"error" => message}))
911 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
912 with %User{} = muted <- User.get_cached_by_id(id),
913 {:ok, muter} <- User.unmute(muter, muted) do
915 |> put_view(AccountView)
916 |> render("relationship.json", %{user: muter, target: muted})
920 |> put_resp_content_type("application/json")
921 |> send_resp(403, Jason.encode!(%{"error" => message}))
925 def mutes(%{assigns: %{user: user}} = conn, _) do
926 with muted_accounts <- User.muted_users(user) do
927 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
932 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
933 with %User{} = blocked <- User.get_cached_by_id(id),
934 {:ok, blocker} <- User.block(blocker, blocked),
935 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
937 |> put_view(AccountView)
938 |> render("relationship.json", %{user: blocker, target: blocked})
942 |> put_resp_content_type("application/json")
943 |> send_resp(403, Jason.encode!(%{"error" => message}))
947 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
948 with %User{} = blocked <- User.get_cached_by_id(id),
949 {:ok, blocker} <- User.unblock(blocker, blocked),
950 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
952 |> put_view(AccountView)
953 |> render("relationship.json", %{user: blocker, target: blocked})
957 |> put_resp_content_type("application/json")
958 |> send_resp(403, Jason.encode!(%{"error" => message}))
962 def blocks(%{assigns: %{user: user}} = conn, _) do
963 with blocked_accounts <- User.blocked_users(user) do
964 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
969 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
970 json(conn, info.domain_blocks || [])
973 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
974 User.block_domain(blocker, domain)
978 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
979 User.unblock_domain(blocker, domain)
983 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
984 with %User{} = subscription_target <- User.get_cached_by_id(id),
985 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
987 |> put_view(AccountView)
988 |> render("relationship.json", %{user: user, target: subscription_target})
992 |> put_resp_content_type("application/json")
993 |> send_resp(403, Jason.encode!(%{"error" => message}))
997 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
998 with %User{} = subscription_target <- User.get_cached_by_id(id),
999 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1001 |> put_view(AccountView)
1002 |> render("relationship.json", %{user: user, target: subscription_target})
1004 {:error, message} ->
1006 |> put_resp_content_type("application/json")
1007 |> send_resp(403, Jason.encode!(%{"error" => message}))
1011 def status_search(user, query) do
1013 if Regex.match?(~r/https?:/, query) do
1014 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1015 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1016 true <- Visibility.visible_for_user?(activity, user) do
1025 [a, o] in Activity.with_preloaded_object(Activity),
1026 where: fragment("?->>'type' = 'Create'", a.data),
1027 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1030 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1035 order_by: [desc: :id]
1038 Repo.all(q) ++ fetched
1041 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1042 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1044 statuses = status_search(user, query)
1046 tags_path = Web.base_url() <> "/tag/"
1052 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1053 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1054 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1057 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1059 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1066 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1067 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1069 statuses = status_search(user, query)
1075 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1076 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1079 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1081 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1088 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1089 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1091 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1096 def favourites(%{assigns: %{user: user}} = conn, params) do
1099 |> Map.put("type", "Create")
1100 |> Map.put("favorited_by", user.ap_id)
1101 |> Map.put("blocking_user", user)
1104 ActivityPub.fetch_activities([], params)
1107 user = Repo.preload(user, bookmarks: :activity)
1110 |> add_link_headers(:favourites, activities)
1111 |> put_view(StatusView)
1112 |> render("index.json", %{activities: activities, for: user, as: :activity})
1115 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1116 with %User{} = user <- User.get_by_id(id),
1117 false <- user.info.hide_favorites do
1120 |> Map.put("type", "Create")
1121 |> Map.put("favorited_by", user.ap_id)
1122 |> Map.put("blocking_user", for_user)
1126 ["https://www.w3.org/ns/activitystreams#Public"] ++
1127 [for_user.ap_id | for_user.following]
1129 ["https://www.w3.org/ns/activitystreams#Public"]
1134 |> ActivityPub.fetch_activities(params)
1138 |> add_link_headers(:favourites, activities)
1139 |> put_view(StatusView)
1140 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1143 {:error, :not_found}
1148 |> json(%{error: "Can't get favorites"})
1152 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1153 user = User.get_cached_by_id(user.id)
1154 user = Repo.preload(user, bookmarks: :activity)
1157 Bookmark.for_user_query(user.id)
1158 |> Pagination.fetch_paginated(params)
1162 |> Enum.map(fn b -> b.activity end)
1165 |> add_link_headers(:bookmarks, bookmarks)
1166 |> put_view(StatusView)
1167 |> render("index.json", %{activities: activities, for: user, as: :activity})
1170 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1171 lists = Pleroma.List.for_user(user, opts)
1172 res = ListView.render("lists.json", lists: lists)
1176 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1177 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1178 res = ListView.render("list.json", list: list)
1184 |> json(%{error: "Record not found"})
1188 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1189 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1190 res = ListView.render("lists.json", lists: lists)
1194 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1195 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1196 {:ok, _list} <- Pleroma.List.delete(list) do
1204 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1205 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1206 res = ListView.render("list.json", list: list)
1211 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1213 |> Enum.each(fn account_id ->
1214 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1215 %User{} = followed <- User.get_cached_by_id(account_id) do
1216 Pleroma.List.follow(list, followed)
1223 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1225 |> Enum.each(fn account_id ->
1226 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1227 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1228 Pleroma.List.unfollow(list, followed)
1235 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1236 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1237 {:ok, users} = Pleroma.List.get_following(list) do
1239 |> put_view(AccountView)
1240 |> render("accounts.json", %{users: users, as: :user})
1244 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1245 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1246 {:ok, list} <- Pleroma.List.rename(list, title) do
1247 res = ListView.render("list.json", list: list)
1255 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1256 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1259 |> Map.put("type", "Create")
1260 |> Map.put("blocking_user", user)
1261 |> Map.put("muting_user", user)
1263 # we must filter the following list for the user to avoid leaking statuses the user
1264 # does not actually have permission to see (for more info, peruse security issue #270).
1267 |> Enum.filter(fn x -> x in user.following end)
1268 |> ActivityPub.fetch_activities_bounded(following, params)
1271 user = Repo.preload(user, bookmarks: :activity)
1274 |> put_view(StatusView)
1275 |> render("index.json", %{activities: activities, for: user, as: :activity})
1280 |> json(%{error: "Error."})
1284 def index(%{assigns: %{user: user}} = conn, _params) do
1285 token = get_session(conn, :oauth_token)
1288 mastodon_emoji = mastodonized_emoji()
1290 limit = Config.get([:instance, :limit])
1293 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1295 flavour = get_user_flavour(user)
1300 streaming_api_base_url:
1301 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1302 access_token: token,
1304 domain: Pleroma.Web.Endpoint.host(),
1307 unfollow_modal: false,
1310 auto_play_gif: false,
1311 display_sensitive_media: false,
1312 reduce_motion: false,
1313 max_toot_chars: limit,
1314 mascot: "/images/pleroma-fox-tan-smol.png"
1317 delete_others_notice: present?(user.info.is_moderator),
1318 admin: present?(user.info.is_admin)
1322 default_privacy: user.info.default_scope,
1323 default_sensitive: false,
1324 allow_content_types: Config.get([:instance, :allowed_post_formats])
1326 media_attachments: %{
1327 accept_content_types: [
1343 user.info.settings ||
1373 push_subscription: nil,
1375 custom_emojis: mastodon_emoji,
1381 |> put_layout(false)
1382 |> put_view(MastodonView)
1383 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1386 |> put_session(:return_to, conn.request_path)
1387 |> redirect(to: "/web/login")
1391 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1392 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1394 with changeset <- Ecto.Changeset.change(user),
1395 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1396 {:ok, _user} <- User.update_and_set_cache(changeset) do
1401 |> put_resp_content_type("application/json")
1402 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1406 @supported_flavours ["glitch", "vanilla"]
1408 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1409 when flavour in @supported_flavours do
1410 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1412 with changeset <- Ecto.Changeset.change(user),
1413 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1414 {:ok, user} <- User.update_and_set_cache(changeset),
1415 flavour <- user.info.flavour do
1420 |> put_resp_content_type("application/json")
1421 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1425 def set_flavour(conn, _params) do
1428 |> json(%{error: "Unsupported flavour"})
1431 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1432 json(conn, get_user_flavour(user))
1435 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1439 defp get_user_flavour(_) do
1443 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1444 redirect(conn, to: local_mastodon_root_path(conn))
1447 @doc "Local Mastodon FE login init action"
1448 def login(conn, %{"code" => auth_token}) do
1449 with {:ok, app} <- get_or_make_app(),
1450 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1451 {:ok, token} <- Token.exchange_token(app, auth) do
1453 |> put_session(:oauth_token, token.token)
1454 |> redirect(to: local_mastodon_root_path(conn))
1458 @doc "Local Mastodon FE callback action"
1459 def login(conn, _) do
1460 with {:ok, app} <- get_or_make_app() do
1465 response_type: "code",
1466 client_id: app.client_id,
1468 scope: Enum.join(app.scopes, " ")
1471 redirect(conn, to: path)
1475 defp local_mastodon_root_path(conn) do
1476 case get_session(conn, :return_to) do
1478 mastodon_api_path(conn, :index, ["getting-started"])
1481 delete_session(conn, :return_to)
1486 defp get_or_make_app do
1487 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1488 scopes = ["read", "write", "follow", "push"]
1490 with %App{} = app <- Repo.get_by(App, find_attrs) do
1492 if app.scopes == scopes do
1496 |> Ecto.Changeset.change(%{scopes: scopes})
1504 App.register_changeset(
1506 Map.put(find_attrs, :scopes, scopes)
1513 def logout(conn, _) do
1516 |> redirect(to: "/")
1519 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1520 Logger.debug("Unimplemented, returning unmodified relationship")
1522 with %User{} = target <- User.get_cached_by_id(id) do
1524 |> put_view(AccountView)
1525 |> render("relationship.json", %{user: user, target: target})
1529 def empty_array(conn, _) do
1530 Logger.debug("Unimplemented, returning an empty array")
1534 def empty_object(conn, _) do
1535 Logger.debug("Unimplemented, returning an empty object")
1539 def get_filters(%{assigns: %{user: user}} = conn, _) do
1540 filters = Filter.get_filters(user)
1541 res = FilterView.render("filters.json", filters: filters)
1546 %{assigns: %{user: user}} = conn,
1547 %{"phrase" => phrase, "context" => context} = params
1553 hide: Map.get(params, "irreversible", nil),
1554 whole_word: Map.get(params, "boolean", true)
1558 {:ok, response} = Filter.create(query)
1559 res = FilterView.render("filter.json", filter: response)
1563 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1564 filter = Filter.get(filter_id, user)
1565 res = FilterView.render("filter.json", filter: filter)
1570 %{assigns: %{user: user}} = conn,
1571 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1575 filter_id: filter_id,
1578 hide: Map.get(params, "irreversible", nil),
1579 whole_word: Map.get(params, "boolean", true)
1583 {:ok, response} = Filter.update(query)
1584 res = FilterView.render("filter.json", filter: response)
1588 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1591 filter_id: filter_id
1594 {:ok, _} = Filter.delete(query)
1600 def errors(conn, {:error, %Changeset{} = changeset}) do
1603 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1604 |> Enum.map_join(", ", fn {_k, v} -> v end)
1608 |> json(%{error: error_message})
1611 def errors(conn, {:error, :not_found}) do
1614 |> json(%{error: "Record not found"})
1617 def errors(conn, _) do
1620 |> json("Something went wrong")
1623 def suggestions(%{assigns: %{user: user}} = conn, _) do
1624 suggestions = Config.get(:suggestions)
1626 if Keyword.get(suggestions, :enabled, false) do
1627 api = Keyword.get(suggestions, :third_party_engine, "")
1628 timeout = Keyword.get(suggestions, :timeout, 5000)
1629 limit = Keyword.get(suggestions, :limit, 23)
1631 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1633 user = user.nickname
1637 |> String.replace("{{host}}", host)
1638 |> String.replace("{{user}}", user)
1640 with {:ok, %{status: 200, body: body}} <-
1645 recv_timeout: timeout,
1649 {:ok, data} <- Jason.decode(body) do
1652 |> Enum.slice(0, limit)
1657 case User.get_or_fetch(x["acct"]) do
1658 {:ok, %User{id: id}} -> id
1664 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1667 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1673 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1680 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1681 with %Activity{} = activity <- Activity.get_by_id(status_id),
1682 true <- Visibility.visible_for_user?(activity, user) do
1686 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1696 def reports(%{assigns: %{user: user}} = conn, params) do
1697 case CommonAPI.report(user, params) do
1700 |> put_view(ReportView)
1701 |> try_render("report.json", %{activity: activity})
1705 |> put_status(:bad_request)
1706 |> json(%{error: err})
1710 def conversations(%{assigns: %{user: user}} = conn, params) do
1711 participations = Participation.for_user_with_last_activity_id(user, params)
1714 Enum.map(participations, fn participation ->
1715 ConversationView.render("participation.json", %{participation: participation, user: user})
1719 |> add_link_headers(:conversations, participations)
1720 |> json(conversations)
1723 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1724 with %Participation{} = participation <-
1725 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1726 {:ok, participation} <- Participation.mark_as_read(participation) do
1727 participation_view =
1728 ConversationView.render("participation.json", %{participation: participation, user: user})
1731 |> json(participation_view)
1735 def try_render(conn, target, params)
1736 when is_binary(target) do
1737 res = render(conn, target, params)
1742 |> json(%{error: "Can't display this activity"})
1748 def try_render(conn, _, _) do
1751 |> json(%{error: "Can't display this activity"})
1754 defp present?(nil), do: false
1755 defp present?(false), do: false
1756 defp present?(_), do: true