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
359 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
362 |> add_link_headers(:user_statuses, activities, params["id"])
363 |> put_view(StatusView)
364 |> render("index.json", %{
365 activities: activities,
372 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
375 |> Map.put("type", "Create")
376 |> Map.put("blocking_user", user)
377 |> Map.put("user", user)
378 |> Map.put(:visibility, "direct")
382 |> ActivityPub.fetch_activities_query(params)
383 |> Pagination.fetch_paginated(params)
386 |> add_link_headers(:dm_timeline, activities)
387 |> put_view(StatusView)
388 |> render("index.json", %{activities: activities, for: user, as: :activity})
391 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
392 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
393 true <- Visibility.visible_for_user?(activity, user) do
395 |> put_view(StatusView)
396 |> try_render("status.json", %{activity: activity, for: user})
400 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
401 with %Activity{} = activity <- Activity.get_by_id(id),
403 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
404 "blocking_user" => user,
408 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
410 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
411 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
417 activities: grouped_activities[true] || [],
421 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
426 activities: grouped_activities[false] || [],
430 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
437 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
438 with %Object{} = object <- Object.get_by_id(id),
439 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
440 true <- Visibility.visible_for_user?(activity, user) do
442 |> put_view(StatusView)
443 |> try_render("poll.json", %{object: object, for: user})
448 |> json(%{error: "Record not found"})
453 |> json(%{error: "Record not found"})
457 defp get_cached_vote_or_vote(user, object, choices) do
458 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
461 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
462 case CommonAPI.vote(user, object, choices) do
463 {:error, _message} = res -> {:ignore, res}
464 res -> {:commit, res}
471 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
472 with %Object{} = object <- Object.get_by_id(id),
473 true <- object.data["type"] == "Question",
474 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
475 true <- Visibility.visible_for_user?(activity, user),
476 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
478 |> put_view(StatusView)
479 |> try_render("poll.json", %{object: object, for: user})
484 |> json(%{error: "Record not found"})
489 |> json(%{error: "Record not found"})
494 |> json(%{error: message})
498 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
499 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
501 |> add_link_headers(:scheduled_statuses, scheduled_activities)
502 |> put_view(ScheduledActivityView)
503 |> render("index.json", %{scheduled_activities: scheduled_activities})
507 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
508 with %ScheduledActivity{} = scheduled_activity <-
509 ScheduledActivity.get(user, scheduled_activity_id) do
511 |> put_view(ScheduledActivityView)
512 |> render("show.json", %{scheduled_activity: scheduled_activity})
514 _ -> {:error, :not_found}
518 def update_scheduled_status(
519 %{assigns: %{user: user}} = conn,
520 %{"id" => scheduled_activity_id} = params
522 with %ScheduledActivity{} = scheduled_activity <-
523 ScheduledActivity.get(user, scheduled_activity_id),
524 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
526 |> put_view(ScheduledActivityView)
527 |> render("show.json", %{scheduled_activity: scheduled_activity})
529 nil -> {:error, :not_found}
534 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
535 with %ScheduledActivity{} = scheduled_activity <-
536 ScheduledActivity.get(user, scheduled_activity_id),
537 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
539 |> put_view(ScheduledActivityView)
540 |> render("show.json", %{scheduled_activity: scheduled_activity})
542 nil -> {:error, :not_found}
547 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
550 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
552 scheduled_at = params["scheduled_at"]
554 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
555 with {:ok, scheduled_activity} <-
556 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
558 |> put_view(ScheduledActivityView)
559 |> render("show.json", %{scheduled_activity: scheduled_activity})
562 params = Map.drop(params, ["scheduled_at"])
564 case get_cached_status_or_post(conn, params) do
565 {:ignore, message} ->
568 |> json(%{error: message})
573 |> json(%{error: message})
577 |> put_view(StatusView)
578 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
583 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
585 case get_req_header(conn, "idempotency-key") do
587 _ -> Ecto.UUID.generate()
590 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
591 case CommonAPI.post(user, params) do
592 {:ok, activity} -> activity
593 {:error, message} -> {:ignore, message}
598 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
599 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
605 |> json(%{error: "Can't delete this post"})
609 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
610 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
611 %Activity{} = announce <- Activity.normalize(announce.data) do
613 |> put_view(StatusView)
614 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
618 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
619 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
620 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
622 |> put_view(StatusView)
623 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
627 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
628 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
629 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
631 |> put_view(StatusView)
632 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
636 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
637 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
638 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
640 |> put_view(StatusView)
641 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
645 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
646 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
648 |> put_view(StatusView)
649 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
653 |> put_resp_content_type("application/json")
654 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
658 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
659 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
661 |> put_view(StatusView)
662 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
666 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
667 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
668 %User{} = user <- User.get_cached_by_nickname(user.nickname),
669 true <- Visibility.visible_for_user?(activity, user),
670 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
672 |> put_view(StatusView)
673 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
677 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
678 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
679 %User{} = user <- User.get_cached_by_nickname(user.nickname),
680 true <- Visibility.visible_for_user?(activity, user),
681 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
683 |> put_view(StatusView)
684 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
688 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
689 activity = Activity.get_by_id(id)
691 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
693 |> put_view(StatusView)
694 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
698 |> put_resp_content_type("application/json")
699 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
703 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
704 activity = Activity.get_by_id(id)
706 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
708 |> put_view(StatusView)
709 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
713 def notifications(%{assigns: %{user: user}} = conn, params) do
714 notifications = MastodonAPI.get_notifications(user, params)
717 |> add_link_headers(:notifications, notifications)
718 |> put_view(NotificationView)
719 |> render("index.json", %{notifications: notifications, for: user})
722 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
723 with {:ok, notification} <- Notification.get(user, id) do
725 |> put_view(NotificationView)
726 |> render("show.json", %{notification: notification, for: user})
730 |> put_resp_content_type("application/json")
731 |> send_resp(403, Jason.encode!(%{"error" => reason}))
735 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
736 Notification.clear(user)
740 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
741 with {:ok, _notif} <- Notification.dismiss(user, id) do
746 |> put_resp_content_type("application/json")
747 |> send_resp(403, Jason.encode!(%{"error" => reason}))
751 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
752 Notification.destroy_multiple(user, ids)
756 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
758 q = from(u in User, where: u.id in ^id)
759 targets = Repo.all(q)
762 |> put_view(AccountView)
763 |> render("relationships.json", %{user: user, targets: targets})
766 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
767 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
769 def update_media(%{assigns: %{user: user}} = conn, data) do
770 with %Object{} = object <- Repo.get(Object, data["id"]),
771 true <- Object.authorize_mutation(object, user),
772 true <- is_binary(data["description"]),
773 description <- data["description"] do
774 new_data = %{object.data | "name" => description}
778 |> Object.change(%{data: new_data})
781 attachment_data = Map.put(new_data, "id", object.id)
784 |> put_view(StatusView)
785 |> render("attachment.json", %{attachment: attachment_data})
789 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
790 with {:ok, object} <-
793 actor: User.ap_id(user),
794 description: Map.get(data, "description")
796 attachment_data = Map.put(object.data, "id", object.id)
799 |> put_view(StatusView)
800 |> render("attachment.json", %{attachment: attachment_data})
804 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
805 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
806 %{} = attachment_data <- Map.put(object.data, "id", object.id),
807 %{type: type} = rendered <-
808 StatusView.render("attachment.json", %{attachment: attachment_data}) do
809 # Reject if not an image
810 if type == "image" do
812 # Save to the user's info
813 info_changeset = User.Info.mascot_update(user.info, rendered)
817 |> Ecto.Changeset.change()
818 |> Ecto.Changeset.put_embed(:info, info_changeset)
820 {:ok, _user} = User.update_and_set_cache(user_changeset)
826 |> put_resp_content_type("application/json")
827 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
832 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
833 mascot = User.get_mascot(user)
839 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
840 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
841 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
842 q = from(u in User, where: u.ap_id in ^likes)
846 |> put_view(AccountView)
847 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
853 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
854 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
855 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
856 q = from(u in User, where: u.ap_id in ^announces)
860 |> put_view(AccountView)
861 |> render("accounts.json", %{for: user, users: users, as: :user})
867 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
868 local_only = params["local"] in [true, "True", "true", "1"]
871 [params["tag"], params["any"]]
875 |> Enum.map(&String.downcase(&1))
880 |> Enum.map(&String.downcase(&1))
885 |> Enum.map(&String.downcase(&1))
889 |> Map.put("type", "Create")
890 |> Map.put("local_only", local_only)
891 |> Map.put("blocking_user", user)
892 |> Map.put("muting_user", user)
893 |> Map.put("tag", tags)
894 |> Map.put("tag_all", tag_all)
895 |> Map.put("tag_reject", tag_reject)
896 |> ActivityPub.fetch_public_activities()
900 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
901 |> put_view(StatusView)
902 |> render("index.json", %{activities: activities, for: user, as: :activity})
905 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
906 with %User{} = user <- User.get_cached_by_id(id),
907 followers <- MastodonAPI.get_followers(user, params) do
910 for_user && user.id == for_user.id -> followers
911 user.info.hide_followers -> []
916 |> add_link_headers(:followers, followers, user)
917 |> put_view(AccountView)
918 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
922 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
923 with %User{} = user <- User.get_cached_by_id(id),
924 followers <- MastodonAPI.get_friends(user, params) do
927 for_user && user.id == for_user.id -> followers
928 user.info.hide_follows -> []
933 |> add_link_headers(:following, followers, user)
934 |> put_view(AccountView)
935 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
939 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
940 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
942 |> put_view(AccountView)
943 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
947 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
948 with %User{} = follower <- User.get_cached_by_id(id),
949 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
951 |> put_view(AccountView)
952 |> render("relationship.json", %{user: followed, target: follower})
956 |> put_resp_content_type("application/json")
957 |> send_resp(403, Jason.encode!(%{"error" => message}))
961 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
962 with %User{} = follower <- User.get_cached_by_id(id),
963 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
965 |> put_view(AccountView)
966 |> render("relationship.json", %{user: followed, target: follower})
970 |> put_resp_content_type("application/json")
971 |> send_resp(403, Jason.encode!(%{"error" => message}))
975 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
976 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
977 {_, true} <- {:followed, follower.id != followed.id},
978 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
980 |> put_view(AccountView)
981 |> render("relationship.json", %{user: follower, target: followed})
988 |> put_resp_content_type("application/json")
989 |> send_resp(403, Jason.encode!(%{"error" => message}))
993 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
994 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
995 {_, true} <- {:followed, follower.id != followed.id},
996 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
998 |> put_view(AccountView)
999 |> render("account.json", %{user: followed, for: follower})
1002 {:error, :not_found}
1004 {:error, message} ->
1006 |> put_resp_content_type("application/json")
1007 |> send_resp(403, Jason.encode!(%{"error" => message}))
1011 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1012 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1013 {_, true} <- {:followed, follower.id != followed.id},
1014 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1016 |> put_view(AccountView)
1017 |> render("relationship.json", %{user: follower, target: followed})
1020 {:error, :not_found}
1027 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1028 with %User{} = muted <- User.get_cached_by_id(id),
1029 {:ok, muter} <- User.mute(muter, muted) do
1031 |> put_view(AccountView)
1032 |> render("relationship.json", %{user: muter, target: muted})
1034 {:error, message} ->
1036 |> put_resp_content_type("application/json")
1037 |> send_resp(403, Jason.encode!(%{"error" => message}))
1041 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1042 with %User{} = muted <- User.get_cached_by_id(id),
1043 {:ok, muter} <- User.unmute(muter, muted) do
1045 |> put_view(AccountView)
1046 |> render("relationship.json", %{user: muter, target: muted})
1048 {:error, message} ->
1050 |> put_resp_content_type("application/json")
1051 |> send_resp(403, Jason.encode!(%{"error" => message}))
1055 def mutes(%{assigns: %{user: user}} = conn, _) do
1056 with muted_accounts <- User.muted_users(user) do
1057 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1062 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1063 with %User{} = blocked <- User.get_cached_by_id(id),
1064 {:ok, blocker} <- User.block(blocker, blocked),
1065 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1067 |> put_view(AccountView)
1068 |> render("relationship.json", %{user: blocker, target: blocked})
1070 {:error, message} ->
1072 |> put_resp_content_type("application/json")
1073 |> send_resp(403, Jason.encode!(%{"error" => message}))
1077 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1078 with %User{} = blocked <- User.get_cached_by_id(id),
1079 {:ok, blocker} <- User.unblock(blocker, blocked),
1080 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1082 |> put_view(AccountView)
1083 |> render("relationship.json", %{user: blocker, target: blocked})
1085 {:error, message} ->
1087 |> put_resp_content_type("application/json")
1088 |> send_resp(403, Jason.encode!(%{"error" => message}))
1092 def blocks(%{assigns: %{user: user}} = conn, _) do
1093 with blocked_accounts <- User.blocked_users(user) do
1094 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1099 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1100 json(conn, info.domain_blocks || [])
1103 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1104 User.block_domain(blocker, domain)
1108 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1109 User.unblock_domain(blocker, domain)
1113 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1114 with %User{} = subscription_target <- User.get_cached_by_id(id),
1115 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1117 |> put_view(AccountView)
1118 |> render("relationship.json", %{user: user, target: subscription_target})
1120 {:error, message} ->
1122 |> put_resp_content_type("application/json")
1123 |> send_resp(403, Jason.encode!(%{"error" => message}))
1127 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1128 with %User{} = subscription_target <- User.get_cached_by_id(id),
1129 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1131 |> put_view(AccountView)
1132 |> render("relationship.json", %{user: user, target: subscription_target})
1134 {:error, message} ->
1136 |> put_resp_content_type("application/json")
1137 |> send_resp(403, Jason.encode!(%{"error" => message}))
1141 def favourites(%{assigns: %{user: user}} = conn, params) do
1144 |> Map.put("type", "Create")
1145 |> Map.put("favorited_by", user.ap_id)
1146 |> Map.put("blocking_user", user)
1149 ActivityPub.fetch_activities([], params)
1153 |> add_link_headers(:favourites, activities)
1154 |> put_view(StatusView)
1155 |> render("index.json", %{activities: activities, for: user, as: :activity})
1158 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1159 with %User{} = user <- User.get_by_id(id),
1160 false <- user.info.hide_favorites do
1163 |> Map.put("type", "Create")
1164 |> Map.put("favorited_by", user.ap_id)
1165 |> Map.put("blocking_user", for_user)
1169 ["https://www.w3.org/ns/activitystreams#Public"] ++
1170 [for_user.ap_id | for_user.following]
1172 ["https://www.w3.org/ns/activitystreams#Public"]
1177 |> ActivityPub.fetch_activities(params)
1181 |> add_link_headers(:favourites, activities)
1182 |> put_view(StatusView)
1183 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1186 {:error, :not_found}
1191 |> json(%{error: "Can't get favorites"})
1195 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1196 user = User.get_cached_by_id(user.id)
1199 Bookmark.for_user_query(user.id)
1200 |> Pagination.fetch_paginated(params)
1204 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1207 |> add_link_headers(:bookmarks, bookmarks)
1208 |> put_view(StatusView)
1209 |> render("index.json", %{activities: activities, for: user, as: :activity})
1212 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1213 lists = Pleroma.List.for_user(user, opts)
1214 res = ListView.render("lists.json", lists: lists)
1218 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1219 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1220 res = ListView.render("list.json", list: list)
1226 |> json(%{error: "Record not found"})
1230 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1231 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1232 res = ListView.render("lists.json", lists: lists)
1236 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1237 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1238 {:ok, _list} <- Pleroma.List.delete(list) do
1246 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1247 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1248 res = ListView.render("list.json", list: list)
1253 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1255 |> Enum.each(fn account_id ->
1256 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1257 %User{} = followed <- User.get_cached_by_id(account_id) do
1258 Pleroma.List.follow(list, followed)
1265 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1267 |> Enum.each(fn account_id ->
1268 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1269 %User{} = followed <- User.get_cached_by_id(account_id) do
1270 Pleroma.List.unfollow(list, followed)
1277 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1278 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1279 {:ok, users} = Pleroma.List.get_following(list) do
1281 |> put_view(AccountView)
1282 |> render("accounts.json", %{for: user, users: users, as: :user})
1286 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1287 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1288 {:ok, list} <- Pleroma.List.rename(list, title) do
1289 res = ListView.render("list.json", list: list)
1297 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1298 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1301 |> Map.put("type", "Create")
1302 |> Map.put("blocking_user", user)
1303 |> Map.put("muting_user", user)
1305 # we must filter the following list for the user to avoid leaking statuses the user
1306 # does not actually have permission to see (for more info, peruse security issue #270).
1309 |> Enum.filter(fn x -> x in user.following end)
1310 |> ActivityPub.fetch_activities_bounded(following, params)
1314 |> put_view(StatusView)
1315 |> render("index.json", %{activities: activities, for: user, as: :activity})
1320 |> json(%{error: "Error."})
1324 def index(%{assigns: %{user: user}} = conn, _params) do
1325 token = get_session(conn, :oauth_token)
1328 mastodon_emoji = mastodonized_emoji()
1330 limit = Config.get([:instance, :limit])
1333 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1338 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1339 access_token: token,
1341 domain: Pleroma.Web.Endpoint.host(),
1344 unfollow_modal: false,
1347 auto_play_gif: false,
1348 display_sensitive_media: false,
1349 reduce_motion: false,
1350 max_toot_chars: limit,
1351 mascot: User.get_mascot(user)["url"]
1353 poll_limits: Config.get([:instance, :poll_limits]),
1355 delete_others_notice: present?(user.info.is_moderator),
1356 admin: present?(user.info.is_admin)
1360 default_privacy: user.info.default_scope,
1361 default_sensitive: false,
1362 allow_content_types: Config.get([:instance, :allowed_post_formats])
1364 media_attachments: %{
1365 accept_content_types: [
1381 user.info.settings ||
1411 push_subscription: nil,
1413 custom_emojis: mastodon_emoji,
1419 |> put_layout(false)
1420 |> put_view(MastodonView)
1421 |> render("index.html", %{initial_state: initial_state})
1424 |> put_session(:return_to, conn.request_path)
1425 |> redirect(to: "/web/login")
1429 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1430 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1432 with changeset <- Ecto.Changeset.change(user),
1433 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1434 {:ok, _user} <- User.update_and_set_cache(changeset) do
1439 |> put_resp_content_type("application/json")
1440 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1444 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1445 redirect(conn, to: local_mastodon_root_path(conn))
1448 @doc "Local Mastodon FE login init action"
1449 def login(conn, %{"code" => auth_token}) do
1450 with {:ok, app} <- get_or_make_app(),
1451 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1452 {:ok, token} <- Token.exchange_token(app, auth) do
1454 |> put_session(:oauth_token, token.token)
1455 |> redirect(to: local_mastodon_root_path(conn))
1459 @doc "Local Mastodon FE callback action"
1460 def login(conn, _) do
1461 with {:ok, app} <- get_or_make_app() do
1466 response_type: "code",
1467 client_id: app.client_id,
1469 scope: Enum.join(app.scopes, " ")
1472 redirect(conn, to: path)
1476 defp local_mastodon_root_path(conn) do
1477 case get_session(conn, :return_to) do
1479 mastodon_api_path(conn, :index, ["getting-started"])
1482 delete_session(conn, :return_to)
1487 defp get_or_make_app do
1488 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1489 scopes = ["read", "write", "follow", "push"]
1491 with %App{} = app <- Repo.get_by(App, find_attrs) do
1493 if app.scopes == scopes do
1497 |> Ecto.Changeset.change(%{scopes: scopes})
1505 App.register_changeset(
1507 Map.put(find_attrs, :scopes, scopes)
1514 def logout(conn, _) do
1517 |> redirect(to: "/")
1520 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1521 Logger.debug("Unimplemented, returning unmodified relationship")
1523 with %User{} = target <- User.get_cached_by_id(id) do
1525 |> put_view(AccountView)
1526 |> render("relationship.json", %{user: user, target: target})
1530 def empty_array(conn, _) do
1531 Logger.debug("Unimplemented, returning an empty array")
1535 def empty_object(conn, _) do
1536 Logger.debug("Unimplemented, returning an empty object")
1540 def get_filters(%{assigns: %{user: user}} = conn, _) do
1541 filters = Filter.get_filters(user)
1542 res = FilterView.render("filters.json", filters: filters)
1547 %{assigns: %{user: user}} = conn,
1548 %{"phrase" => phrase, "context" => context} = params
1554 hide: Map.get(params, "irreversible", false),
1555 whole_word: Map.get(params, "boolean", true)
1559 {:ok, response} = Filter.create(query)
1560 res = FilterView.render("filter.json", filter: response)
1564 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1565 filter = Filter.get(filter_id, user)
1566 res = FilterView.render("filter.json", filter: filter)
1571 %{assigns: %{user: user}} = conn,
1572 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1576 filter_id: filter_id,
1579 hide: Map.get(params, "irreversible", nil),
1580 whole_word: Map.get(params, "boolean", true)
1584 {:ok, response} = Filter.update(query)
1585 res = FilterView.render("filter.json", filter: response)
1589 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1592 filter_id: filter_id
1595 {:ok, _} = Filter.delete(query)
1601 def errors(conn, {:error, %Changeset{} = changeset}) do
1604 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1605 |> Enum.map_join(", ", fn {_k, v} -> v end)
1609 |> json(%{error: error_message})
1612 def errors(conn, {:error, :not_found}) do
1615 |> json(%{error: "Record not found"})
1618 def errors(conn, _) do
1621 |> json("Something went wrong")
1624 def suggestions(%{assigns: %{user: user}} = conn, _) do
1625 suggestions = Config.get(:suggestions)
1627 if Keyword.get(suggestions, :enabled, false) do
1628 api = Keyword.get(suggestions, :third_party_engine, "")
1629 timeout = Keyword.get(suggestions, :timeout, 5000)
1630 limit = Keyword.get(suggestions, :limit, 23)
1632 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1634 user = user.nickname
1638 |> String.replace("{{host}}", host)
1639 |> String.replace("{{user}}", user)
1641 with {:ok, %{status: 200, body: body}} <-
1646 recv_timeout: timeout,
1650 {:ok, data} <- Jason.decode(body) do
1653 |> Enum.slice(0, limit)
1658 case User.get_or_fetch(x["acct"]) do
1659 {:ok, %User{id: id}} -> id
1665 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1668 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1674 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1681 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1682 with %Activity{} = activity <- Activity.get_by_id(status_id),
1683 true <- Visibility.visible_for_user?(activity, user) do
1687 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1697 def reports(%{assigns: %{user: user}} = conn, params) do
1698 case CommonAPI.report(user, params) do
1701 |> put_view(ReportView)
1702 |> try_render("report.json", %{activity: activity})
1706 |> put_status(:bad_request)
1707 |> json(%{error: err})
1711 def account_register(
1712 %{assigns: %{app: app}} = conn,
1713 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1721 "captcha_answer_data",
1725 |> Map.put("nickname", nickname)
1726 |> Map.put("fullname", params["fullname"] || nickname)
1727 |> Map.put("bio", params["bio"] || "")
1728 |> Map.put("confirm", params["password"])
1730 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1731 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1733 token_type: "Bearer",
1734 access_token: token.token,
1736 created_at: Token.Utils.format_created_at(token)
1742 |> json(Jason.encode!(errors))
1746 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1749 |> json(%{error: "Missing parameters"})
1752 def account_register(conn, _) do
1755 |> json(%{error: "Invalid credentials"})
1758 def conversations(%{assigns: %{user: user}} = conn, params) do
1759 participations = Participation.for_user_with_last_activity_id(user, params)
1762 Enum.map(participations, fn participation ->
1763 ConversationView.render("participation.json", %{participation: participation, user: user})
1767 |> add_link_headers(:conversations, participations)
1768 |> json(conversations)
1771 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1772 with %Participation{} = participation <-
1773 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1774 {:ok, participation} <- Participation.mark_as_read(participation) do
1775 participation_view =
1776 ConversationView.render("participation.json", %{participation: participation, user: user})
1779 |> json(participation_view)
1783 def try_render(conn, target, params)
1784 when is_binary(target) do
1785 res = render(conn, target, params)
1790 |> json(%{error: "Can't display this activity"})
1796 def try_render(conn, _, _) do
1799 |> json(%{error: "Can't display this activity"})
1802 defp present?(nil), do: false
1803 defp present?(false), do: false
1804 defp present?(_), do: true