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.Object.Fetcher
18 alias Pleroma.Pagination
20 alias Pleroma.ScheduledActivity
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.ConversationView
30 alias Pleroma.Web.MastodonAPI.FilterView
31 alias Pleroma.Web.MastodonAPI.ListView
32 alias Pleroma.Web.MastodonAPI.MastodonAPI
33 alias Pleroma.Web.MastodonAPI.MastodonView
34 alias Pleroma.Web.MastodonAPI.NotificationView
35 alias Pleroma.Web.MastodonAPI.ReportView
36 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
37 alias Pleroma.Web.MastodonAPI.StatusView
38 alias Pleroma.Web.MediaProxy
39 alias Pleroma.Web.OAuth.App
40 alias Pleroma.Web.OAuth.Authorization
41 alias Pleroma.Web.OAuth.Scopes
42 alias Pleroma.Web.OAuth.Token
43 alias Pleroma.Web.TwitterAPI.TwitterAPI
45 alias Pleroma.Web.ControllerHelper
51 Pleroma.Plugs.RateLimitPlug,
53 max_requests: Config.get([:app_account_creation, :max_requests]),
54 interval: Config.get([:app_account_creation, :interval])
56 when action in [:account_register]
59 @local_mastodon_name "Mastodon-Local"
61 action_fallback(:errors)
63 def create_app(conn, params) do
64 scopes = Scopes.fetch_scopes(params, ["read"])
68 |> Map.drop(["scope", "scopes"])
69 |> Map.put("scopes", scopes)
71 with cs <- App.register_changeset(%App{}, app_attrs),
72 false <- cs.changes[:client_name] == @local_mastodon_name,
73 {:ok, app} <- Repo.insert(cs) do
76 |> render("show.json", %{app: app})
85 value_function \\ fn x -> {:ok, x} end
87 if Map.has_key?(params, params_field) do
88 case value_function.(params[params_field]) do
89 {:ok, new_value} -> Map.put(map, map_field, new_value)
97 def update_credentials(%{assigns: %{user: user}} = conn, params) do
102 |> add_if_present(params, "display_name", :name)
103 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
104 |> add_if_present(params, "avatar", :avatar, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
113 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
116 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
121 |> Enum.reduce(%{}, fn key, acc ->
122 add_if_present(acc, params, to_string(key), key, fn value ->
123 {:ok, ControllerHelper.truthy_param?(value)}
126 |> add_if_present(params, "default_scope", :default_scope)
127 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
128 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
130 |> add_if_present(params, "header", :banner, fn value ->
131 with %Plug.Upload{} <- value,
132 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
138 |> Map.put(:emoji, user_info_emojis)
140 info_cng = User.Info.profile_update(user.info, info_params)
142 with changeset <- User.update_changeset(user, user_params),
143 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
144 {:ok, user} <- User.update_and_set_cache(changeset) do
145 if original_user != user do
146 CommonAPI.update(user)
151 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
157 |> json(%{error: "Invalid request"})
161 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
163 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
168 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
169 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
172 |> render("short.json", %{app: app})
176 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
177 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
178 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
179 account = AccountView.render("account.json", %{user: user, for: for_user})
185 |> json(%{error: "Can't find user"})
189 @mastodon_api_level "2.7.2"
191 def masto_instance(conn, _params) do
192 instance = Config.get(:instance)
196 title: Keyword.get(instance, :name),
197 description: Keyword.get(instance, :description),
198 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
199 email: Keyword.get(instance, :email),
201 streaming_api: Pleroma.Web.Endpoint.websocket_url()
203 stats: Stats.get_stats(),
204 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
206 registrations: Pleroma.Config.get([:instance, :registrations_open]),
207 # Extra (not present in Mastodon):
208 max_toot_chars: Keyword.get(instance, :limit),
209 poll_limits: Keyword.get(instance, :poll_limits)
215 def peers(conn, _params) do
216 json(conn, Stats.get_peers())
219 defp mastodonized_emoji do
220 Pleroma.Emoji.get_all()
221 |> Enum.map(fn {shortcode, relative_url, tags} ->
222 url = to_string(URI.merge(Web.base_url(), relative_url))
225 "shortcode" => shortcode,
227 "visible_in_picker" => true,
234 def custom_emojis(conn, _params) do
235 mastodon_emoji = mastodonized_emoji()
236 json(conn, mastodon_emoji)
239 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
242 |> Map.drop(["since_id", "max_id", "min_id"])
245 last = List.last(activities)
252 |> Map.get("limit", "20")
253 |> String.to_integer()
256 if length(activities) <= limit do
262 |> Enum.at(limit * -1)
266 {next_url, prev_url} =
270 Pleroma.Web.Endpoint,
273 Map.merge(params, %{max_id: max_id})
276 Pleroma.Web.Endpoint,
279 Map.merge(params, %{min_id: min_id})
285 Pleroma.Web.Endpoint,
287 Map.merge(params, %{max_id: max_id})
290 Pleroma.Web.Endpoint,
292 Map.merge(params, %{min_id: min_id})
298 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
304 def home_timeline(%{assigns: %{user: user}} = conn, params) do
307 |> Map.put("type", ["Create", "Announce"])
308 |> Map.put("blocking_user", user)
309 |> Map.put("muting_user", user)
310 |> Map.put("user", user)
313 [user.ap_id | user.following]
314 |> ActivityPub.fetch_activities(params)
318 |> add_link_headers(:home_timeline, activities)
319 |> put_view(StatusView)
320 |> render("index.json", %{activities: activities, for: user, as: :activity})
323 def public_timeline(%{assigns: %{user: user}} = conn, params) do
324 local_only = params["local"] in [true, "True", "true", "1"]
328 |> Map.put("type", ["Create", "Announce"])
329 |> Map.put("local_only", local_only)
330 |> Map.put("blocking_user", user)
331 |> Map.put("muting_user", user)
332 |> ActivityPub.fetch_public_activities()
336 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
337 |> put_view(StatusView)
338 |> render("index.json", %{activities: activities, for: user, as: :activity})
341 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
342 with %User{} = user <- User.get_cached_by_id(params["id"]) do
343 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
346 |> add_link_headers(:user_statuses, activities, params["id"])
347 |> put_view(StatusView)
348 |> render("index.json", %{
349 activities: activities,
356 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
359 |> Map.put("type", "Create")
360 |> Map.put("blocking_user", user)
361 |> Map.put("user", user)
362 |> Map.put(:visibility, "direct")
366 |> ActivityPub.fetch_activities_query(params)
367 |> Pagination.fetch_paginated(params)
370 |> add_link_headers(:dm_timeline, activities)
371 |> put_view(StatusView)
372 |> render("index.json", %{activities: activities, for: user, as: :activity})
375 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
376 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
377 true <- Visibility.visible_for_user?(activity, user) do
379 |> put_view(StatusView)
380 |> try_render("status.json", %{activity: activity, for: user})
384 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
385 with %Activity{} = activity <- Activity.get_by_id(id),
387 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
388 "blocking_user" => user,
392 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
394 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
395 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
401 activities: grouped_activities[true] || [],
405 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
410 activities: grouped_activities[false] || [],
414 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
421 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
422 with %Object{} = object <- Object.get_by_id(id),
423 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
424 true <- Visibility.visible_for_user?(activity, user) do
426 |> put_view(StatusView)
427 |> try_render("poll.json", %{object: object, for: user})
432 |> json(%{error: "Record not found"})
437 |> json(%{error: "Record not found"})
441 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
442 with %Object{} = object <- Object.get_by_id(id),
443 true <- object.data["type"] == "Question",
444 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
445 true <- Visibility.visible_for_user?(activity, user),
446 {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
448 |> put_view(StatusView)
449 |> try_render("poll.json", %{object: object, for: user})
454 |> json(%{error: "Record not found"})
459 |> json(%{error: "Record not found"})
464 |> json(%{error: message})
468 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
469 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
471 |> add_link_headers(:scheduled_statuses, scheduled_activities)
472 |> put_view(ScheduledActivityView)
473 |> render("index.json", %{scheduled_activities: scheduled_activities})
477 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
478 with %ScheduledActivity{} = scheduled_activity <-
479 ScheduledActivity.get(user, scheduled_activity_id) do
481 |> put_view(ScheduledActivityView)
482 |> render("show.json", %{scheduled_activity: scheduled_activity})
484 _ -> {:error, :not_found}
488 def update_scheduled_status(
489 %{assigns: %{user: user}} = conn,
490 %{"id" => scheduled_activity_id} = params
492 with %ScheduledActivity{} = scheduled_activity <-
493 ScheduledActivity.get(user, scheduled_activity_id),
494 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
496 |> put_view(ScheduledActivityView)
497 |> render("show.json", %{scheduled_activity: scheduled_activity})
499 nil -> {:error, :not_found}
504 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
505 with %ScheduledActivity{} = scheduled_activity <-
506 ScheduledActivity.get(user, scheduled_activity_id),
507 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
509 |> put_view(ScheduledActivityView)
510 |> render("show.json", %{scheduled_activity: scheduled_activity})
512 nil -> {:error, :not_found}
517 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
518 when length(media_ids) > 0 do
521 |> Map.put("status", ".")
523 post_status(conn, params)
526 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
529 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
531 scheduled_at = params["scheduled_at"]
533 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
534 with {:ok, scheduled_activity} <-
535 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
537 |> put_view(ScheduledActivityView)
538 |> render("show.json", %{scheduled_activity: scheduled_activity})
541 params = Map.drop(params, ["scheduled_at"])
543 case get_cached_status_or_post(conn, params) do
544 {:ignore, message} ->
547 |> json(%{error: message})
552 |> json(%{error: message})
556 |> put_view(StatusView)
557 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
562 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
564 case get_req_header(conn, "idempotency-key") do
566 _ -> Ecto.UUID.generate()
569 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
570 case CommonAPI.post(user, params) do
571 {:ok, activity} -> activity
572 {:error, message} -> {:ignore, message}
577 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
578 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
584 |> json(%{error: "Can't delete this post"})
588 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
589 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
590 %Activity{} = announce <- Activity.normalize(announce.data) do
592 |> put_view(StatusView)
593 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
597 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
598 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
599 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
601 |> put_view(StatusView)
602 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
606 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
607 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
608 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
610 |> put_view(StatusView)
611 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
615 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
616 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
617 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
619 |> put_view(StatusView)
620 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
624 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
625 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
627 |> put_view(StatusView)
628 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
632 |> put_resp_content_type("application/json")
633 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
637 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
638 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
640 |> put_view(StatusView)
641 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
645 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
646 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
647 %User{} = user <- User.get_cached_by_nickname(user.nickname),
648 true <- Visibility.visible_for_user?(activity, user),
649 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
651 |> put_view(StatusView)
652 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
656 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
657 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
658 %User{} = user <- User.get_cached_by_nickname(user.nickname),
659 true <- Visibility.visible_for_user?(activity, user),
660 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
662 |> put_view(StatusView)
663 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
667 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
668 activity = Activity.get_by_id(id)
670 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
672 |> put_view(StatusView)
673 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
677 |> put_resp_content_type("application/json")
678 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
682 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
683 activity = Activity.get_by_id(id)
685 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
687 |> put_view(StatusView)
688 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
692 def notifications(%{assigns: %{user: user}} = conn, params) do
693 notifications = MastodonAPI.get_notifications(user, params)
696 |> add_link_headers(:notifications, notifications)
697 |> put_view(NotificationView)
698 |> render("index.json", %{notifications: notifications, for: user})
701 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
702 with {:ok, notification} <- Notification.get(user, id) do
704 |> put_view(NotificationView)
705 |> render("show.json", %{notification: notification, for: user})
709 |> put_resp_content_type("application/json")
710 |> send_resp(403, Jason.encode!(%{"error" => reason}))
714 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
715 Notification.clear(user)
719 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
720 with {:ok, _notif} <- Notification.dismiss(user, id) do
725 |> put_resp_content_type("application/json")
726 |> send_resp(403, Jason.encode!(%{"error" => reason}))
730 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
731 Notification.destroy_multiple(user, ids)
735 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
737 q = from(u in User, where: u.id in ^id)
738 targets = Repo.all(q)
741 |> put_view(AccountView)
742 |> render("relationships.json", %{user: user, targets: targets})
745 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
746 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
748 def update_media(%{assigns: %{user: user}} = conn, data) do
749 with %Object{} = object <- Repo.get(Object, data["id"]),
750 true <- Object.authorize_mutation(object, user),
751 true <- is_binary(data["description"]),
752 description <- data["description"] do
753 new_data = %{object.data | "name" => description}
757 |> Object.change(%{data: new_data})
760 attachment_data = Map.put(new_data, "id", object.id)
763 |> put_view(StatusView)
764 |> render("attachment.json", %{attachment: attachment_data})
768 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
769 with {:ok, object} <-
772 actor: User.ap_id(user),
773 description: Map.get(data, "description")
775 attachment_data = Map.put(object.data, "id", object.id)
778 |> put_view(StatusView)
779 |> render("attachment.json", %{attachment: attachment_data})
783 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
784 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
785 %{} = attachment_data <- Map.put(object.data, "id", object.id),
786 %{type: type} = rendered <-
787 StatusView.render("attachment.json", %{attachment: attachment_data}) do
788 # Reject if not an image
789 if type == "image" do
791 # Save to the user's info
792 info_changeset = User.Info.mascot_update(user.info, rendered)
796 |> Ecto.Changeset.change()
797 |> Ecto.Changeset.put_embed(:info, info_changeset)
799 {:ok, _user} = User.update_and_set_cache(user_changeset)
805 |> put_resp_content_type("application/json")
806 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
811 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
812 mascot = User.get_mascot(user)
818 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
819 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
820 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
821 q = from(u in User, where: u.ap_id in ^likes)
825 |> put_view(AccountView)
826 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
832 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
833 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
834 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
835 q = from(u in User, where: u.ap_id in ^announces)
839 |> put_view(AccountView)
840 |> render("accounts.json", %{for: user, users: users, as: :user})
846 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
847 local_only = params["local"] in [true, "True", "true", "1"]
850 [params["tag"], params["any"]]
854 |> Enum.map(&String.downcase(&1))
859 |> Enum.map(&String.downcase(&1))
864 |> Enum.map(&String.downcase(&1))
868 |> Map.put("type", "Create")
869 |> Map.put("local_only", local_only)
870 |> Map.put("blocking_user", user)
871 |> Map.put("muting_user", user)
872 |> Map.put("tag", tags)
873 |> Map.put("tag_all", tag_all)
874 |> Map.put("tag_reject", tag_reject)
875 |> ActivityPub.fetch_public_activities()
879 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
880 |> put_view(StatusView)
881 |> render("index.json", %{activities: activities, for: user, as: :activity})
884 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
885 with %User{} = user <- User.get_cached_by_id(id),
886 followers <- MastodonAPI.get_followers(user, params) do
889 for_user && user.id == for_user.id -> followers
890 user.info.hide_followers -> []
895 |> add_link_headers(:followers, followers, user)
896 |> put_view(AccountView)
897 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
901 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
902 with %User{} = user <- User.get_cached_by_id(id),
903 followers <- MastodonAPI.get_friends(user, params) do
906 for_user && user.id == for_user.id -> followers
907 user.info.hide_follows -> []
912 |> add_link_headers(:following, followers, user)
913 |> put_view(AccountView)
914 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
918 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
919 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
921 |> put_view(AccountView)
922 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
926 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
927 with %User{} = follower <- User.get_cached_by_id(id),
928 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
930 |> put_view(AccountView)
931 |> render("relationship.json", %{user: followed, target: follower})
935 |> put_resp_content_type("application/json")
936 |> send_resp(403, Jason.encode!(%{"error" => message}))
940 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
941 with %User{} = follower <- User.get_cached_by_id(id),
942 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
944 |> put_view(AccountView)
945 |> render("relationship.json", %{user: followed, target: follower})
949 |> put_resp_content_type("application/json")
950 |> send_resp(403, Jason.encode!(%{"error" => message}))
954 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
955 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
956 {_, true} <- {:followed, follower.id != followed.id},
957 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
959 |> put_view(AccountView)
960 |> render("relationship.json", %{user: follower, target: followed})
967 |> put_resp_content_type("application/json")
968 |> send_resp(403, Jason.encode!(%{"error" => message}))
972 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
973 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
974 {_, true} <- {:followed, follower.id != followed.id},
975 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
977 |> put_view(AccountView)
978 |> render("account.json", %{user: followed, for: follower})
985 |> put_resp_content_type("application/json")
986 |> send_resp(403, Jason.encode!(%{"error" => message}))
990 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
991 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
992 {_, true} <- {:followed, follower.id != followed.id},
993 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
995 |> put_view(AccountView)
996 |> render("relationship.json", %{user: follower, target: followed})
1006 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1007 with %User{} = muted <- User.get_cached_by_id(id),
1008 {:ok, muter} <- User.mute(muter, muted) do
1010 |> put_view(AccountView)
1011 |> render("relationship.json", %{user: muter, target: muted})
1013 {:error, message} ->
1015 |> put_resp_content_type("application/json")
1016 |> send_resp(403, Jason.encode!(%{"error" => message}))
1020 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1021 with %User{} = muted <- User.get_cached_by_id(id),
1022 {:ok, muter} <- User.unmute(muter, muted) do
1024 |> put_view(AccountView)
1025 |> render("relationship.json", %{user: muter, target: muted})
1027 {:error, message} ->
1029 |> put_resp_content_type("application/json")
1030 |> send_resp(403, Jason.encode!(%{"error" => message}))
1034 def mutes(%{assigns: %{user: user}} = conn, _) do
1035 with muted_accounts <- User.muted_users(user) do
1036 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1041 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1042 with %User{} = blocked <- User.get_cached_by_id(id),
1043 {:ok, blocker} <- User.block(blocker, blocked),
1044 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1046 |> put_view(AccountView)
1047 |> render("relationship.json", %{user: blocker, target: blocked})
1049 {:error, message} ->
1051 |> put_resp_content_type("application/json")
1052 |> send_resp(403, Jason.encode!(%{"error" => message}))
1056 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1057 with %User{} = blocked <- User.get_cached_by_id(id),
1058 {:ok, blocker} <- User.unblock(blocker, blocked),
1059 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1061 |> put_view(AccountView)
1062 |> render("relationship.json", %{user: blocker, target: blocked})
1064 {:error, message} ->
1066 |> put_resp_content_type("application/json")
1067 |> send_resp(403, Jason.encode!(%{"error" => message}))
1071 def blocks(%{assigns: %{user: user}} = conn, _) do
1072 with blocked_accounts <- User.blocked_users(user) do
1073 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1078 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1079 json(conn, info.domain_blocks || [])
1082 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1083 User.block_domain(blocker, domain)
1087 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1088 User.unblock_domain(blocker, domain)
1092 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1093 with %User{} = subscription_target <- User.get_cached_by_id(id),
1094 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1096 |> put_view(AccountView)
1097 |> render("relationship.json", %{user: user, target: subscription_target})
1099 {:error, message} ->
1101 |> put_resp_content_type("application/json")
1102 |> send_resp(403, Jason.encode!(%{"error" => message}))
1106 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1107 with %User{} = subscription_target <- User.get_cached_by_id(id),
1108 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1110 |> put_view(AccountView)
1111 |> render("relationship.json", %{user: user, target: subscription_target})
1113 {:error, message} ->
1115 |> put_resp_content_type("application/json")
1116 |> send_resp(403, Jason.encode!(%{"error" => message}))
1120 def status_search_query_with_gin(q, query) do
1124 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1128 order_by: [desc: :id]
1132 def status_search_query_with_rum(q, query) do
1136 "? @@ plainto_tsquery('english', ?)",
1140 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1144 def status_search(user, query) do
1146 if Regex.match?(~r/https?:/, query) do
1147 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1148 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1149 true <- Visibility.visible_for_user?(activity, user) do
1157 from([a, o] in Activity.with_preloaded_object(Activity),
1158 where: fragment("?->>'type' = 'Create'", a.data),
1159 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1164 if Pleroma.Config.get([:database, :rum_enabled]) do
1165 status_search_query_with_rum(q, query)
1167 status_search_query_with_gin(q, query)
1170 Repo.all(q) ++ fetched
1173 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1174 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1176 statuses = status_search(user, query)
1178 tags_path = Web.base_url() <> "/tag/"
1184 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1185 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1186 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1189 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1191 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1198 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1199 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1201 statuses = status_search(user, query)
1207 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1208 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1211 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1213 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1220 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1221 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1223 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1228 def favourites(%{assigns: %{user: user}} = conn, params) do
1231 |> Map.put("type", "Create")
1232 |> Map.put("favorited_by", user.ap_id)
1233 |> Map.put("blocking_user", user)
1236 ActivityPub.fetch_activities([], params)
1240 |> add_link_headers(:favourites, activities)
1241 |> put_view(StatusView)
1242 |> render("index.json", %{activities: activities, for: user, as: :activity})
1245 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1246 with %User{} = user <- User.get_by_id(id),
1247 false <- user.info.hide_favorites do
1250 |> Map.put("type", "Create")
1251 |> Map.put("favorited_by", user.ap_id)
1252 |> Map.put("blocking_user", for_user)
1256 ["https://www.w3.org/ns/activitystreams#Public"] ++
1257 [for_user.ap_id | for_user.following]
1259 ["https://www.w3.org/ns/activitystreams#Public"]
1264 |> ActivityPub.fetch_activities(params)
1268 |> add_link_headers(:favourites, activities)
1269 |> put_view(StatusView)
1270 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1273 {:error, :not_found}
1278 |> json(%{error: "Can't get favorites"})
1282 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1283 user = User.get_cached_by_id(user.id)
1286 Bookmark.for_user_query(user.id)
1287 |> Pagination.fetch_paginated(params)
1291 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1294 |> add_link_headers(:bookmarks, bookmarks)
1295 |> put_view(StatusView)
1296 |> render("index.json", %{activities: activities, for: user, as: :activity})
1299 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1300 lists = Pleroma.List.for_user(user, opts)
1301 res = ListView.render("lists.json", lists: lists)
1305 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1306 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1307 res = ListView.render("list.json", list: list)
1313 |> json(%{error: "Record not found"})
1317 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1318 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1319 res = ListView.render("lists.json", lists: lists)
1323 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1324 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1325 {:ok, _list} <- Pleroma.List.delete(list) do
1333 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1334 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1335 res = ListView.render("list.json", list: list)
1340 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1342 |> Enum.each(fn account_id ->
1343 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1344 %User{} = followed <- User.get_cached_by_id(account_id) do
1345 Pleroma.List.follow(list, followed)
1352 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1354 |> Enum.each(fn account_id ->
1355 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1356 %User{} = followed <- User.get_cached_by_id(account_id) do
1357 Pleroma.List.unfollow(list, followed)
1364 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1365 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1366 {:ok, users} = Pleroma.List.get_following(list) do
1368 |> put_view(AccountView)
1369 |> render("accounts.json", %{for: user, users: users, as: :user})
1373 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1374 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1375 {:ok, list} <- Pleroma.List.rename(list, title) do
1376 res = ListView.render("list.json", list: list)
1384 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1385 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1388 |> Map.put("type", "Create")
1389 |> Map.put("blocking_user", user)
1390 |> Map.put("muting_user", user)
1392 # we must filter the following list for the user to avoid leaking statuses the user
1393 # does not actually have permission to see (for more info, peruse security issue #270).
1396 |> Enum.filter(fn x -> x in user.following end)
1397 |> ActivityPub.fetch_activities_bounded(following, params)
1401 |> put_view(StatusView)
1402 |> render("index.json", %{activities: activities, for: user, as: :activity})
1407 |> json(%{error: "Error."})
1411 def index(%{assigns: %{user: user}} = conn, _params) do
1412 token = get_session(conn, :oauth_token)
1415 mastodon_emoji = mastodonized_emoji()
1417 limit = Config.get([:instance, :limit])
1420 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1425 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1426 access_token: token,
1428 domain: Pleroma.Web.Endpoint.host(),
1431 unfollow_modal: false,
1434 auto_play_gif: false,
1435 display_sensitive_media: false,
1436 reduce_motion: false,
1437 max_toot_chars: limit,
1438 mascot: User.get_mascot(user)["url"]
1440 poll_limits: Config.get([:instance, :poll_limits]),
1442 delete_others_notice: present?(user.info.is_moderator),
1443 admin: present?(user.info.is_admin)
1447 default_privacy: user.info.default_scope,
1448 default_sensitive: false,
1449 allow_content_types: Config.get([:instance, :allowed_post_formats])
1451 media_attachments: %{
1452 accept_content_types: [
1468 user.info.settings ||
1498 push_subscription: nil,
1500 custom_emojis: mastodon_emoji,
1506 |> put_layout(false)
1507 |> put_view(MastodonView)
1508 |> render("index.html", %{initial_state: initial_state})
1511 |> put_session(:return_to, conn.request_path)
1512 |> redirect(to: "/web/login")
1516 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1517 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1519 with changeset <- Ecto.Changeset.change(user),
1520 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1521 {:ok, _user} <- User.update_and_set_cache(changeset) do
1526 |> put_resp_content_type("application/json")
1527 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1531 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1532 redirect(conn, to: local_mastodon_root_path(conn))
1535 @doc "Local Mastodon FE login init action"
1536 def login(conn, %{"code" => auth_token}) do
1537 with {:ok, app} <- get_or_make_app(),
1538 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1539 {:ok, token} <- Token.exchange_token(app, auth) do
1541 |> put_session(:oauth_token, token.token)
1542 |> redirect(to: local_mastodon_root_path(conn))
1546 @doc "Local Mastodon FE callback action"
1547 def login(conn, _) do
1548 with {:ok, app} <- get_or_make_app() do
1553 response_type: "code",
1554 client_id: app.client_id,
1556 scope: Enum.join(app.scopes, " ")
1559 redirect(conn, to: path)
1563 defp local_mastodon_root_path(conn) do
1564 case get_session(conn, :return_to) do
1566 mastodon_api_path(conn, :index, ["getting-started"])
1569 delete_session(conn, :return_to)
1574 defp get_or_make_app do
1575 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1576 scopes = ["read", "write", "follow", "push"]
1578 with %App{} = app <- Repo.get_by(App, find_attrs) do
1580 if app.scopes == scopes do
1584 |> Ecto.Changeset.change(%{scopes: scopes})
1592 App.register_changeset(
1594 Map.put(find_attrs, :scopes, scopes)
1601 def logout(conn, _) do
1604 |> redirect(to: "/")
1607 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1608 Logger.debug("Unimplemented, returning unmodified relationship")
1610 with %User{} = target <- User.get_cached_by_id(id) do
1612 |> put_view(AccountView)
1613 |> render("relationship.json", %{user: user, target: target})
1617 def empty_array(conn, _) do
1618 Logger.debug("Unimplemented, returning an empty array")
1622 def empty_object(conn, _) do
1623 Logger.debug("Unimplemented, returning an empty object")
1627 def get_filters(%{assigns: %{user: user}} = conn, _) do
1628 filters = Filter.get_filters(user)
1629 res = FilterView.render("filters.json", filters: filters)
1634 %{assigns: %{user: user}} = conn,
1635 %{"phrase" => phrase, "context" => context} = params
1641 hide: Map.get(params, "irreversible", false),
1642 whole_word: Map.get(params, "boolean", true)
1646 {:ok, response} = Filter.create(query)
1647 res = FilterView.render("filter.json", filter: response)
1651 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1652 filter = Filter.get(filter_id, user)
1653 res = FilterView.render("filter.json", filter: filter)
1658 %{assigns: %{user: user}} = conn,
1659 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1663 filter_id: filter_id,
1666 hide: Map.get(params, "irreversible", nil),
1667 whole_word: Map.get(params, "boolean", true)
1671 {:ok, response} = Filter.update(query)
1672 res = FilterView.render("filter.json", filter: response)
1676 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1679 filter_id: filter_id
1682 {:ok, _} = Filter.delete(query)
1688 def errors(conn, {:error, %Changeset{} = changeset}) do
1691 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1692 |> Enum.map_join(", ", fn {_k, v} -> v end)
1696 |> json(%{error: error_message})
1699 def errors(conn, {:error, :not_found}) do
1702 |> json(%{error: "Record not found"})
1705 def errors(conn, _) do
1708 |> json("Something went wrong")
1711 def suggestions(%{assigns: %{user: user}} = conn, _) do
1712 suggestions = Config.get(:suggestions)
1714 if Keyword.get(suggestions, :enabled, false) do
1715 api = Keyword.get(suggestions, :third_party_engine, "")
1716 timeout = Keyword.get(suggestions, :timeout, 5000)
1717 limit = Keyword.get(suggestions, :limit, 23)
1719 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1721 user = user.nickname
1725 |> String.replace("{{host}}", host)
1726 |> String.replace("{{user}}", user)
1728 with {:ok, %{status: 200, body: body}} <-
1733 recv_timeout: timeout,
1737 {:ok, data} <- Jason.decode(body) do
1740 |> Enum.slice(0, limit)
1745 case User.get_or_fetch(x["acct"]) do
1746 {:ok, %User{id: id}} -> id
1752 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1755 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1761 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1768 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1769 with %Activity{} = activity <- Activity.get_by_id(status_id),
1770 true <- Visibility.visible_for_user?(activity, user) do
1774 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1784 def reports(%{assigns: %{user: user}} = conn, params) do
1785 case CommonAPI.report(user, params) do
1788 |> put_view(ReportView)
1789 |> try_render("report.json", %{activity: activity})
1793 |> put_status(:bad_request)
1794 |> json(%{error: err})
1798 def account_register(
1799 %{assigns: %{app: app}} = conn,
1800 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1808 "captcha_answer_data",
1812 |> Map.put("nickname", nickname)
1813 |> Map.put("fullname", params["fullname"] || nickname)
1814 |> Map.put("bio", params["bio"] || "")
1815 |> Map.put("confirm", params["password"])
1817 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1818 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1820 token_type: "Bearer",
1821 access_token: token.token,
1823 created_at: Token.Utils.format_created_at(token)
1829 |> json(Jason.encode!(errors))
1833 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1836 |> json(%{error: "Missing parameters"})
1839 def account_register(conn, _) do
1842 |> json(%{error: "Invalid credentials"})
1845 def conversations(%{assigns: %{user: user}} = conn, params) do
1846 participations = Participation.for_user_with_last_activity_id(user, params)
1849 Enum.map(participations, fn participation ->
1850 ConversationView.render("participation.json", %{participation: participation, user: user})
1854 |> add_link_headers(:conversations, participations)
1855 |> json(conversations)
1858 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1859 with %Participation{} = participation <-
1860 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1861 {:ok, participation} <- Participation.mark_as_read(participation) do
1862 participation_view =
1863 ConversationView.render("participation.json", %{participation: participation, user: user})
1866 |> json(participation_view)
1870 def try_render(conn, target, params)
1871 when is_binary(target) do
1872 res = render(conn, target, params)
1877 |> json(%{error: "Can't display this activity"})
1883 def try_render(conn, _, _) do
1886 |> json(%{error: "Can't display this activity"})
1889 defp present?(nil), do: false
1890 defp present?(false), do: false
1891 defp present?(_), do: true