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
14 alias Pleroma.Notification
16 alias Pleroma.Object.Fetcher
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
50 Pleroma.Plugs.RateLimitPlug,
52 max_requests: Config.get([:app_account_creation, :max_requests]),
53 interval: Config.get([:app_account_creation, :interval])
55 when action in [:account_register]
58 @httpoison Application.get_env(:pleroma, :httpoison)
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, "header", :banner, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
135 |> Map.put(:emoji, user_info_emojis)
137 info_cng = User.Info.profile_update(user.info, info_params)
139 with changeset <- User.update_changeset(user, user_params),
140 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
141 {:ok, user} <- User.update_and_set_cache(changeset) do
142 if original_user != user do
143 CommonAPI.update(user)
146 json(conn, AccountView.render("account.json", %{user: user, for: user}))
151 |> json(%{error: "Invalid request"})
155 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
156 account = AccountView.render("account.json", %{user: user, for: user})
160 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
161 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
164 |> render("short.json", %{app: app})
168 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
169 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
170 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
171 account = AccountView.render("account.json", %{user: user, for: for_user})
177 |> json(%{error: "Can't find user"})
181 @mastodon_api_level "2.7.2"
183 def masto_instance(conn, _params) do
184 instance = Config.get(:instance)
188 title: Keyword.get(instance, :name),
189 description: Keyword.get(instance, :description),
190 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
191 email: Keyword.get(instance, :email),
193 streaming_api: Pleroma.Web.Endpoint.websocket_url()
195 stats: Stats.get_stats(),
196 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
198 registrations: Pleroma.Config.get([:instance, :registrations_open]),
199 # Extra (not present in Mastodon):
200 max_toot_chars: Keyword.get(instance, :limit)
206 def peers(conn, _params) do
207 json(conn, Stats.get_peers())
210 defp mastodonized_emoji do
211 Pleroma.Emoji.get_all()
212 |> Enum.map(fn {shortcode, relative_url, tags} ->
213 url = to_string(URI.merge(Web.base_url(), relative_url))
216 "shortcode" => shortcode,
218 "visible_in_picker" => true,
225 def custom_emojis(conn, _params) do
226 mastodon_emoji = mastodonized_emoji()
227 json(conn, mastodon_emoji)
230 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
233 |> Map.drop(["since_id", "max_id", "min_id"])
236 last = List.last(activities)
243 |> Map.get("limit", "20")
244 |> String.to_integer()
247 if length(activities) <= limit do
253 |> Enum.at(limit * -1)
257 {next_url, prev_url} =
261 Pleroma.Web.Endpoint,
264 Map.merge(params, %{max_id: max_id})
267 Pleroma.Web.Endpoint,
270 Map.merge(params, %{min_id: min_id})
276 Pleroma.Web.Endpoint,
278 Map.merge(params, %{max_id: max_id})
281 Pleroma.Web.Endpoint,
283 Map.merge(params, %{min_id: min_id})
289 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
295 def home_timeline(%{assigns: %{user: user}} = conn, params) do
298 |> Map.put("type", ["Create", "Announce"])
299 |> Map.put("blocking_user", user)
300 |> Map.put("muting_user", user)
301 |> Map.put("user", user)
304 [user.ap_id | user.following]
305 |> ActivityPub.fetch_activities(params)
309 |> add_link_headers(:home_timeline, activities)
310 |> put_view(StatusView)
311 |> render("index.json", %{activities: activities, for: user, as: :activity})
314 def public_timeline(%{assigns: %{user: user}} = conn, params) do
315 local_only = params["local"] in [true, "True", "true", "1"]
319 |> Map.put("type", ["Create", "Announce"])
320 |> Map.put("local_only", local_only)
321 |> Map.put("blocking_user", user)
322 |> Map.put("muting_user", user)
323 |> ActivityPub.fetch_public_activities()
327 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
328 |> put_view(StatusView)
329 |> render("index.json", %{activities: activities, for: user, as: :activity})
332 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
333 with %User{} = user <- User.get_cached_by_id(params["id"]) do
334 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
337 |> add_link_headers(:user_statuses, activities, params["id"])
338 |> put_view(StatusView)
339 |> render("index.json", %{
340 activities: activities,
347 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
350 |> Map.put("type", "Create")
351 |> Map.put("blocking_user", user)
352 |> Map.put("user", user)
353 |> Map.put(:visibility, "direct")
357 |> ActivityPub.fetch_activities_query(params)
358 |> Pagination.fetch_paginated(params)
361 |> add_link_headers(:dm_timeline, activities)
362 |> put_view(StatusView)
363 |> render("index.json", %{activities: activities, for: user, as: :activity})
366 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
367 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
368 true <- Visibility.visible_for_user?(activity, user) do
370 |> put_view(StatusView)
371 |> try_render("status.json", %{activity: activity, for: user})
375 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
376 with %Activity{} = activity <- Activity.get_by_id(id),
378 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
379 "blocking_user" => user,
383 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
385 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
386 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
392 activities: grouped_activities[true] || [],
396 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
401 activities: grouped_activities[false] || [],
405 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
412 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
413 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
415 |> add_link_headers(:scheduled_statuses, scheduled_activities)
416 |> put_view(ScheduledActivityView)
417 |> render("index.json", %{scheduled_activities: scheduled_activities})
421 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
422 with %ScheduledActivity{} = scheduled_activity <-
423 ScheduledActivity.get(user, scheduled_activity_id) do
425 |> put_view(ScheduledActivityView)
426 |> render("show.json", %{scheduled_activity: scheduled_activity})
428 _ -> {:error, :not_found}
432 def update_scheduled_status(
433 %{assigns: %{user: user}} = conn,
434 %{"id" => scheduled_activity_id} = params
436 with %ScheduledActivity{} = scheduled_activity <-
437 ScheduledActivity.get(user, scheduled_activity_id),
438 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
440 |> put_view(ScheduledActivityView)
441 |> render("show.json", %{scheduled_activity: scheduled_activity})
443 nil -> {:error, :not_found}
448 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
449 with %ScheduledActivity{} = scheduled_activity <-
450 ScheduledActivity.get(user, scheduled_activity_id),
451 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
453 |> put_view(ScheduledActivityView)
454 |> render("show.json", %{scheduled_activity: scheduled_activity})
456 nil -> {:error, :not_found}
461 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
462 when length(media_ids) > 0 do
465 |> Map.put("status", ".")
467 post_status(conn, params)
470 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
473 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
476 case get_req_header(conn, "idempotency-key") do
478 _ -> Ecto.UUID.generate()
481 scheduled_at = params["scheduled_at"]
483 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
484 with {:ok, scheduled_activity} <-
485 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
487 |> put_view(ScheduledActivityView)
488 |> render("show.json", %{scheduled_activity: scheduled_activity})
491 params = Map.drop(params, ["scheduled_at"])
494 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
495 CommonAPI.post(user, params)
499 |> put_view(StatusView)
500 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
504 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
505 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
511 |> json(%{error: "Can't delete this post"})
515 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
517 %Activity{} = announce <- Activity.normalize(announce.data) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
524 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
526 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
528 |> put_view(StatusView)
529 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
533 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
534 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
535 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
537 |> put_view(StatusView)
538 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
542 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
543 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
544 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
546 |> put_view(StatusView)
547 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
551 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
552 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
554 |> put_view(StatusView)
555 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
559 |> put_resp_content_type("application/json")
560 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
564 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
565 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
567 |> put_view(StatusView)
568 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
572 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
573 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
574 %User{} = user <- User.get_cached_by_nickname(user.nickname),
575 true <- Visibility.visible_for_user?(activity, user),
576 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
578 |> put_view(StatusView)
579 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
583 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
584 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
585 %User{} = user <- User.get_cached_by_nickname(user.nickname),
586 true <- Visibility.visible_for_user?(activity, user),
587 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
589 |> put_view(StatusView)
590 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
594 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
595 activity = Activity.get_by_id(id)
597 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
599 |> put_view(StatusView)
600 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
604 |> put_resp_content_type("application/json")
605 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
609 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
610 activity = Activity.get_by_id(id)
612 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
614 |> put_view(StatusView)
615 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
619 def notifications(%{assigns: %{user: user}} = conn, params) do
620 notifications = MastodonAPI.get_notifications(user, params)
623 |> add_link_headers(:notifications, notifications)
624 |> put_view(NotificationView)
625 |> render("index.json", %{notifications: notifications, for: user})
628 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
629 with {:ok, notification} <- Notification.get(user, id) do
631 |> put_view(NotificationView)
632 |> render("show.json", %{notification: notification, for: user})
636 |> put_resp_content_type("application/json")
637 |> send_resp(403, Jason.encode!(%{"error" => reason}))
641 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
642 Notification.clear(user)
646 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
647 with {:ok, _notif} <- Notification.dismiss(user, id) do
652 |> put_resp_content_type("application/json")
653 |> send_resp(403, Jason.encode!(%{"error" => reason}))
657 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
658 Notification.destroy_multiple(user, ids)
662 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
664 q = from(u in User, where: u.id in ^id)
665 targets = Repo.all(q)
668 |> put_view(AccountView)
669 |> render("relationships.json", %{user: user, targets: targets})
672 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
673 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
675 def update_media(%{assigns: %{user: user}} = conn, data) do
676 with %Object{} = object <- Repo.get(Object, data["id"]),
677 true <- Object.authorize_mutation(object, user),
678 true <- is_binary(data["description"]),
679 description <- data["description"] do
680 new_data = %{object.data | "name" => description}
684 |> Object.change(%{data: new_data})
687 attachment_data = Map.put(new_data, "id", object.id)
690 |> put_view(StatusView)
691 |> render("attachment.json", %{attachment: attachment_data})
695 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
696 with {:ok, object} <-
699 actor: User.ap_id(user),
700 description: Map.get(data, "description")
702 attachment_data = Map.put(object.data, "id", object.id)
705 |> put_view(StatusView)
706 |> render("attachment.json", %{attachment: attachment_data})
710 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
711 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
712 %{} = attachment_data <- Map.put(object.data, "id", object.id),
713 %{type: type} = rendered <-
714 StatusView.render("attachment.json", %{attachment: attachment_data}) do
715 # Reject if not an image
716 if type == "image" do
718 # Save to the user's info
719 info_changeset = User.Info.mascot_update(user.info, rendered)
723 |> Ecto.Changeset.change()
724 |> Ecto.Changeset.put_embed(:info, info_changeset)
726 {:ok, _user} = User.update_and_set_cache(user_changeset)
732 |> put_resp_content_type("application/json")
733 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
738 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
739 mascot = User.get_mascot(user)
745 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
746 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
747 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
748 q = from(u in User, where: u.ap_id in ^likes)
752 |> put_view(AccountView)
753 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
759 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
760 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
761 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
762 q = from(u in User, where: u.ap_id in ^announces)
766 |> put_view(AccountView)
767 |> render("accounts.json", %{for: user, users: users, as: :user})
773 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
774 local_only = params["local"] in [true, "True", "true", "1"]
777 [params["tag"], params["any"]]
781 |> Enum.map(&String.downcase(&1))
786 |> Enum.map(&String.downcase(&1))
791 |> Enum.map(&String.downcase(&1))
795 |> Map.put("type", "Create")
796 |> Map.put("local_only", local_only)
797 |> Map.put("blocking_user", user)
798 |> Map.put("muting_user", user)
799 |> Map.put("tag", tags)
800 |> Map.put("tag_all", tag_all)
801 |> Map.put("tag_reject", tag_reject)
802 |> ActivityPub.fetch_public_activities()
806 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
807 |> put_view(StatusView)
808 |> render("index.json", %{activities: activities, for: user, as: :activity})
811 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
812 with %User{} = user <- User.get_cached_by_id(id),
813 followers <- MastodonAPI.get_followers(user, params) do
816 for_user && user.id == for_user.id -> followers
817 user.info.hide_followers -> []
822 |> add_link_headers(:followers, followers, user)
823 |> put_view(AccountView)
824 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
828 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
829 with %User{} = user <- User.get_cached_by_id(id),
830 followers <- MastodonAPI.get_friends(user, params) do
833 for_user && user.id == for_user.id -> followers
834 user.info.hide_follows -> []
839 |> add_link_headers(:following, followers, user)
840 |> put_view(AccountView)
841 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
845 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
846 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
848 |> put_view(AccountView)
849 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
853 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
854 with %User{} = follower <- User.get_cached_by_id(id),
855 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
857 |> put_view(AccountView)
858 |> render("relationship.json", %{user: followed, target: follower})
862 |> put_resp_content_type("application/json")
863 |> send_resp(403, Jason.encode!(%{"error" => message}))
867 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
868 with %User{} = follower <- User.get_cached_by_id(id),
869 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
871 |> put_view(AccountView)
872 |> render("relationship.json", %{user: followed, target: follower})
876 |> put_resp_content_type("application/json")
877 |> send_resp(403, Jason.encode!(%{"error" => message}))
881 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
882 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
883 {_, true} <- {:followed, follower.id != followed.id},
884 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
886 |> put_view(AccountView)
887 |> render("relationship.json", %{user: follower, target: followed})
894 |> put_resp_content_type("application/json")
895 |> send_resp(403, Jason.encode!(%{"error" => message}))
899 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
900 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
901 {_, true} <- {:followed, follower.id != followed.id},
902 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
904 |> put_view(AccountView)
905 |> render("account.json", %{user: followed, for: follower})
912 |> put_resp_content_type("application/json")
913 |> send_resp(403, Jason.encode!(%{"error" => message}))
917 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
918 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
919 {_, true} <- {:followed, follower.id != followed.id},
920 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
922 |> put_view(AccountView)
923 |> render("relationship.json", %{user: follower, target: followed})
933 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
934 with %User{} = muted <- User.get_cached_by_id(id),
935 {:ok, muter} <- User.mute(muter, muted) do
937 |> put_view(AccountView)
938 |> render("relationship.json", %{user: muter, target: muted})
942 |> put_resp_content_type("application/json")
943 |> send_resp(403, Jason.encode!(%{"error" => message}))
947 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
948 with %User{} = muted <- User.get_cached_by_id(id),
949 {:ok, muter} <- User.unmute(muter, muted) do
951 |> put_view(AccountView)
952 |> render("relationship.json", %{user: muter, target: muted})
956 |> put_resp_content_type("application/json")
957 |> send_resp(403, Jason.encode!(%{"error" => message}))
961 def mutes(%{assigns: %{user: user}} = conn, _) do
962 with muted_accounts <- User.muted_users(user) do
963 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
968 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
969 with %User{} = blocked <- User.get_cached_by_id(id),
970 {:ok, blocker} <- User.block(blocker, blocked),
971 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
973 |> put_view(AccountView)
974 |> render("relationship.json", %{user: blocker, target: blocked})
978 |> put_resp_content_type("application/json")
979 |> send_resp(403, Jason.encode!(%{"error" => message}))
983 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
984 with %User{} = blocked <- User.get_cached_by_id(id),
985 {:ok, blocker} <- User.unblock(blocker, blocked),
986 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
988 |> put_view(AccountView)
989 |> render("relationship.json", %{user: blocker, target: blocked})
993 |> put_resp_content_type("application/json")
994 |> send_resp(403, Jason.encode!(%{"error" => message}))
998 def blocks(%{assigns: %{user: user}} = conn, _) do
999 with blocked_accounts <- User.blocked_users(user) do
1000 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1005 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1006 json(conn, info.domain_blocks || [])
1009 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1010 User.block_domain(blocker, domain)
1014 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1015 User.unblock_domain(blocker, domain)
1019 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1020 with %User{} = subscription_target <- User.get_cached_by_id(id),
1021 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1023 |> put_view(AccountView)
1024 |> render("relationship.json", %{user: user, target: subscription_target})
1026 {:error, message} ->
1028 |> put_resp_content_type("application/json")
1029 |> send_resp(403, Jason.encode!(%{"error" => message}))
1033 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1034 with %User{} = subscription_target <- User.get_cached_by_id(id),
1035 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1037 |> put_view(AccountView)
1038 |> render("relationship.json", %{user: user, target: subscription_target})
1040 {:error, message} ->
1042 |> put_resp_content_type("application/json")
1043 |> send_resp(403, Jason.encode!(%{"error" => message}))
1047 def status_search_query_with_gin(q, query) do
1051 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1055 order_by: [desc: :id]
1059 def status_search_query_with_rum(q, query) do
1063 "? @@ plainto_tsquery('english', ?)",
1067 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1071 def status_search(user, query) do
1073 if Regex.match?(~r/https?:/, query) do
1074 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1075 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1076 true <- Visibility.visible_for_user?(activity, user) do
1084 from([a, o] in Activity.with_preloaded_object(Activity),
1085 where: fragment("?->>'type' = 'Create'", a.data),
1086 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1091 if Pleroma.Config.get([:database, :rum_enabled]) do
1092 status_search_query_with_rum(q, query)
1094 status_search_query_with_gin(q, query)
1097 Repo.all(q) ++ fetched
1100 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1101 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1103 statuses = status_search(user, query)
1105 tags_path = Web.base_url() <> "/tag/"
1111 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1112 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1113 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1116 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1118 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1125 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1126 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1128 statuses = status_search(user, query)
1134 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1135 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1138 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1140 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1147 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1148 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1150 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1155 def favourites(%{assigns: %{user: user}} = conn, params) do
1158 |> Map.put("type", "Create")
1159 |> Map.put("favorited_by", user.ap_id)
1160 |> Map.put("blocking_user", user)
1163 ActivityPub.fetch_activities([], params)
1167 |> add_link_headers(:favourites, activities)
1168 |> put_view(StatusView)
1169 |> render("index.json", %{activities: activities, for: user, as: :activity})
1172 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1173 with %User{} = user <- User.get_by_id(id),
1174 false <- user.info.hide_favorites do
1177 |> Map.put("type", "Create")
1178 |> Map.put("favorited_by", user.ap_id)
1179 |> Map.put("blocking_user", for_user)
1183 ["https://www.w3.org/ns/activitystreams#Public"] ++
1184 [for_user.ap_id | for_user.following]
1186 ["https://www.w3.org/ns/activitystreams#Public"]
1191 |> ActivityPub.fetch_activities(params)
1195 |> add_link_headers(:favourites, activities)
1196 |> put_view(StatusView)
1197 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1200 {:error, :not_found}
1205 |> json(%{error: "Can't get favorites"})
1209 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1210 user = User.get_cached_by_id(user.id)
1213 Bookmark.for_user_query(user.id)
1214 |> Pagination.fetch_paginated(params)
1218 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1221 |> add_link_headers(:bookmarks, bookmarks)
1222 |> put_view(StatusView)
1223 |> render("index.json", %{activities: activities, for: user, as: :activity})
1226 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1227 lists = Pleroma.List.for_user(user, opts)
1228 res = ListView.render("lists.json", lists: lists)
1232 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1233 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1234 res = ListView.render("list.json", list: list)
1240 |> json(%{error: "Record not found"})
1244 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1245 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1246 res = ListView.render("lists.json", lists: lists)
1250 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1251 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1252 {:ok, _list} <- Pleroma.List.delete(list) do
1260 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1261 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1262 res = ListView.render("list.json", list: list)
1267 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1269 |> Enum.each(fn account_id ->
1270 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1271 %User{} = followed <- User.get_cached_by_id(account_id) do
1272 Pleroma.List.follow(list, followed)
1279 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1281 |> Enum.each(fn account_id ->
1282 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1283 %User{} = followed <- User.get_cached_by_id(account_id) do
1284 Pleroma.List.unfollow(list, followed)
1291 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1292 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1293 {:ok, users} = Pleroma.List.get_following(list) do
1295 |> put_view(AccountView)
1296 |> render("accounts.json", %{for: user, users: users, as: :user})
1300 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1301 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1302 {:ok, list} <- Pleroma.List.rename(list, title) do
1303 res = ListView.render("list.json", list: list)
1311 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1312 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1315 |> Map.put("type", "Create")
1316 |> Map.put("blocking_user", user)
1317 |> Map.put("muting_user", user)
1319 # we must filter the following list for the user to avoid leaking statuses the user
1320 # does not actually have permission to see (for more info, peruse security issue #270).
1323 |> Enum.filter(fn x -> x in user.following end)
1324 |> ActivityPub.fetch_activities_bounded(following, params)
1328 |> put_view(StatusView)
1329 |> render("index.json", %{activities: activities, for: user, as: :activity})
1334 |> json(%{error: "Error."})
1338 def index(%{assigns: %{user: user}} = conn, _params) do
1339 token = get_session(conn, :oauth_token)
1342 mastodon_emoji = mastodonized_emoji()
1344 limit = Config.get([:instance, :limit])
1347 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1349 flavour = get_user_flavour(user)
1354 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1355 access_token: token,
1357 domain: Pleroma.Web.Endpoint.host(),
1360 unfollow_modal: false,
1363 auto_play_gif: false,
1364 display_sensitive_media: false,
1365 reduce_motion: false,
1366 max_toot_chars: limit,
1367 mascot: User.get_mascot(user)["url"]
1370 delete_others_notice: present?(user.info.is_moderator),
1371 admin: present?(user.info.is_admin)
1375 default_privacy: user.info.default_scope,
1376 default_sensitive: false,
1377 allow_content_types: Config.get([:instance, :allowed_post_formats])
1379 media_attachments: %{
1380 accept_content_types: [
1396 user.info.settings ||
1426 push_subscription: nil,
1428 custom_emojis: mastodon_emoji,
1434 |> put_layout(false)
1435 |> put_view(MastodonView)
1436 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1439 |> put_session(:return_to, conn.request_path)
1440 |> redirect(to: "/web/login")
1444 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1445 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1447 with changeset <- Ecto.Changeset.change(user),
1448 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1449 {:ok, _user} <- User.update_and_set_cache(changeset) do
1454 |> put_resp_content_type("application/json")
1455 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1459 @supported_flavours ["glitch", "vanilla"]
1461 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1462 when flavour in @supported_flavours do
1463 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1465 with changeset <- Ecto.Changeset.change(user),
1466 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1467 {:ok, user} <- User.update_and_set_cache(changeset),
1468 flavour <- user.info.flavour do
1473 |> put_resp_content_type("application/json")
1474 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1478 def set_flavour(conn, _params) do
1481 |> json(%{error: "Unsupported flavour"})
1484 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1485 json(conn, get_user_flavour(user))
1488 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1492 defp get_user_flavour(_) do
1496 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1497 redirect(conn, to: local_mastodon_root_path(conn))
1500 @doc "Local Mastodon FE login init action"
1501 def login(conn, %{"code" => auth_token}) do
1502 with {:ok, app} <- get_or_make_app(),
1503 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1504 {:ok, token} <- Token.exchange_token(app, auth) do
1506 |> put_session(:oauth_token, token.token)
1507 |> redirect(to: local_mastodon_root_path(conn))
1511 @doc "Local Mastodon FE callback action"
1512 def login(conn, _) do
1513 with {:ok, app} <- get_or_make_app() do
1518 response_type: "code",
1519 client_id: app.client_id,
1521 scope: Enum.join(app.scopes, " ")
1524 redirect(conn, to: path)
1528 defp local_mastodon_root_path(conn) do
1529 case get_session(conn, :return_to) do
1531 mastodon_api_path(conn, :index, ["getting-started"])
1534 delete_session(conn, :return_to)
1539 defp get_or_make_app do
1540 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1541 scopes = ["read", "write", "follow", "push"]
1543 with %App{} = app <- Repo.get_by(App, find_attrs) do
1545 if app.scopes == scopes do
1549 |> Ecto.Changeset.change(%{scopes: scopes})
1557 App.register_changeset(
1559 Map.put(find_attrs, :scopes, scopes)
1566 def logout(conn, _) do
1569 |> redirect(to: "/")
1572 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1573 Logger.debug("Unimplemented, returning unmodified relationship")
1575 with %User{} = target <- User.get_cached_by_id(id) do
1577 |> put_view(AccountView)
1578 |> render("relationship.json", %{user: user, target: target})
1582 def empty_array(conn, _) do
1583 Logger.debug("Unimplemented, returning an empty array")
1587 def empty_object(conn, _) do
1588 Logger.debug("Unimplemented, returning an empty object")
1592 def get_filters(%{assigns: %{user: user}} = conn, _) do
1593 filters = Filter.get_filters(user)
1594 res = FilterView.render("filters.json", filters: filters)
1599 %{assigns: %{user: user}} = conn,
1600 %{"phrase" => phrase, "context" => context} = params
1606 hide: Map.get(params, "irreversible", false),
1607 whole_word: Map.get(params, "boolean", true)
1611 {:ok, response} = Filter.create(query)
1612 res = FilterView.render("filter.json", filter: response)
1616 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1617 filter = Filter.get(filter_id, user)
1618 res = FilterView.render("filter.json", filter: filter)
1623 %{assigns: %{user: user}} = conn,
1624 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1628 filter_id: filter_id,
1631 hide: Map.get(params, "irreversible", nil),
1632 whole_word: Map.get(params, "boolean", true)
1636 {:ok, response} = Filter.update(query)
1637 res = FilterView.render("filter.json", filter: response)
1641 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1644 filter_id: filter_id
1647 {:ok, _} = Filter.delete(query)
1653 def errors(conn, {:error, %Changeset{} = changeset}) do
1656 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1657 |> Enum.map_join(", ", fn {_k, v} -> v end)
1661 |> json(%{error: error_message})
1664 def errors(conn, {:error, :not_found}) do
1667 |> json(%{error: "Record not found"})
1670 def errors(conn, _) do
1673 |> json("Something went wrong")
1676 def suggestions(%{assigns: %{user: user}} = conn, _) do
1677 suggestions = Config.get(:suggestions)
1679 if Keyword.get(suggestions, :enabled, false) do
1680 api = Keyword.get(suggestions, :third_party_engine, "")
1681 timeout = Keyword.get(suggestions, :timeout, 5000)
1682 limit = Keyword.get(suggestions, :limit, 23)
1684 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1686 user = user.nickname
1690 |> String.replace("{{host}}", host)
1691 |> String.replace("{{user}}", user)
1693 with {:ok, %{status: 200, body: body}} <-
1698 recv_timeout: timeout,
1702 {:ok, data} <- Jason.decode(body) do
1705 |> Enum.slice(0, limit)
1710 case User.get_or_fetch(x["acct"]) do
1711 {:ok, %User{id: id}} -> id
1717 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1720 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1726 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1733 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1734 with %Activity{} = activity <- Activity.get_by_id(status_id),
1735 true <- Visibility.visible_for_user?(activity, user) do
1739 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1749 def reports(%{assigns: %{user: user}} = conn, params) do
1750 case CommonAPI.report(user, params) do
1753 |> put_view(ReportView)
1754 |> try_render("report.json", %{activity: activity})
1758 |> put_status(:bad_request)
1759 |> json(%{error: err})
1763 def account_register(
1764 %{assigns: %{app: app}} = conn,
1765 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1773 "captcha_answer_data",
1777 |> Map.put("nickname", nickname)
1778 |> Map.put("fullname", params["fullname"] || nickname)
1779 |> Map.put("bio", params["bio"] || "")
1780 |> Map.put("confirm", params["password"])
1782 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1783 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1785 token_type: "Bearer",
1786 access_token: token.token,
1788 created_at: Token.Utils.format_created_at(token)
1794 |> json(Jason.encode!(errors))
1798 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1801 |> json(%{error: "Missing parameters"})
1804 def account_register(conn, _) do
1807 |> json(%{error: "Invalid credentials"})
1810 def conversations(%{assigns: %{user: user}} = conn, params) do
1811 participations = Participation.for_user_with_last_activity_id(user, params)
1814 Enum.map(participations, fn participation ->
1815 ConversationView.render("participation.json", %{participation: participation, user: user})
1819 |> add_link_headers(:conversations, participations)
1820 |> json(conversations)
1823 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1824 with %Participation{} = participation <-
1825 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1826 {:ok, participation} <- Participation.mark_as_read(participation) do
1827 participation_view =
1828 ConversationView.render("participation.json", %{participation: participation, user: user})
1831 |> json(participation_view)
1835 def try_render(conn, target, params)
1836 when is_binary(target) do
1837 res = render(conn, target, params)
1842 |> json(%{error: "Can't display this activity"})
1848 def try_render(conn, _, _) do
1851 |> json(%{error: "Can't display this activity"})
1854 defp present?(nil), do: false
1855 defp present?(false), do: false
1856 defp present?(_), do: true