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 |> Map.put(:emoji, user_info_emojis)
141 info_cng = User.Info.profile_update(user.info, info_params)
143 with changeset <- User.update_changeset(user, user_params),
144 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
145 {:ok, user} <- User.update_and_set_cache(changeset) do
146 if original_user != user do
147 CommonAPI.update(user)
152 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
158 |> json(%{error: "Invalid request"})
162 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
164 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
169 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
170 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
173 |> render("short.json", %{app: app})
177 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
178 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
179 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
180 account = AccountView.render("account.json", %{user: user, for: for_user})
186 |> json(%{error: "Can't find user"})
190 @mastodon_api_level "2.7.2"
192 def masto_instance(conn, _params) do
193 instance = Config.get(:instance)
197 title: Keyword.get(instance, :name),
198 description: Keyword.get(instance, :description),
199 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
200 email: Keyword.get(instance, :email),
202 streaming_api: Pleroma.Web.Endpoint.websocket_url()
204 stats: Stats.get_stats(),
205 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
207 registrations: Pleroma.Config.get([:instance, :registrations_open]),
208 # Extra (not present in Mastodon):
209 max_toot_chars: Keyword.get(instance, :limit),
210 poll_limits: Keyword.get(instance, :poll_limits)
216 def peers(conn, _params) do
217 json(conn, Stats.get_peers())
220 defp mastodonized_emoji do
221 Pleroma.Emoji.get_all()
222 |> Enum.map(fn {shortcode, relative_url, tags} ->
223 url = to_string(URI.merge(Web.base_url(), relative_url))
226 "shortcode" => shortcode,
228 "visible_in_picker" => true,
235 def custom_emojis(conn, _params) do
236 mastodon_emoji = mastodonized_emoji()
237 json(conn, mastodon_emoji)
240 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
243 |> Map.drop(["since_id", "max_id", "min_id"])
246 last = List.last(activities)
253 |> Map.get("limit", "20")
254 |> String.to_integer()
257 if length(activities) <= limit do
263 |> Enum.at(limit * -1)
267 {next_url, prev_url} =
271 Pleroma.Web.Endpoint,
274 Map.merge(params, %{max_id: max_id})
277 Pleroma.Web.Endpoint,
280 Map.merge(params, %{min_id: min_id})
286 Pleroma.Web.Endpoint,
288 Map.merge(params, %{max_id: max_id})
291 Pleroma.Web.Endpoint,
293 Map.merge(params, %{min_id: min_id})
299 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
305 def home_timeline(%{assigns: %{user: user}} = conn, params) do
308 |> Map.put("type", ["Create", "Announce"])
309 |> Map.put("blocking_user", user)
310 |> Map.put("muting_user", user)
311 |> Map.put("user", user)
314 [user.ap_id | user.following]
315 |> ActivityPub.fetch_activities(params)
319 |> add_link_headers(:home_timeline, activities)
320 |> put_view(StatusView)
321 |> render("index.json", %{activities: activities, for: user, as: :activity})
324 def public_timeline(%{assigns: %{user: user}} = conn, params) do
325 local_only = params["local"] in [true, "True", "true", "1"]
329 |> Map.put("type", ["Create", "Announce"])
330 |> Map.put("local_only", local_only)
331 |> Map.put("blocking_user", user)
332 |> Map.put("muting_user", user)
333 |> ActivityPub.fetch_public_activities()
337 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
338 |> put_view(StatusView)
339 |> render("index.json", %{activities: activities, for: user, as: :activity})
342 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
343 with %User{} = user <- User.get_cached_by_id(params["id"]) do
344 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
347 |> add_link_headers(:user_statuses, activities, params["id"])
348 |> put_view(StatusView)
349 |> render("index.json", %{
350 activities: activities,
357 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
360 |> Map.put("type", "Create")
361 |> Map.put("blocking_user", user)
362 |> Map.put("user", user)
363 |> Map.put(:visibility, "direct")
367 |> ActivityPub.fetch_activities_query(params)
368 |> Pagination.fetch_paginated(params)
371 |> add_link_headers(:dm_timeline, activities)
372 |> put_view(StatusView)
373 |> render("index.json", %{activities: activities, for: user, as: :activity})
376 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
378 true <- Visibility.visible_for_user?(activity, user) do
380 |> put_view(StatusView)
381 |> try_render("status.json", %{activity: activity, for: user})
385 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
386 with %Activity{} = activity <- Activity.get_by_id(id),
388 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
389 "blocking_user" => user,
393 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
395 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
396 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
402 activities: grouped_activities[true] || [],
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
411 activities: grouped_activities[false] || [],
415 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
422 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
423 with %Object{} = object <- Object.get_by_id(id),
424 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
425 true <- Visibility.visible_for_user?(activity, user) do
427 |> put_view(StatusView)
428 |> try_render("poll.json", %{object: object, for: user})
433 |> json(%{error: "Record not found"})
438 |> json(%{error: "Record not found"})
442 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
443 with %Object{} = object <- Object.get_by_id(id),
444 true <- object.data["type"] == "Question",
445 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
446 true <- Visibility.visible_for_user?(activity, user),
447 {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
449 |> put_view(StatusView)
450 |> try_render("poll.json", %{object: object, for: user})
455 |> json(%{error: "Record not found"})
460 |> json(%{error: "Record not found"})
465 |> json(%{error: message})
469 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
470 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
472 |> add_link_headers(:scheduled_statuses, scheduled_activities)
473 |> put_view(ScheduledActivityView)
474 |> render("index.json", %{scheduled_activities: scheduled_activities})
478 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
479 with %ScheduledActivity{} = scheduled_activity <-
480 ScheduledActivity.get(user, scheduled_activity_id) do
482 |> put_view(ScheduledActivityView)
483 |> render("show.json", %{scheduled_activity: scheduled_activity})
485 _ -> {:error, :not_found}
489 def update_scheduled_status(
490 %{assigns: %{user: user}} = conn,
491 %{"id" => scheduled_activity_id} = params
493 with %ScheduledActivity{} = scheduled_activity <-
494 ScheduledActivity.get(user, scheduled_activity_id),
495 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
497 |> put_view(ScheduledActivityView)
498 |> render("show.json", %{scheduled_activity: scheduled_activity})
500 nil -> {:error, :not_found}
505 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
506 with %ScheduledActivity{} = scheduled_activity <-
507 ScheduledActivity.get(user, scheduled_activity_id),
508 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
510 |> put_view(ScheduledActivityView)
511 |> render("show.json", %{scheduled_activity: scheduled_activity})
513 nil -> {:error, :not_found}
518 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
519 when length(media_ids) > 0 do
522 |> Map.put("status", ".")
524 post_status(conn, params)
527 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
530 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
532 scheduled_at = params["scheduled_at"]
534 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
535 with {:ok, scheduled_activity} <-
536 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
538 |> put_view(ScheduledActivityView)
539 |> render("show.json", %{scheduled_activity: scheduled_activity})
542 params = Map.drop(params, ["scheduled_at"])
544 case get_cached_status_or_post(conn, params) do
545 {:ignore, message} ->
548 |> json(%{error: message})
553 |> json(%{error: message})
557 |> put_view(StatusView)
558 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
563 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
565 case get_req_header(conn, "idempotency-key") do
567 _ -> Ecto.UUID.generate()
570 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
571 case CommonAPI.post(user, params) do
572 {:ok, activity} -> activity
573 {:error, message} -> {:ignore, message}
578 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
579 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
585 |> json(%{error: "Can't delete this post"})
589 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
590 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
591 %Activity{} = announce <- Activity.normalize(announce.data) do
593 |> put_view(StatusView)
594 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
598 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
599 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
600 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
602 |> put_view(StatusView)
603 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
607 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
608 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
609 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
611 |> put_view(StatusView)
612 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
616 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
617 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
618 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
620 |> put_view(StatusView)
621 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
625 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
626 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
628 |> put_view(StatusView)
629 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
633 |> put_resp_content_type("application/json")
634 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
638 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
639 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
641 |> put_view(StatusView)
642 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
646 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
647 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
648 %User{} = user <- User.get_cached_by_nickname(user.nickname),
649 true <- Visibility.visible_for_user?(activity, user),
650 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
658 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
659 %User{} = user <- User.get_cached_by_nickname(user.nickname),
660 true <- Visibility.visible_for_user?(activity, user),
661 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
663 |> put_view(StatusView)
664 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
668 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
669 activity = Activity.get_by_id(id)
671 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
673 |> put_view(StatusView)
674 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
678 |> put_resp_content_type("application/json")
679 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
683 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
684 activity = Activity.get_by_id(id)
686 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
688 |> put_view(StatusView)
689 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
693 def notifications(%{assigns: %{user: user}} = conn, params) do
694 notifications = MastodonAPI.get_notifications(user, params)
697 |> add_link_headers(:notifications, notifications)
698 |> put_view(NotificationView)
699 |> render("index.json", %{notifications: notifications, for: user})
702 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
703 with {:ok, notification} <- Notification.get(user, id) do
705 |> put_view(NotificationView)
706 |> render("show.json", %{notification: notification, for: user})
710 |> put_resp_content_type("application/json")
711 |> send_resp(403, Jason.encode!(%{"error" => reason}))
715 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
716 Notification.clear(user)
720 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
721 with {:ok, _notif} <- Notification.dismiss(user, id) do
726 |> put_resp_content_type("application/json")
727 |> send_resp(403, Jason.encode!(%{"error" => reason}))
731 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
732 Notification.destroy_multiple(user, ids)
736 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
738 q = from(u in User, where: u.id in ^id)
739 targets = Repo.all(q)
742 |> put_view(AccountView)
743 |> render("relationships.json", %{user: user, targets: targets})
746 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
747 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
749 def update_media(%{assigns: %{user: user}} = conn, data) do
750 with %Object{} = object <- Repo.get(Object, data["id"]),
751 true <- Object.authorize_mutation(object, user),
752 true <- is_binary(data["description"]),
753 description <- data["description"] do
754 new_data = %{object.data | "name" => description}
758 |> Object.change(%{data: new_data})
761 attachment_data = Map.put(new_data, "id", object.id)
764 |> put_view(StatusView)
765 |> render("attachment.json", %{attachment: attachment_data})
769 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
770 with {:ok, object} <-
773 actor: User.ap_id(user),
774 description: Map.get(data, "description")
776 attachment_data = Map.put(object.data, "id", object.id)
779 |> put_view(StatusView)
780 |> render("attachment.json", %{attachment: attachment_data})
784 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
785 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
786 %{} = attachment_data <- Map.put(object.data, "id", object.id),
787 %{type: type} = rendered <-
788 StatusView.render("attachment.json", %{attachment: attachment_data}) do
789 # Reject if not an image
790 if type == "image" do
792 # Save to the user's info
793 info_changeset = User.Info.mascot_update(user.info, rendered)
797 |> Ecto.Changeset.change()
798 |> Ecto.Changeset.put_embed(:info, info_changeset)
800 {:ok, _user} = User.update_and_set_cache(user_changeset)
806 |> put_resp_content_type("application/json")
807 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
812 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
813 mascot = User.get_mascot(user)
819 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
820 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
821 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
822 q = from(u in User, where: u.ap_id in ^likes)
826 |> put_view(AccountView)
827 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
833 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
834 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
835 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
836 q = from(u in User, where: u.ap_id in ^announces)
840 |> put_view(AccountView)
841 |> render("accounts.json", %{for: user, users: users, as: :user})
847 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
848 local_only = params["local"] in [true, "True", "true", "1"]
851 [params["tag"], params["any"]]
855 |> Enum.map(&String.downcase(&1))
860 |> Enum.map(&String.downcase(&1))
865 |> Enum.map(&String.downcase(&1))
869 |> Map.put("type", "Create")
870 |> Map.put("local_only", local_only)
871 |> Map.put("blocking_user", user)
872 |> Map.put("muting_user", user)
873 |> Map.put("tag", tags)
874 |> Map.put("tag_all", tag_all)
875 |> Map.put("tag_reject", tag_reject)
876 |> ActivityPub.fetch_public_activities()
880 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
881 |> put_view(StatusView)
882 |> render("index.json", %{activities: activities, for: user, as: :activity})
885 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
886 with %User{} = user <- User.get_cached_by_id(id),
887 followers <- MastodonAPI.get_followers(user, params) do
890 for_user && user.id == for_user.id -> followers
891 user.info.hide_followers -> []
896 |> add_link_headers(:followers, followers, user)
897 |> put_view(AccountView)
898 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
902 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
903 with %User{} = user <- User.get_cached_by_id(id),
904 followers <- MastodonAPI.get_friends(user, params) do
907 for_user && user.id == for_user.id -> followers
908 user.info.hide_follows -> []
913 |> add_link_headers(:following, followers, user)
914 |> put_view(AccountView)
915 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
919 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
920 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
922 |> put_view(AccountView)
923 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
927 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
928 with %User{} = follower <- User.get_cached_by_id(id),
929 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
931 |> put_view(AccountView)
932 |> render("relationship.json", %{user: followed, target: follower})
936 |> put_resp_content_type("application/json")
937 |> send_resp(403, Jason.encode!(%{"error" => message}))
941 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
942 with %User{} = follower <- User.get_cached_by_id(id),
943 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
945 |> put_view(AccountView)
946 |> render("relationship.json", %{user: followed, target: follower})
950 |> put_resp_content_type("application/json")
951 |> send_resp(403, Jason.encode!(%{"error" => message}))
955 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
956 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
957 {_, true} <- {:followed, follower.id != followed.id},
958 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
960 |> put_view(AccountView)
961 |> render("relationship.json", %{user: follower, target: followed})
968 |> put_resp_content_type("application/json")
969 |> send_resp(403, Jason.encode!(%{"error" => message}))
973 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
974 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
975 {_, true} <- {:followed, follower.id != followed.id},
976 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
978 |> put_view(AccountView)
979 |> render("account.json", %{user: followed, for: follower})
986 |> put_resp_content_type("application/json")
987 |> send_resp(403, Jason.encode!(%{"error" => message}))
991 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
992 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
993 {_, true} <- {:followed, follower.id != followed.id},
994 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
996 |> put_view(AccountView)
997 |> render("relationship.json", %{user: follower, target: followed})
1000 {:error, :not_found}
1007 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1008 with %User{} = muted <- User.get_cached_by_id(id),
1009 {:ok, muter} <- User.mute(muter, muted) do
1011 |> put_view(AccountView)
1012 |> render("relationship.json", %{user: muter, target: muted})
1014 {:error, message} ->
1016 |> put_resp_content_type("application/json")
1017 |> send_resp(403, Jason.encode!(%{"error" => message}))
1021 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1022 with %User{} = muted <- User.get_cached_by_id(id),
1023 {:ok, muter} <- User.unmute(muter, muted) do
1025 |> put_view(AccountView)
1026 |> render("relationship.json", %{user: muter, target: muted})
1028 {:error, message} ->
1030 |> put_resp_content_type("application/json")
1031 |> send_resp(403, Jason.encode!(%{"error" => message}))
1035 def mutes(%{assigns: %{user: user}} = conn, _) do
1036 with muted_accounts <- User.muted_users(user) do
1037 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1042 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1043 with %User{} = blocked <- User.get_cached_by_id(id),
1044 {:ok, blocker} <- User.block(blocker, blocked),
1045 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1047 |> put_view(AccountView)
1048 |> render("relationship.json", %{user: blocker, target: blocked})
1050 {:error, message} ->
1052 |> put_resp_content_type("application/json")
1053 |> send_resp(403, Jason.encode!(%{"error" => message}))
1057 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1058 with %User{} = blocked <- User.get_cached_by_id(id),
1059 {:ok, blocker} <- User.unblock(blocker, blocked),
1060 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1062 |> put_view(AccountView)
1063 |> render("relationship.json", %{user: blocker, target: blocked})
1065 {:error, message} ->
1067 |> put_resp_content_type("application/json")
1068 |> send_resp(403, Jason.encode!(%{"error" => message}))
1072 def blocks(%{assigns: %{user: user}} = conn, _) do
1073 with blocked_accounts <- User.blocked_users(user) do
1074 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1079 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1080 json(conn, info.domain_blocks || [])
1083 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1084 User.block_domain(blocker, domain)
1088 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1089 User.unblock_domain(blocker, domain)
1093 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1094 with %User{} = subscription_target <- User.get_cached_by_id(id),
1095 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1097 |> put_view(AccountView)
1098 |> render("relationship.json", %{user: user, target: subscription_target})
1100 {:error, message} ->
1102 |> put_resp_content_type("application/json")
1103 |> send_resp(403, Jason.encode!(%{"error" => message}))
1107 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1108 with %User{} = subscription_target <- User.get_cached_by_id(id),
1109 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1111 |> put_view(AccountView)
1112 |> render("relationship.json", %{user: user, target: subscription_target})
1114 {:error, message} ->
1116 |> put_resp_content_type("application/json")
1117 |> send_resp(403, Jason.encode!(%{"error" => message}))
1121 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1122 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1123 statuses = Activity.search(user, query)
1124 tags_path = Web.base_url() <> "/tag/"
1130 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1131 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1132 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1135 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1137 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1144 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1145 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1146 statuses = Activity.search(user, query)
1152 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1153 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1156 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1158 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1165 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1166 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1168 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1173 def favourites(%{assigns: %{user: user}} = conn, params) do
1176 |> Map.put("type", "Create")
1177 |> Map.put("favorited_by", user.ap_id)
1178 |> Map.put("blocking_user", user)
1181 ActivityPub.fetch_activities([], params)
1185 |> add_link_headers(:favourites, activities)
1186 |> put_view(StatusView)
1187 |> render("index.json", %{activities: activities, for: user, as: :activity})
1190 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1191 with %User{} = user <- User.get_by_id(id),
1192 false <- user.info.hide_favorites do
1195 |> Map.put("type", "Create")
1196 |> Map.put("favorited_by", user.ap_id)
1197 |> Map.put("blocking_user", for_user)
1201 ["https://www.w3.org/ns/activitystreams#Public"] ++
1202 [for_user.ap_id | for_user.following]
1204 ["https://www.w3.org/ns/activitystreams#Public"]
1209 |> ActivityPub.fetch_activities(params)
1213 |> add_link_headers(:favourites, activities)
1214 |> put_view(StatusView)
1215 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1218 {:error, :not_found}
1223 |> json(%{error: "Can't get favorites"})
1227 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1228 user = User.get_cached_by_id(user.id)
1231 Bookmark.for_user_query(user.id)
1232 |> Pagination.fetch_paginated(params)
1236 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1239 |> add_link_headers(:bookmarks, bookmarks)
1240 |> put_view(StatusView)
1241 |> render("index.json", %{activities: activities, for: user, as: :activity})
1244 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1245 lists = Pleroma.List.for_user(user, opts)
1246 res = ListView.render("lists.json", lists: lists)
1250 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1251 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1252 res = ListView.render("list.json", list: list)
1258 |> json(%{error: "Record not found"})
1262 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1263 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1264 res = ListView.render("lists.json", lists: lists)
1268 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1269 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1270 {:ok, _list} <- Pleroma.List.delete(list) do
1278 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1279 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1280 res = ListView.render("list.json", list: list)
1285 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1287 |> Enum.each(fn account_id ->
1288 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1289 %User{} = followed <- User.get_cached_by_id(account_id) do
1290 Pleroma.List.follow(list, followed)
1297 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1299 |> Enum.each(fn account_id ->
1300 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1301 %User{} = followed <- User.get_cached_by_id(account_id) do
1302 Pleroma.List.unfollow(list, followed)
1309 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1310 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1311 {:ok, users} = Pleroma.List.get_following(list) do
1313 |> put_view(AccountView)
1314 |> render("accounts.json", %{for: user, users: users, as: :user})
1318 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1319 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1320 {:ok, list} <- Pleroma.List.rename(list, title) do
1321 res = ListView.render("list.json", list: list)
1329 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1330 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1333 |> Map.put("type", "Create")
1334 |> Map.put("blocking_user", user)
1335 |> Map.put("muting_user", user)
1337 # we must filter the following list for the user to avoid leaking statuses the user
1338 # does not actually have permission to see (for more info, peruse security issue #270).
1341 |> Enum.filter(fn x -> x in user.following end)
1342 |> ActivityPub.fetch_activities_bounded(following, params)
1346 |> put_view(StatusView)
1347 |> render("index.json", %{activities: activities, for: user, as: :activity})
1352 |> json(%{error: "Error."})
1356 def index(%{assigns: %{user: user}} = conn, _params) do
1357 token = get_session(conn, :oauth_token)
1360 mastodon_emoji = mastodonized_emoji()
1362 limit = Config.get([:instance, :limit])
1365 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1370 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1371 access_token: token,
1373 domain: Pleroma.Web.Endpoint.host(),
1376 unfollow_modal: false,
1379 auto_play_gif: false,
1380 display_sensitive_media: false,
1381 reduce_motion: false,
1382 max_toot_chars: limit,
1383 mascot: User.get_mascot(user)["url"]
1385 poll_limits: Config.get([:instance, :poll_limits]),
1387 delete_others_notice: present?(user.info.is_moderator),
1388 admin: present?(user.info.is_admin)
1392 default_privacy: user.info.default_scope,
1393 default_sensitive: false,
1394 allow_content_types: Config.get([:instance, :allowed_post_formats])
1396 media_attachments: %{
1397 accept_content_types: [
1413 user.info.settings ||
1443 push_subscription: nil,
1445 custom_emojis: mastodon_emoji,
1451 |> put_layout(false)
1452 |> put_view(MastodonView)
1453 |> render("index.html", %{initial_state: initial_state})
1456 |> put_session(:return_to, conn.request_path)
1457 |> redirect(to: "/web/login")
1461 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1462 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1464 with changeset <- Ecto.Changeset.change(user),
1465 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1466 {:ok, _user} <- User.update_and_set_cache(changeset) do
1471 |> put_resp_content_type("application/json")
1472 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1476 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1477 redirect(conn, to: local_mastodon_root_path(conn))
1480 @doc "Local Mastodon FE login init action"
1481 def login(conn, %{"code" => auth_token}) do
1482 with {:ok, app} <- get_or_make_app(),
1483 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1484 {:ok, token} <- Token.exchange_token(app, auth) do
1486 |> put_session(:oauth_token, token.token)
1487 |> redirect(to: local_mastodon_root_path(conn))
1491 @doc "Local Mastodon FE callback action"
1492 def login(conn, _) do
1493 with {:ok, app} <- get_or_make_app() do
1498 response_type: "code",
1499 client_id: app.client_id,
1501 scope: Enum.join(app.scopes, " ")
1504 redirect(conn, to: path)
1508 defp local_mastodon_root_path(conn) do
1509 case get_session(conn, :return_to) do
1511 mastodon_api_path(conn, :index, ["getting-started"])
1514 delete_session(conn, :return_to)
1519 defp get_or_make_app do
1520 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1521 scopes = ["read", "write", "follow", "push"]
1523 with %App{} = app <- Repo.get_by(App, find_attrs) do
1525 if app.scopes == scopes do
1529 |> Ecto.Changeset.change(%{scopes: scopes})
1537 App.register_changeset(
1539 Map.put(find_attrs, :scopes, scopes)
1546 def logout(conn, _) do
1549 |> redirect(to: "/")
1552 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1553 Logger.debug("Unimplemented, returning unmodified relationship")
1555 with %User{} = target <- User.get_cached_by_id(id) do
1557 |> put_view(AccountView)
1558 |> render("relationship.json", %{user: user, target: target})
1562 def empty_array(conn, _) do
1563 Logger.debug("Unimplemented, returning an empty array")
1567 def empty_object(conn, _) do
1568 Logger.debug("Unimplemented, returning an empty object")
1572 def get_filters(%{assigns: %{user: user}} = conn, _) do
1573 filters = Filter.get_filters(user)
1574 res = FilterView.render("filters.json", filters: filters)
1579 %{assigns: %{user: user}} = conn,
1580 %{"phrase" => phrase, "context" => context} = params
1586 hide: Map.get(params, "irreversible", false),
1587 whole_word: Map.get(params, "boolean", true)
1591 {:ok, response} = Filter.create(query)
1592 res = FilterView.render("filter.json", filter: response)
1596 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1597 filter = Filter.get(filter_id, user)
1598 res = FilterView.render("filter.json", filter: filter)
1603 %{assigns: %{user: user}} = conn,
1604 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1608 filter_id: filter_id,
1611 hide: Map.get(params, "irreversible", nil),
1612 whole_word: Map.get(params, "boolean", true)
1616 {:ok, response} = Filter.update(query)
1617 res = FilterView.render("filter.json", filter: response)
1621 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1624 filter_id: filter_id
1627 {:ok, _} = Filter.delete(query)
1633 def errors(conn, {:error, %Changeset{} = changeset}) do
1636 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1637 |> Enum.map_join(", ", fn {_k, v} -> v end)
1641 |> json(%{error: error_message})
1644 def errors(conn, {:error, :not_found}) do
1647 |> json(%{error: "Record not found"})
1650 def errors(conn, _) do
1653 |> json("Something went wrong")
1656 def suggestions(%{assigns: %{user: user}} = conn, _) do
1657 suggestions = Config.get(:suggestions)
1659 if Keyword.get(suggestions, :enabled, false) do
1660 api = Keyword.get(suggestions, :third_party_engine, "")
1661 timeout = Keyword.get(suggestions, :timeout, 5000)
1662 limit = Keyword.get(suggestions, :limit, 23)
1664 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1666 user = user.nickname
1670 |> String.replace("{{host}}", host)
1671 |> String.replace("{{user}}", user)
1673 with {:ok, %{status: 200, body: body}} <-
1678 recv_timeout: timeout,
1682 {:ok, data} <- Jason.decode(body) do
1685 |> Enum.slice(0, limit)
1690 case User.get_or_fetch(x["acct"]) do
1691 {:ok, %User{id: id}} -> id
1697 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1700 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1706 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1713 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1714 with %Activity{} = activity <- Activity.get_by_id(status_id),
1715 true <- Visibility.visible_for_user?(activity, user) do
1719 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1729 def reports(%{assigns: %{user: user}} = conn, params) do
1730 case CommonAPI.report(user, params) do
1733 |> put_view(ReportView)
1734 |> try_render("report.json", %{activity: activity})
1738 |> put_status(:bad_request)
1739 |> json(%{error: err})
1743 def account_register(
1744 %{assigns: %{app: app}} = conn,
1745 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1753 "captcha_answer_data",
1757 |> Map.put("nickname", nickname)
1758 |> Map.put("fullname", params["fullname"] || nickname)
1759 |> Map.put("bio", params["bio"] || "")
1760 |> Map.put("confirm", params["password"])
1762 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1763 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1765 token_type: "Bearer",
1766 access_token: token.token,
1768 created_at: Token.Utils.format_created_at(token)
1774 |> json(Jason.encode!(errors))
1778 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1781 |> json(%{error: "Missing parameters"})
1784 def account_register(conn, _) do
1787 |> json(%{error: "Invalid credentials"})
1790 def conversations(%{assigns: %{user: user}} = conn, params) do
1791 participations = Participation.for_user_with_last_activity_id(user, params)
1794 Enum.map(participations, fn participation ->
1795 ConversationView.render("participation.json", %{participation: participation, user: user})
1799 |> add_link_headers(:conversations, participations)
1800 |> json(conversations)
1803 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1804 with %Participation{} = participation <-
1805 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1806 {:ok, participation} <- Participation.mark_as_read(participation) do
1807 participation_view =
1808 ConversationView.render("participation.json", %{participation: participation, user: user})
1811 |> json(participation_view)
1815 def try_render(conn, target, params)
1816 when is_binary(target) do
1817 res = render(conn, target, params)
1822 |> json(%{error: "Can't display this activity"})
1828 def try_render(conn, _, _) do
1831 |> json(%{error: "Can't display this activity"})
1834 defp present?(nil), do: false
1835 defp present?(false), do: false
1836 defp present?(_), do: true