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.Formatter
15 alias Pleroma.Notification
17 alias Pleroma.Pagination
19 alias Pleroma.ScheduledActivity
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
44 alias Pleroma.Web.ControllerHelper
49 plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register)
50 plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
52 @local_mastodon_name "Mastodon-Local"
54 action_fallback(:errors)
56 def create_app(conn, params) do
57 scopes = Scopes.fetch_scopes(params, ["read"])
61 |> Map.drop(["scope", "scopes"])
62 |> Map.put("scopes", scopes)
64 with cs <- App.register_changeset(%App{}, app_attrs),
65 false <- cs.changes[:client_name] == @local_mastodon_name,
66 {:ok, app} <- Repo.insert(cs) do
69 |> render("show.json", %{app: app})
78 value_function \\ fn x -> {:ok, x} end
80 if Map.has_key?(params, params_field) do
81 case value_function.(params[params_field]) do
82 {:ok, new_value} -> Map.put(map, map_field, new_value)
90 def update_credentials(%{assigns: %{user: user}} = conn, params) do
95 |> add_if_present(params, "display_name", :name)
96 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
97 |> add_if_present(params, "avatar", :avatar, fn value ->
98 with %Plug.Upload{} <- value,
99 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
106 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
109 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 :skip_thread_containment
122 |> Enum.reduce(%{}, fn key, acc ->
123 add_if_present(acc, params, to_string(key), key, fn value ->
124 {:ok, ControllerHelper.truthy_param?(value)}
127 |> add_if_present(params, "default_scope", :default_scope)
128 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
129 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
131 |> add_if_present(params, "header", :banner, fn value ->
132 with %Plug.Upload{} <- value,
133 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
139 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
140 with %Plug.Upload{} <- value,
141 {:ok, object} <- ActivityPub.upload(value, type: :background) do
147 |> Map.put(:emoji, user_info_emojis)
149 info_cng = User.Info.profile_update(user.info, info_params)
151 with changeset <- User.update_changeset(user, user_params),
152 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
153 {:ok, user} <- User.update_and_set_cache(changeset) do
154 if original_user != user do
155 CommonAPI.update(user)
160 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
166 |> json(%{error: "Invalid request"})
170 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
171 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
174 AccountView.render("account.json", %{
177 with_pleroma_settings: true,
178 with_chat_token: chat_token
184 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
185 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
188 |> render("short.json", %{app: app})
192 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
193 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
194 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
195 account = AccountView.render("account.json", %{user: user, for: for_user})
201 |> json(%{error: "Can't find user"})
205 @mastodon_api_level "2.7.2"
207 def masto_instance(conn, _params) do
208 instance = Config.get(:instance)
212 title: Keyword.get(instance, :name),
213 description: Keyword.get(instance, :description),
214 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
215 email: Keyword.get(instance, :email),
217 streaming_api: Pleroma.Web.Endpoint.websocket_url()
219 stats: Stats.get_stats(),
220 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
222 registrations: Pleroma.Config.get([:instance, :registrations_open]),
223 # Extra (not present in Mastodon):
224 max_toot_chars: Keyword.get(instance, :limit),
225 poll_limits: Keyword.get(instance, :poll_limits)
231 def peers(conn, _params) do
232 json(conn, Stats.get_peers())
235 defp mastodonized_emoji do
236 Pleroma.Emoji.get_all()
237 |> Enum.map(fn {shortcode, relative_url, tags} ->
238 url = to_string(URI.merge(Web.base_url(), relative_url))
241 "shortcode" => shortcode,
243 "visible_in_picker" => true,
250 def custom_emojis(conn, _params) do
251 mastodon_emoji = mastodonized_emoji()
252 json(conn, mastodon_emoji)
255 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
258 |> Map.drop(["since_id", "max_id", "min_id"])
261 last = List.last(activities)
268 |> Map.get("limit", "20")
269 |> String.to_integer()
272 if length(activities) <= limit do
278 |> Enum.at(limit * -1)
282 {next_url, prev_url} =
286 Pleroma.Web.Endpoint,
289 Map.merge(params, %{max_id: max_id})
292 Pleroma.Web.Endpoint,
295 Map.merge(params, %{min_id: min_id})
301 Pleroma.Web.Endpoint,
303 Map.merge(params, %{max_id: max_id})
306 Pleroma.Web.Endpoint,
308 Map.merge(params, %{min_id: min_id})
314 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
320 def home_timeline(%{assigns: %{user: user}} = conn, params) do
323 |> Map.put("type", ["Create", "Announce"])
324 |> Map.put("blocking_user", user)
325 |> Map.put("muting_user", user)
326 |> Map.put("user", user)
329 [user.ap_id | user.following]
330 |> ActivityPub.fetch_activities(params)
334 |> add_link_headers(:home_timeline, activities)
335 |> put_view(StatusView)
336 |> render("index.json", %{activities: activities, for: user, as: :activity})
339 def public_timeline(%{assigns: %{user: user}} = conn, params) do
340 local_only = params["local"] in [true, "True", "true", "1"]
344 |> Map.put("type", ["Create", "Announce"])
345 |> Map.put("local_only", local_only)
346 |> Map.put("blocking_user", user)
347 |> Map.put("muting_user", user)
348 |> ActivityPub.fetch_public_activities()
352 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
353 |> put_view(StatusView)
354 |> render("index.json", %{activities: activities, for: user, as: :activity})
357 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
358 with %User{} = user <- User.get_cached_by_id(params["id"]) do
361 |> Map.put("tag", params["tagged"])
363 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
366 |> add_link_headers(:user_statuses, activities, params["id"])
367 |> put_view(StatusView)
368 |> render("index.json", %{
369 activities: activities,
376 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
379 |> Map.put("type", "Create")
380 |> Map.put("blocking_user", user)
381 |> Map.put("user", user)
382 |> Map.put(:visibility, "direct")
386 |> ActivityPub.fetch_activities_query(params)
387 |> Pagination.fetch_paginated(params)
390 |> add_link_headers(:dm_timeline, activities)
391 |> put_view(StatusView)
392 |> render("index.json", %{activities: activities, for: user, as: :activity})
395 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
396 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
397 true <- Visibility.visible_for_user?(activity, user) do
399 |> put_view(StatusView)
400 |> try_render("status.json", %{activity: activity, for: user})
404 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
405 with %Activity{} = activity <- Activity.get_by_id(id),
407 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
408 "blocking_user" => user,
412 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
414 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
415 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
421 activities: grouped_activities[true] || [],
425 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
430 activities: grouped_activities[false] || [],
434 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
441 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
442 with %Object{} = object <- Object.get_by_id(id),
443 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
444 true <- Visibility.visible_for_user?(activity, user) do
446 |> put_view(StatusView)
447 |> try_render("poll.json", %{object: object, for: user})
452 |> json(%{error: "Record not found"})
457 |> json(%{error: "Record not found"})
461 defp get_cached_vote_or_vote(user, object, choices) do
462 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
465 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
466 case CommonAPI.vote(user, object, choices) do
467 {:error, _message} = res -> {:ignore, res}
468 res -> {:commit, res}
475 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
476 with %Object{} = object <- Object.get_by_id(id),
477 true <- object.data["type"] == "Question",
478 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
479 true <- Visibility.visible_for_user?(activity, user),
480 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
482 |> put_view(StatusView)
483 |> try_render("poll.json", %{object: object, for: user})
488 |> json(%{error: "Record not found"})
493 |> json(%{error: "Record not found"})
498 |> json(%{error: message})
502 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
503 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
505 |> add_link_headers(:scheduled_statuses, scheduled_activities)
506 |> put_view(ScheduledActivityView)
507 |> render("index.json", %{scheduled_activities: scheduled_activities})
511 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
512 with %ScheduledActivity{} = scheduled_activity <-
513 ScheduledActivity.get(user, scheduled_activity_id) do
515 |> put_view(ScheduledActivityView)
516 |> render("show.json", %{scheduled_activity: scheduled_activity})
518 _ -> {:error, :not_found}
522 def update_scheduled_status(
523 %{assigns: %{user: user}} = conn,
524 %{"id" => scheduled_activity_id} = params
526 with %ScheduledActivity{} = scheduled_activity <-
527 ScheduledActivity.get(user, scheduled_activity_id),
528 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
530 |> put_view(ScheduledActivityView)
531 |> render("show.json", %{scheduled_activity: scheduled_activity})
533 nil -> {:error, :not_found}
538 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
539 with %ScheduledActivity{} = scheduled_activity <-
540 ScheduledActivity.get(user, scheduled_activity_id),
541 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
543 |> put_view(ScheduledActivityView)
544 |> render("show.json", %{scheduled_activity: scheduled_activity})
546 nil -> {:error, :not_found}
551 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
554 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
556 scheduled_at = params["scheduled_at"]
558 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
559 with {:ok, scheduled_activity} <-
560 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
562 |> put_view(ScheduledActivityView)
563 |> render("show.json", %{scheduled_activity: scheduled_activity})
566 params = Map.drop(params, ["scheduled_at"])
568 case CommonAPI.post(user, params) do
571 |> put_status(:unprocessable_entity)
572 |> json(%{error: message})
576 |> put_view(StatusView)
577 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
582 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
583 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
589 |> json(%{error: "Can't delete this post"})
593 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
594 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
595 %Activity{} = announce <- Activity.normalize(announce.data) do
597 |> put_view(StatusView)
598 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
602 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
603 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
604 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
606 |> put_view(StatusView)
607 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
611 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
612 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
613 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
615 |> put_view(StatusView)
616 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
620 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
621 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
622 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
624 |> put_view(StatusView)
625 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
629 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
630 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
632 |> put_view(StatusView)
633 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
637 |> put_resp_content_type("application/json")
638 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
642 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
643 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
645 |> put_view(StatusView)
646 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
650 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
651 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
652 %User{} = user <- User.get_cached_by_nickname(user.nickname),
653 true <- Visibility.visible_for_user?(activity, user),
654 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
656 |> put_view(StatusView)
657 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
661 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
662 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
663 %User{} = user <- User.get_cached_by_nickname(user.nickname),
664 true <- Visibility.visible_for_user?(activity, user),
665 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
667 |> put_view(StatusView)
668 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
672 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
673 activity = Activity.get_by_id(id)
675 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
677 |> put_view(StatusView)
678 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
682 |> put_resp_content_type("application/json")
683 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
687 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
688 activity = Activity.get_by_id(id)
690 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
692 |> put_view(StatusView)
693 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
697 def notifications(%{assigns: %{user: user}} = conn, params) do
698 notifications = MastodonAPI.get_notifications(user, params)
701 |> add_link_headers(:notifications, notifications)
702 |> put_view(NotificationView)
703 |> render("index.json", %{notifications: notifications, for: user})
706 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
707 with {:ok, notification} <- Notification.get(user, id) do
709 |> put_view(NotificationView)
710 |> render("show.json", %{notification: notification, for: user})
714 |> put_resp_content_type("application/json")
715 |> send_resp(403, Jason.encode!(%{"error" => reason}))
719 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
720 Notification.clear(user)
724 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
725 with {:ok, _notif} <- Notification.dismiss(user, id) do
730 |> put_resp_content_type("application/json")
731 |> send_resp(403, Jason.encode!(%{"error" => reason}))
735 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
736 Notification.destroy_multiple(user, ids)
740 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
742 q = from(u in User, where: u.id in ^id)
743 targets = Repo.all(q)
746 |> put_view(AccountView)
747 |> render("relationships.json", %{user: user, targets: targets})
750 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
751 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
753 def update_media(%{assigns: %{user: user}} = conn, data) do
754 with %Object{} = object <- Repo.get(Object, data["id"]),
755 true <- Object.authorize_mutation(object, user),
756 true <- is_binary(data["description"]),
757 description <- data["description"] do
758 new_data = %{object.data | "name" => description}
762 |> Object.change(%{data: new_data})
765 attachment_data = Map.put(new_data, "id", object.id)
768 |> put_view(StatusView)
769 |> render("attachment.json", %{attachment: attachment_data})
773 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
774 with {:ok, object} <-
777 actor: User.ap_id(user),
778 description: Map.get(data, "description")
780 attachment_data = Map.put(object.data, "id", object.id)
783 |> put_view(StatusView)
784 |> render("attachment.json", %{attachment: attachment_data})
788 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
789 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
790 %{} = attachment_data <- Map.put(object.data, "id", object.id),
791 %{type: type} = rendered <-
792 StatusView.render("attachment.json", %{attachment: attachment_data}) do
793 # Reject if not an image
794 if type == "image" do
796 # Save to the user's info
797 info_changeset = User.Info.mascot_update(user.info, rendered)
801 |> Ecto.Changeset.change()
802 |> Ecto.Changeset.put_embed(:info, info_changeset)
804 {:ok, _user} = User.update_and_set_cache(user_changeset)
810 |> put_resp_content_type("application/json")
811 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
816 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
817 mascot = User.get_mascot(user)
823 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
824 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
825 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
826 q = from(u in User, where: u.ap_id in ^likes)
830 |> put_view(AccountView)
831 |> render("accounts.json", %{for: user, users: users, as: :user})
837 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
838 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
839 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
840 q = from(u in User, where: u.ap_id in ^announces)
844 |> put_view(AccountView)
845 |> render("accounts.json", %{for: user, users: users, as: :user})
851 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
852 local_only = params["local"] in [true, "True", "true", "1"]
855 [params["tag"], params["any"]]
859 |> Enum.map(&String.downcase(&1))
864 |> Enum.map(&String.downcase(&1))
869 |> Enum.map(&String.downcase(&1))
873 |> Map.put("type", "Create")
874 |> Map.put("local_only", local_only)
875 |> Map.put("blocking_user", user)
876 |> Map.put("muting_user", user)
877 |> Map.put("tag", tags)
878 |> Map.put("tag_all", tag_all)
879 |> Map.put("tag_reject", tag_reject)
880 |> ActivityPub.fetch_public_activities()
884 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
885 |> put_view(StatusView)
886 |> render("index.json", %{activities: activities, for: user, as: :activity})
889 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
890 with %User{} = user <- User.get_cached_by_id(id),
891 followers <- MastodonAPI.get_followers(user, params) do
894 for_user && user.id == for_user.id -> followers
895 user.info.hide_followers -> []
900 |> add_link_headers(:followers, followers, user)
901 |> put_view(AccountView)
902 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
906 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
907 with %User{} = user <- User.get_cached_by_id(id),
908 followers <- MastodonAPI.get_friends(user, params) do
911 for_user && user.id == for_user.id -> followers
912 user.info.hide_follows -> []
917 |> add_link_headers(:following, followers, user)
918 |> put_view(AccountView)
919 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
923 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
924 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
926 |> put_view(AccountView)
927 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
931 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
932 with %User{} = follower <- User.get_cached_by_id(id),
933 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
935 |> put_view(AccountView)
936 |> render("relationship.json", %{user: followed, target: follower})
940 |> put_resp_content_type("application/json")
941 |> send_resp(403, Jason.encode!(%{"error" => message}))
945 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
946 with %User{} = follower <- User.get_cached_by_id(id),
947 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
949 |> put_view(AccountView)
950 |> render("relationship.json", %{user: followed, target: follower})
954 |> put_resp_content_type("application/json")
955 |> send_resp(403, Jason.encode!(%{"error" => message}))
959 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
960 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
961 {_, true} <- {:followed, follower.id != followed.id},
962 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
964 |> put_view(AccountView)
965 |> render("relationship.json", %{user: follower, target: followed})
972 |> put_resp_content_type("application/json")
973 |> send_resp(403, Jason.encode!(%{"error" => message}))
977 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
978 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
979 {_, true} <- {:followed, follower.id != followed.id},
980 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
982 |> put_view(AccountView)
983 |> render("account.json", %{user: followed, for: follower})
990 |> put_resp_content_type("application/json")
991 |> send_resp(403, Jason.encode!(%{"error" => message}))
995 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
996 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
997 {_, true} <- {:followed, follower.id != followed.id},
998 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1000 |> put_view(AccountView)
1001 |> render("relationship.json", %{user: follower, target: followed})
1004 {:error, :not_found}
1011 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1012 with %User{} = muted <- User.get_cached_by_id(id),
1013 {:ok, muter} <- User.mute(muter, muted) do
1015 |> put_view(AccountView)
1016 |> render("relationship.json", %{user: muter, target: muted})
1018 {:error, message} ->
1020 |> put_resp_content_type("application/json")
1021 |> send_resp(403, Jason.encode!(%{"error" => message}))
1025 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1026 with %User{} = muted <- User.get_cached_by_id(id),
1027 {:ok, muter} <- User.unmute(muter, muted) do
1029 |> put_view(AccountView)
1030 |> render("relationship.json", %{user: muter, target: muted})
1032 {:error, message} ->
1034 |> put_resp_content_type("application/json")
1035 |> send_resp(403, Jason.encode!(%{"error" => message}))
1039 def mutes(%{assigns: %{user: user}} = conn, _) do
1040 with muted_accounts <- User.muted_users(user) do
1041 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1046 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1047 with %User{} = blocked <- User.get_cached_by_id(id),
1048 {:ok, blocker} <- User.block(blocker, blocked),
1049 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1051 |> put_view(AccountView)
1052 |> render("relationship.json", %{user: blocker, target: blocked})
1054 {:error, message} ->
1056 |> put_resp_content_type("application/json")
1057 |> send_resp(403, Jason.encode!(%{"error" => message}))
1061 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1062 with %User{} = blocked <- User.get_cached_by_id(id),
1063 {:ok, blocker} <- User.unblock(blocker, blocked),
1064 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1066 |> put_view(AccountView)
1067 |> render("relationship.json", %{user: blocker, target: blocked})
1069 {:error, message} ->
1071 |> put_resp_content_type("application/json")
1072 |> send_resp(403, Jason.encode!(%{"error" => message}))
1076 def blocks(%{assigns: %{user: user}} = conn, _) do
1077 with blocked_accounts <- User.blocked_users(user) do
1078 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1083 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1084 json(conn, info.domain_blocks || [])
1087 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1088 User.block_domain(blocker, domain)
1092 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1093 User.unblock_domain(blocker, domain)
1097 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1098 with %User{} = subscription_target <- User.get_cached_by_id(id),
1099 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1101 |> put_view(AccountView)
1102 |> render("relationship.json", %{user: user, target: subscription_target})
1104 {:error, message} ->
1106 |> put_resp_content_type("application/json")
1107 |> send_resp(403, Jason.encode!(%{"error" => message}))
1111 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1112 with %User{} = subscription_target <- User.get_cached_by_id(id),
1113 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1115 |> put_view(AccountView)
1116 |> render("relationship.json", %{user: user, target: subscription_target})
1118 {:error, message} ->
1120 |> put_resp_content_type("application/json")
1121 |> send_resp(403, Jason.encode!(%{"error" => message}))
1125 def favourites(%{assigns: %{user: user}} = conn, params) do
1128 |> Map.put("type", "Create")
1129 |> Map.put("favorited_by", user.ap_id)
1130 |> Map.put("blocking_user", user)
1133 ActivityPub.fetch_activities([], params)
1137 |> add_link_headers(:favourites, activities)
1138 |> put_view(StatusView)
1139 |> render("index.json", %{activities: activities, for: user, as: :activity})
1142 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1143 with %User{} = user <- User.get_by_id(id),
1144 false <- user.info.hide_favorites do
1147 |> Map.put("type", "Create")
1148 |> Map.put("favorited_by", user.ap_id)
1149 |> Map.put("blocking_user", for_user)
1153 ["https://www.w3.org/ns/activitystreams#Public"] ++
1154 [for_user.ap_id | for_user.following]
1156 ["https://www.w3.org/ns/activitystreams#Public"]
1161 |> ActivityPub.fetch_activities(params)
1165 |> add_link_headers(:favourites, activities)
1166 |> put_view(StatusView)
1167 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1170 {:error, :not_found}
1175 |> json(%{error: "Can't get favorites"})
1179 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1180 user = User.get_cached_by_id(user.id)
1183 Bookmark.for_user_query(user.id)
1184 |> Pagination.fetch_paginated(params)
1188 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1191 |> add_link_headers(:bookmarks, bookmarks)
1192 |> put_view(StatusView)
1193 |> render("index.json", %{activities: activities, for: user, as: :activity})
1196 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1197 lists = Pleroma.List.for_user(user, opts)
1198 res = ListView.render("lists.json", lists: lists)
1202 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1203 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1204 res = ListView.render("list.json", list: list)
1210 |> json(%{error: "Record not found"})
1214 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1215 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1216 res = ListView.render("lists.json", lists: lists)
1220 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1221 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1222 {:ok, _list} <- Pleroma.List.delete(list) do
1230 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1231 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1232 res = ListView.render("list.json", list: list)
1237 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1239 |> Enum.each(fn account_id ->
1240 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1241 %User{} = followed <- User.get_cached_by_id(account_id) do
1242 Pleroma.List.follow(list, followed)
1249 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1251 |> Enum.each(fn account_id ->
1252 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1253 %User{} = followed <- User.get_cached_by_id(account_id) do
1254 Pleroma.List.unfollow(list, followed)
1261 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1262 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1263 {:ok, users} = Pleroma.List.get_following(list) do
1265 |> put_view(AccountView)
1266 |> render("accounts.json", %{for: user, users: users, as: :user})
1270 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1271 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1272 {:ok, list} <- Pleroma.List.rename(list, title) do
1273 res = ListView.render("list.json", list: list)
1281 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1282 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1285 |> Map.put("type", "Create")
1286 |> Map.put("blocking_user", user)
1287 |> Map.put("muting_user", user)
1289 # we must filter the following list for the user to avoid leaking statuses the user
1290 # does not actually have permission to see (for more info, peruse security issue #270).
1293 |> Enum.filter(fn x -> x in user.following end)
1294 |> ActivityPub.fetch_activities_bounded(following, params)
1298 |> put_view(StatusView)
1299 |> render("index.json", %{activities: activities, for: user, as: :activity})
1304 |> json(%{error: "Error."})
1308 def index(%{assigns: %{user: user}} = conn, _params) do
1309 token = get_session(conn, :oauth_token)
1312 mastodon_emoji = mastodonized_emoji()
1314 limit = Config.get([:instance, :limit])
1317 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1322 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1323 access_token: token,
1325 domain: Pleroma.Web.Endpoint.host(),
1328 unfollow_modal: false,
1331 auto_play_gif: false,
1332 display_sensitive_media: false,
1333 reduce_motion: false,
1334 max_toot_chars: limit,
1335 mascot: User.get_mascot(user)["url"]
1337 poll_limits: Config.get([:instance, :poll_limits]),
1339 delete_others_notice: present?(user.info.is_moderator),
1340 admin: present?(user.info.is_admin)
1344 default_privacy: user.info.default_scope,
1345 default_sensitive: false,
1346 allow_content_types: Config.get([:instance, :allowed_post_formats])
1348 media_attachments: %{
1349 accept_content_types: [
1365 user.info.settings ||
1395 push_subscription: nil,
1397 custom_emojis: mastodon_emoji,
1403 |> put_layout(false)
1404 |> put_view(MastodonView)
1405 |> render("index.html", %{initial_state: initial_state})
1408 |> put_session(:return_to, conn.request_path)
1409 |> redirect(to: "/web/login")
1413 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1414 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1416 with changeset <- Ecto.Changeset.change(user),
1417 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1418 {:ok, _user} <- User.update_and_set_cache(changeset) do
1423 |> put_resp_content_type("application/json")
1424 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1428 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1429 redirect(conn, to: local_mastodon_root_path(conn))
1432 @doc "Local Mastodon FE login init action"
1433 def login(conn, %{"code" => auth_token}) do
1434 with {:ok, app} <- get_or_make_app(),
1435 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1436 {:ok, token} <- Token.exchange_token(app, auth) do
1438 |> put_session(:oauth_token, token.token)
1439 |> redirect(to: local_mastodon_root_path(conn))
1443 @doc "Local Mastodon FE callback action"
1444 def login(conn, _) do
1445 with {:ok, app} <- get_or_make_app() do
1450 response_type: "code",
1451 client_id: app.client_id,
1453 scope: Enum.join(app.scopes, " ")
1456 redirect(conn, to: path)
1460 defp local_mastodon_root_path(conn) do
1461 case get_session(conn, :return_to) do
1463 mastodon_api_path(conn, :index, ["getting-started"])
1466 delete_session(conn, :return_to)
1471 defp get_or_make_app do
1472 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1473 scopes = ["read", "write", "follow", "push"]
1475 with %App{} = app <- Repo.get_by(App, find_attrs) do
1477 if app.scopes == scopes do
1481 |> Ecto.Changeset.change(%{scopes: scopes})
1489 App.register_changeset(
1491 Map.put(find_attrs, :scopes, scopes)
1498 def logout(conn, _) do
1501 |> redirect(to: "/")
1504 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1505 Logger.debug("Unimplemented, returning unmodified relationship")
1507 with %User{} = target <- User.get_cached_by_id(id) do
1509 |> put_view(AccountView)
1510 |> render("relationship.json", %{user: user, target: target})
1514 def empty_array(conn, _) do
1515 Logger.debug("Unimplemented, returning an empty array")
1519 def empty_object(conn, _) do
1520 Logger.debug("Unimplemented, returning an empty object")
1524 def get_filters(%{assigns: %{user: user}} = conn, _) do
1525 filters = Filter.get_filters(user)
1526 res = FilterView.render("filters.json", filters: filters)
1531 %{assigns: %{user: user}} = conn,
1532 %{"phrase" => phrase, "context" => context} = params
1538 hide: Map.get(params, "irreversible", false),
1539 whole_word: Map.get(params, "boolean", true)
1543 {:ok, response} = Filter.create(query)
1544 res = FilterView.render("filter.json", filter: response)
1548 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1549 filter = Filter.get(filter_id, user)
1550 res = FilterView.render("filter.json", filter: filter)
1555 %{assigns: %{user: user}} = conn,
1556 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1560 filter_id: filter_id,
1563 hide: Map.get(params, "irreversible", nil),
1564 whole_word: Map.get(params, "boolean", true)
1568 {:ok, response} = Filter.update(query)
1569 res = FilterView.render("filter.json", filter: response)
1573 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1576 filter_id: filter_id
1579 {:ok, _} = Filter.delete(query)
1585 def errors(conn, {:error, %Changeset{} = changeset}) do
1588 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1589 |> Enum.map_join(", ", fn {_k, v} -> v end)
1593 |> json(%{error: error_message})
1596 def errors(conn, {:error, :not_found}) do
1599 |> json(%{error: "Record not found"})
1602 def errors(conn, _) do
1605 |> json("Something went wrong")
1608 def suggestions(%{assigns: %{user: user}} = conn, _) do
1609 suggestions = Config.get(:suggestions)
1611 if Keyword.get(suggestions, :enabled, false) do
1612 api = Keyword.get(suggestions, :third_party_engine, "")
1613 timeout = Keyword.get(suggestions, :timeout, 5000)
1614 limit = Keyword.get(suggestions, :limit, 23)
1616 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1618 user = user.nickname
1622 |> String.replace("{{host}}", host)
1623 |> String.replace("{{user}}", user)
1625 with {:ok, %{status: 200, body: body}} <-
1630 recv_timeout: timeout,
1634 {:ok, data} <- Jason.decode(body) do
1637 |> Enum.slice(0, limit)
1642 case User.get_or_fetch(x["acct"]) do
1643 {:ok, %User{id: id}} -> id
1649 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1652 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1658 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1665 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1666 with %Activity{} = activity <- Activity.get_by_id(status_id),
1667 true <- Visibility.visible_for_user?(activity, user) do
1671 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1681 def reports(%{assigns: %{user: user}} = conn, params) do
1682 case CommonAPI.report(user, params) do
1685 |> put_view(ReportView)
1686 |> try_render("report.json", %{activity: activity})
1690 |> put_status(:bad_request)
1691 |> json(%{error: err})
1695 def account_register(
1696 %{assigns: %{app: app}} = conn,
1697 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1705 "captcha_answer_data",
1709 |> Map.put("nickname", nickname)
1710 |> Map.put("fullname", params["fullname"] || nickname)
1711 |> Map.put("bio", params["bio"] || "")
1712 |> Map.put("confirm", params["password"])
1714 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1715 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1717 token_type: "Bearer",
1718 access_token: token.token,
1720 created_at: Token.Utils.format_created_at(token)
1726 |> json(Jason.encode!(errors))
1730 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1733 |> json(%{error: "Missing parameters"})
1736 def account_register(conn, _) do
1739 |> json(%{error: "Invalid credentials"})
1742 def conversations(%{assigns: %{user: user}} = conn, params) do
1743 participations = Participation.for_user_with_last_activity_id(user, params)
1746 Enum.map(participations, fn participation ->
1747 ConversationView.render("participation.json", %{participation: participation, user: user})
1751 |> add_link_headers(:conversations, participations)
1752 |> json(conversations)
1755 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1756 with %Participation{} = participation <-
1757 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1758 {:ok, participation} <- Participation.mark_as_read(participation) do
1759 participation_view =
1760 ConversationView.render("participation.json", %{participation: participation, user: user})
1763 |> json(participation_view)
1767 def try_render(conn, target, params)
1768 when is_binary(target) do
1769 res = render(conn, target, params)
1774 |> json(%{error: "Can't display this activity"})
1780 def try_render(conn, _, _) do
1783 |> json(%{error: "Can't display this activity"})
1786 defp present?(nil), do: false
1787 defp present?(false), do: false
1788 defp present?(_), do: true