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(conn, %{"status" => "", "media_ids" => media_ids} = params)
548 when length(media_ids) > 0 do
551 |> Map.put("status", ".")
553 post_status(conn, params)
556 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
559 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
561 scheduled_at = params["scheduled_at"]
563 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
564 with {:ok, scheduled_activity} <-
565 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
567 |> put_view(ScheduledActivityView)
568 |> render("show.json", %{scheduled_activity: scheduled_activity})
571 params = Map.drop(params, ["scheduled_at"])
573 case get_cached_status_or_post(conn, params) do
574 {:ignore, message} ->
577 |> json(%{error: message})
582 |> json(%{error: message})
586 |> put_view(StatusView)
587 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
592 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
594 case get_req_header(conn, "idempotency-key") do
596 _ -> Ecto.UUID.generate()
599 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
600 case CommonAPI.post(user, params) do
601 {:ok, activity} -> activity
602 {:error, message} -> {:ignore, message}
607 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
608 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
614 |> json(%{error: "Can't delete this post"})
618 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
619 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
620 %Activity{} = announce <- Activity.normalize(announce.data) do
622 |> put_view(StatusView)
623 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
627 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
628 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
629 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
631 |> put_view(StatusView)
632 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
636 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
637 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(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 unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
646 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
647 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
649 |> put_view(StatusView)
650 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
654 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
655 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
657 |> put_view(StatusView)
658 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
662 |> put_resp_content_type("application/json")
663 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
667 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
668 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
670 |> put_view(StatusView)
671 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
675 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
676 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
677 %User{} = user <- User.get_cached_by_nickname(user.nickname),
678 true <- Visibility.visible_for_user?(activity, user),
679 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
681 |> put_view(StatusView)
682 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
686 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
687 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
688 %User{} = user <- User.get_cached_by_nickname(user.nickname),
689 true <- Visibility.visible_for_user?(activity, user),
690 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
692 |> put_view(StatusView)
693 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
697 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
698 activity = Activity.get_by_id(id)
700 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
702 |> put_view(StatusView)
703 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
707 |> put_resp_content_type("application/json")
708 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
712 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
713 activity = Activity.get_by_id(id)
715 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
717 |> put_view(StatusView)
718 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
722 def notifications(%{assigns: %{user: user}} = conn, params) do
723 notifications = MastodonAPI.get_notifications(user, params)
726 |> add_link_headers(:notifications, notifications)
727 |> put_view(NotificationView)
728 |> render("index.json", %{notifications: notifications, for: user})
731 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
732 with {:ok, notification} <- Notification.get(user, id) do
734 |> put_view(NotificationView)
735 |> render("show.json", %{notification: notification, for: user})
739 |> put_resp_content_type("application/json")
740 |> send_resp(403, Jason.encode!(%{"error" => reason}))
744 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
745 Notification.clear(user)
749 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
750 with {:ok, _notif} <- Notification.dismiss(user, id) do
755 |> put_resp_content_type("application/json")
756 |> send_resp(403, Jason.encode!(%{"error" => reason}))
760 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
761 Notification.destroy_multiple(user, ids)
765 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
767 q = from(u in User, where: u.id in ^id)
768 targets = Repo.all(q)
771 |> put_view(AccountView)
772 |> render("relationships.json", %{user: user, targets: targets})
775 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
776 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
778 def update_media(%{assigns: %{user: user}} = conn, data) do
779 with %Object{} = object <- Repo.get(Object, data["id"]),
780 true <- Object.authorize_mutation(object, user),
781 true <- is_binary(data["description"]),
782 description <- data["description"] do
783 new_data = %{object.data | "name" => description}
787 |> Object.change(%{data: new_data})
790 attachment_data = Map.put(new_data, "id", object.id)
793 |> put_view(StatusView)
794 |> render("attachment.json", %{attachment: attachment_data})
798 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
799 with {:ok, object} <-
802 actor: User.ap_id(user),
803 description: Map.get(data, "description")
805 attachment_data = Map.put(object.data, "id", object.id)
808 |> put_view(StatusView)
809 |> render("attachment.json", %{attachment: attachment_data})
813 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
814 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
815 %{} = attachment_data <- Map.put(object.data, "id", object.id),
816 %{type: type} = rendered <-
817 StatusView.render("attachment.json", %{attachment: attachment_data}) do
818 # Reject if not an image
819 if type == "image" do
821 # Save to the user's info
822 info_changeset = User.Info.mascot_update(user.info, rendered)
826 |> Ecto.Changeset.change()
827 |> Ecto.Changeset.put_embed(:info, info_changeset)
829 {:ok, _user} = User.update_and_set_cache(user_changeset)
835 |> put_resp_content_type("application/json")
836 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
841 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
842 mascot = User.get_mascot(user)
848 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
849 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
850 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
851 q = from(u in User, where: u.ap_id in ^likes)
855 |> put_view(AccountView)
856 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
862 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
863 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
864 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
865 q = from(u in User, where: u.ap_id in ^announces)
869 |> put_view(AccountView)
870 |> render("accounts.json", %{for: user, users: users, as: :user})
876 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
877 local_only = params["local"] in [true, "True", "true", "1"]
880 [params["tag"], params["any"]]
884 |> Enum.map(&String.downcase(&1))
889 |> Enum.map(&String.downcase(&1))
894 |> Enum.map(&String.downcase(&1))
898 |> Map.put("type", "Create")
899 |> Map.put("local_only", local_only)
900 |> Map.put("blocking_user", user)
901 |> Map.put("muting_user", user)
902 |> Map.put("tag", tags)
903 |> Map.put("tag_all", tag_all)
904 |> Map.put("tag_reject", tag_reject)
905 |> ActivityPub.fetch_public_activities()
909 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
910 |> put_view(StatusView)
911 |> render("index.json", %{activities: activities, for: user, as: :activity})
914 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
915 with %User{} = user <- User.get_cached_by_id(id),
916 followers <- MastodonAPI.get_followers(user, params) do
919 for_user && user.id == for_user.id -> followers
920 user.info.hide_followers -> []
925 |> add_link_headers(:followers, followers, user)
926 |> put_view(AccountView)
927 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
931 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
932 with %User{} = user <- User.get_cached_by_id(id),
933 followers <- MastodonAPI.get_friends(user, params) do
936 for_user && user.id == for_user.id -> followers
937 user.info.hide_follows -> []
942 |> add_link_headers(:following, followers, user)
943 |> put_view(AccountView)
944 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
948 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
949 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
951 |> put_view(AccountView)
952 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
956 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
957 with %User{} = follower <- User.get_cached_by_id(id),
958 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
960 |> put_view(AccountView)
961 |> render("relationship.json", %{user: followed, target: follower})
965 |> put_resp_content_type("application/json")
966 |> send_resp(403, Jason.encode!(%{"error" => message}))
970 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
971 with %User{} = follower <- User.get_cached_by_id(id),
972 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
974 |> put_view(AccountView)
975 |> render("relationship.json", %{user: followed, target: follower})
979 |> put_resp_content_type("application/json")
980 |> send_resp(403, Jason.encode!(%{"error" => message}))
984 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
985 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
986 {_, true} <- {:followed, follower.id != followed.id},
987 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
989 |> put_view(AccountView)
990 |> render("relationship.json", %{user: follower, target: followed})
997 |> put_resp_content_type("application/json")
998 |> send_resp(403, Jason.encode!(%{"error" => message}))
1002 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1003 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1004 {_, true} <- {:followed, follower.id != followed.id},
1005 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1007 |> put_view(AccountView)
1008 |> render("account.json", %{user: followed, for: follower})
1011 {:error, :not_found}
1013 {:error, message} ->
1015 |> put_resp_content_type("application/json")
1016 |> send_resp(403, Jason.encode!(%{"error" => message}))
1020 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1021 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1022 {_, true} <- {:followed, follower.id != followed.id},
1023 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1025 |> put_view(AccountView)
1026 |> render("relationship.json", %{user: follower, target: followed})
1029 {:error, :not_found}
1036 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1037 with %User{} = muted <- User.get_cached_by_id(id),
1038 {:ok, muter} <- User.mute(muter, muted) do
1040 |> put_view(AccountView)
1041 |> render("relationship.json", %{user: muter, target: muted})
1043 {:error, message} ->
1045 |> put_resp_content_type("application/json")
1046 |> send_resp(403, Jason.encode!(%{"error" => message}))
1050 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1051 with %User{} = muted <- User.get_cached_by_id(id),
1052 {:ok, muter} <- User.unmute(muter, muted) do
1054 |> put_view(AccountView)
1055 |> render("relationship.json", %{user: muter, target: muted})
1057 {:error, message} ->
1059 |> put_resp_content_type("application/json")
1060 |> send_resp(403, Jason.encode!(%{"error" => message}))
1064 def mutes(%{assigns: %{user: user}} = conn, _) do
1065 with muted_accounts <- User.muted_users(user) do
1066 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1071 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1072 with %User{} = blocked <- User.get_cached_by_id(id),
1073 {:ok, blocker} <- User.block(blocker, blocked),
1074 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1076 |> put_view(AccountView)
1077 |> render("relationship.json", %{user: blocker, target: blocked})
1079 {:error, message} ->
1081 |> put_resp_content_type("application/json")
1082 |> send_resp(403, Jason.encode!(%{"error" => message}))
1086 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1087 with %User{} = blocked <- User.get_cached_by_id(id),
1088 {:ok, blocker} <- User.unblock(blocker, blocked),
1089 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1091 |> put_view(AccountView)
1092 |> render("relationship.json", %{user: blocker, target: blocked})
1094 {:error, message} ->
1096 |> put_resp_content_type("application/json")
1097 |> send_resp(403, Jason.encode!(%{"error" => message}))
1101 def blocks(%{assigns: %{user: user}} = conn, _) do
1102 with blocked_accounts <- User.blocked_users(user) do
1103 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1108 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1109 json(conn, info.domain_blocks || [])
1112 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1113 User.block_domain(blocker, domain)
1117 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1118 User.unblock_domain(blocker, domain)
1122 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1123 with %User{} = subscription_target <- User.get_cached_by_id(id),
1124 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1126 |> put_view(AccountView)
1127 |> render("relationship.json", %{user: user, target: subscription_target})
1129 {:error, message} ->
1131 |> put_resp_content_type("application/json")
1132 |> send_resp(403, Jason.encode!(%{"error" => message}))
1136 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1137 with %User{} = subscription_target <- User.get_cached_by_id(id),
1138 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1140 |> put_view(AccountView)
1141 |> render("relationship.json", %{user: user, target: subscription_target})
1143 {:error, message} ->
1145 |> put_resp_content_type("application/json")
1146 |> send_resp(403, Jason.encode!(%{"error" => message}))
1150 def favourites(%{assigns: %{user: user}} = conn, params) do
1153 |> Map.put("type", "Create")
1154 |> Map.put("favorited_by", user.ap_id)
1155 |> Map.put("blocking_user", user)
1158 ActivityPub.fetch_activities([], params)
1162 |> add_link_headers(:favourites, activities)
1163 |> put_view(StatusView)
1164 |> render("index.json", %{activities: activities, for: user, as: :activity})
1167 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1168 with %User{} = user <- User.get_by_id(id),
1169 false <- user.info.hide_favorites do
1172 |> Map.put("type", "Create")
1173 |> Map.put("favorited_by", user.ap_id)
1174 |> Map.put("blocking_user", for_user)
1178 ["https://www.w3.org/ns/activitystreams#Public"] ++
1179 [for_user.ap_id | for_user.following]
1181 ["https://www.w3.org/ns/activitystreams#Public"]
1186 |> ActivityPub.fetch_activities(params)
1190 |> add_link_headers(:favourites, activities)
1191 |> put_view(StatusView)
1192 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1195 {:error, :not_found}
1200 |> json(%{error: "Can't get favorites"})
1204 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1205 user = User.get_cached_by_id(user.id)
1208 Bookmark.for_user_query(user.id)
1209 |> Pagination.fetch_paginated(params)
1213 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1216 |> add_link_headers(:bookmarks, bookmarks)
1217 |> put_view(StatusView)
1218 |> render("index.json", %{activities: activities, for: user, as: :activity})
1221 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1222 lists = Pleroma.List.for_user(user, opts)
1223 res = ListView.render("lists.json", lists: lists)
1227 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1228 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1229 res = ListView.render("list.json", list: list)
1235 |> json(%{error: "Record not found"})
1239 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1240 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1241 res = ListView.render("lists.json", lists: lists)
1245 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1246 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1247 {:ok, _list} <- Pleroma.List.delete(list) do
1255 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1256 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1257 res = ListView.render("list.json", list: list)
1262 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1264 |> Enum.each(fn account_id ->
1265 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1266 %User{} = followed <- User.get_cached_by_id(account_id) do
1267 Pleroma.List.follow(list, followed)
1274 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1276 |> Enum.each(fn account_id ->
1277 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1278 %User{} = followed <- User.get_cached_by_id(account_id) do
1279 Pleroma.List.unfollow(list, followed)
1286 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1287 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1288 {:ok, users} = Pleroma.List.get_following(list) do
1290 |> put_view(AccountView)
1291 |> render("accounts.json", %{for: user, users: users, as: :user})
1295 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1296 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1297 {:ok, list} <- Pleroma.List.rename(list, title) do
1298 res = ListView.render("list.json", list: list)
1306 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1307 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1310 |> Map.put("type", "Create")
1311 |> Map.put("blocking_user", user)
1312 |> Map.put("muting_user", user)
1314 # we must filter the following list for the user to avoid leaking statuses the user
1315 # does not actually have permission to see (for more info, peruse security issue #270).
1318 |> Enum.filter(fn x -> x in user.following end)
1319 |> ActivityPub.fetch_activities_bounded(following, params)
1323 |> put_view(StatusView)
1324 |> render("index.json", %{activities: activities, for: user, as: :activity})
1329 |> json(%{error: "Error."})
1333 def index(%{assigns: %{user: user}} = conn, _params) do
1334 token = get_session(conn, :oauth_token)
1337 mastodon_emoji = mastodonized_emoji()
1339 limit = Config.get([:instance, :limit])
1342 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1347 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1348 access_token: token,
1350 domain: Pleroma.Web.Endpoint.host(),
1353 unfollow_modal: false,
1356 auto_play_gif: false,
1357 display_sensitive_media: false,
1358 reduce_motion: false,
1359 max_toot_chars: limit,
1360 mascot: User.get_mascot(user)["url"]
1362 poll_limits: Config.get([:instance, :poll_limits]),
1364 delete_others_notice: present?(user.info.is_moderator),
1365 admin: present?(user.info.is_admin)
1369 default_privacy: user.info.default_scope,
1370 default_sensitive: false,
1371 allow_content_types: Config.get([:instance, :allowed_post_formats])
1373 media_attachments: %{
1374 accept_content_types: [
1390 user.info.settings ||
1420 push_subscription: nil,
1422 custom_emojis: mastodon_emoji,
1428 |> put_layout(false)
1429 |> put_view(MastodonView)
1430 |> render("index.html", %{initial_state: initial_state})
1433 |> put_session(:return_to, conn.request_path)
1434 |> redirect(to: "/web/login")
1438 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1439 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1441 with changeset <- Ecto.Changeset.change(user),
1442 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1443 {:ok, _user} <- User.update_and_set_cache(changeset) do
1448 |> put_resp_content_type("application/json")
1449 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1453 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1454 redirect(conn, to: local_mastodon_root_path(conn))
1457 @doc "Local Mastodon FE login init action"
1458 def login(conn, %{"code" => auth_token}) do
1459 with {:ok, app} <- get_or_make_app(),
1460 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1461 {:ok, token} <- Token.exchange_token(app, auth) do
1463 |> put_session(:oauth_token, token.token)
1464 |> redirect(to: local_mastodon_root_path(conn))
1468 @doc "Local Mastodon FE callback action"
1469 def login(conn, _) do
1470 with {:ok, app} <- get_or_make_app() do
1475 response_type: "code",
1476 client_id: app.client_id,
1478 scope: Enum.join(app.scopes, " ")
1481 redirect(conn, to: path)
1485 defp local_mastodon_root_path(conn) do
1486 case get_session(conn, :return_to) do
1488 mastodon_api_path(conn, :index, ["getting-started"])
1491 delete_session(conn, :return_to)
1496 defp get_or_make_app do
1497 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1498 scopes = ["read", "write", "follow", "push"]
1500 with %App{} = app <- Repo.get_by(App, find_attrs) do
1502 if app.scopes == scopes do
1506 |> Ecto.Changeset.change(%{scopes: scopes})
1514 App.register_changeset(
1516 Map.put(find_attrs, :scopes, scopes)
1523 def logout(conn, _) do
1526 |> redirect(to: "/")
1529 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1530 Logger.debug("Unimplemented, returning unmodified relationship")
1532 with %User{} = target <- User.get_cached_by_id(id) do
1534 |> put_view(AccountView)
1535 |> render("relationship.json", %{user: user, target: target})
1539 def empty_array(conn, _) do
1540 Logger.debug("Unimplemented, returning an empty array")
1544 def empty_object(conn, _) do
1545 Logger.debug("Unimplemented, returning an empty object")
1549 def get_filters(%{assigns: %{user: user}} = conn, _) do
1550 filters = Filter.get_filters(user)
1551 res = FilterView.render("filters.json", filters: filters)
1556 %{assigns: %{user: user}} = conn,
1557 %{"phrase" => phrase, "context" => context} = params
1563 hide: Map.get(params, "irreversible", false),
1564 whole_word: Map.get(params, "boolean", true)
1568 {:ok, response} = Filter.create(query)
1569 res = FilterView.render("filter.json", filter: response)
1573 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1574 filter = Filter.get(filter_id, user)
1575 res = FilterView.render("filter.json", filter: filter)
1580 %{assigns: %{user: user}} = conn,
1581 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1585 filter_id: filter_id,
1588 hide: Map.get(params, "irreversible", nil),
1589 whole_word: Map.get(params, "boolean", true)
1593 {:ok, response} = Filter.update(query)
1594 res = FilterView.render("filter.json", filter: response)
1598 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1601 filter_id: filter_id
1604 {:ok, _} = Filter.delete(query)
1610 def errors(conn, {:error, %Changeset{} = changeset}) do
1613 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1614 |> Enum.map_join(", ", fn {_k, v} -> v end)
1618 |> json(%{error: error_message})
1621 def errors(conn, {:error, :not_found}) do
1624 |> json(%{error: "Record not found"})
1627 def errors(conn, _) do
1630 |> json("Something went wrong")
1633 def suggestions(%{assigns: %{user: user}} = conn, _) do
1634 suggestions = Config.get(:suggestions)
1636 if Keyword.get(suggestions, :enabled, false) do
1637 api = Keyword.get(suggestions, :third_party_engine, "")
1638 timeout = Keyword.get(suggestions, :timeout, 5000)
1639 limit = Keyword.get(suggestions, :limit, 23)
1641 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1643 user = user.nickname
1647 |> String.replace("{{host}}", host)
1648 |> String.replace("{{user}}", user)
1650 with {:ok, %{status: 200, body: body}} <-
1655 recv_timeout: timeout,
1659 {:ok, data} <- Jason.decode(body) do
1662 |> Enum.slice(0, limit)
1667 case User.get_or_fetch(x["acct"]) do
1668 {:ok, %User{id: id}} -> id
1674 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1677 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1683 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1690 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1691 with %Activity{} = activity <- Activity.get_by_id(status_id),
1692 true <- Visibility.visible_for_user?(activity, user) do
1696 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1706 def reports(%{assigns: %{user: user}} = conn, params) do
1707 case CommonAPI.report(user, params) do
1710 |> put_view(ReportView)
1711 |> try_render("report.json", %{activity: activity})
1715 |> put_status(:bad_request)
1716 |> json(%{error: err})
1720 def account_register(
1721 %{assigns: %{app: app}} = conn,
1722 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1730 "captcha_answer_data",
1734 |> Map.put("nickname", nickname)
1735 |> Map.put("fullname", params["fullname"] || nickname)
1736 |> Map.put("bio", params["bio"] || "")
1737 |> Map.put("confirm", params["password"])
1739 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1740 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1742 token_type: "Bearer",
1743 access_token: token.token,
1745 created_at: Token.Utils.format_created_at(token)
1751 |> json(Jason.encode!(errors))
1755 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1758 |> json(%{error: "Missing parameters"})
1761 def account_register(conn, _) do
1764 |> json(%{error: "Invalid credentials"})
1767 def conversations(%{assigns: %{user: user}} = conn, params) do
1768 participations = Participation.for_user_with_last_activity_id(user, params)
1771 Enum.map(participations, fn participation ->
1772 ConversationView.render("participation.json", %{participation: participation, user: user})
1776 |> add_link_headers(:conversations, participations)
1777 |> json(conversations)
1780 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1781 with %Participation{} = participation <-
1782 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1783 {:ok, participation} <- Participation.mark_as_read(participation) do
1784 participation_view =
1785 ConversationView.render("participation.json", %{participation: participation, user: user})
1788 |> json(participation_view)
1792 def try_render(conn, target, params)
1793 when is_binary(target) do
1794 res = render(conn, target, params)
1799 |> json(%{error: "Can't display this activity"})
1805 def try_render(conn, _, _) do
1808 |> json(%{error: "Can't display this activity"})
1811 defp present?(nil), do: false
1812 defp present?(false), do: false
1813 defp present?(_), do: true