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 favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
711 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
712 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
713 q = from(u in User, where: u.ap_id in ^likes)
717 |> put_view(AccountView)
718 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
724 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
725 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
726 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
727 q = from(u in User, where: u.ap_id in ^announces)
731 |> put_view(AccountView)
732 |> render("accounts.json", %{for: user, users: users, as: :user})
738 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
739 local_only = params["local"] in [true, "True", "true", "1"]
742 [params["tag"], params["any"]]
746 |> Enum.map(&String.downcase(&1))
751 |> Enum.map(&String.downcase(&1))
756 |> Enum.map(&String.downcase(&1))
760 |> Map.put("type", "Create")
761 |> Map.put("local_only", local_only)
762 |> Map.put("blocking_user", user)
763 |> Map.put("muting_user", user)
764 |> Map.put("tag", tags)
765 |> Map.put("tag_all", tag_all)
766 |> Map.put("tag_reject", tag_reject)
767 |> ActivityPub.fetch_public_activities()
771 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
772 |> put_view(StatusView)
773 |> render("index.json", %{activities: activities, for: user, as: :activity})
776 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
777 with %User{} = user <- User.get_cached_by_id(id),
778 followers <- MastodonAPI.get_followers(user, params) do
781 for_user && user.id == for_user.id -> followers
782 user.info.hide_followers -> []
787 |> add_link_headers(:followers, followers, user)
788 |> put_view(AccountView)
789 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
793 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
794 with %User{} = user <- User.get_cached_by_id(id),
795 followers <- MastodonAPI.get_friends(user, params) do
798 for_user && user.id == for_user.id -> followers
799 user.info.hide_follows -> []
804 |> add_link_headers(:following, followers, user)
805 |> put_view(AccountView)
806 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
810 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
811 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
813 |> put_view(AccountView)
814 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
818 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
819 with %User{} = follower <- User.get_cached_by_id(id),
820 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
822 |> put_view(AccountView)
823 |> render("relationship.json", %{user: followed, target: follower})
827 |> put_resp_content_type("application/json")
828 |> send_resp(403, Jason.encode!(%{"error" => message}))
832 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
833 with %User{} = follower <- User.get_cached_by_id(id),
834 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
836 |> put_view(AccountView)
837 |> render("relationship.json", %{user: followed, target: follower})
841 |> put_resp_content_type("application/json")
842 |> send_resp(403, Jason.encode!(%{"error" => message}))
846 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
847 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
848 {_, true} <- {:followed, follower.id != followed.id},
849 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
851 |> put_view(AccountView)
852 |> render("relationship.json", %{user: follower, target: followed})
859 |> put_resp_content_type("application/json")
860 |> send_resp(403, Jason.encode!(%{"error" => message}))
864 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
865 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
866 {_, true} <- {:followed, follower.id != followed.id},
867 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
869 |> put_view(AccountView)
870 |> render("account.json", %{user: followed, for: follower})
877 |> put_resp_content_type("application/json")
878 |> send_resp(403, Jason.encode!(%{"error" => message}))
882 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
883 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
884 {_, true} <- {:followed, follower.id != followed.id},
885 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
887 |> put_view(AccountView)
888 |> render("relationship.json", %{user: follower, target: followed})
898 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
899 with %User{} = muted <- User.get_cached_by_id(id),
900 {:ok, muter} <- User.mute(muter, muted) do
902 |> put_view(AccountView)
903 |> render("relationship.json", %{user: muter, target: muted})
907 |> put_resp_content_type("application/json")
908 |> send_resp(403, Jason.encode!(%{"error" => message}))
912 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
913 with %User{} = muted <- User.get_cached_by_id(id),
914 {:ok, muter} <- User.unmute(muter, muted) do
916 |> put_view(AccountView)
917 |> render("relationship.json", %{user: muter, target: muted})
921 |> put_resp_content_type("application/json")
922 |> send_resp(403, Jason.encode!(%{"error" => message}))
926 def mutes(%{assigns: %{user: user}} = conn, _) do
927 with muted_accounts <- User.muted_users(user) do
928 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
933 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
934 with %User{} = blocked <- User.get_cached_by_id(id),
935 {:ok, blocker} <- User.block(blocker, blocked),
936 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: blocker, target: blocked})
943 |> put_resp_content_type("application/json")
944 |> send_resp(403, Jason.encode!(%{"error" => message}))
948 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
949 with %User{} = blocked <- User.get_cached_by_id(id),
950 {:ok, blocker} <- User.unblock(blocker, blocked),
951 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
953 |> put_view(AccountView)
954 |> render("relationship.json", %{user: blocker, target: blocked})
958 |> put_resp_content_type("application/json")
959 |> send_resp(403, Jason.encode!(%{"error" => message}))
963 def blocks(%{assigns: %{user: user}} = conn, _) do
964 with blocked_accounts <- User.blocked_users(user) do
965 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
970 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
971 json(conn, info.domain_blocks || [])
974 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
975 User.block_domain(blocker, domain)
979 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
980 User.unblock_domain(blocker, domain)
984 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
985 with %User{} = subscription_target <- User.get_cached_by_id(id),
986 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
988 |> put_view(AccountView)
989 |> render("relationship.json", %{user: user, target: subscription_target})
993 |> put_resp_content_type("application/json")
994 |> send_resp(403, Jason.encode!(%{"error" => message}))
998 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
999 with %User{} = subscription_target <- User.get_cached_by_id(id),
1000 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1002 |> put_view(AccountView)
1003 |> render("relationship.json", %{user: user, target: subscription_target})
1005 {:error, message} ->
1007 |> put_resp_content_type("application/json")
1008 |> send_resp(403, Jason.encode!(%{"error" => message}))
1012 def status_search(user, query) do
1014 if Regex.match?(~r/https?:/, query) do
1015 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1016 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1017 true <- Visibility.visible_for_user?(activity, user) do
1026 [a, o] in Activity.with_preloaded_object(Activity),
1027 where: fragment("?->>'type' = 'Create'", a.data),
1028 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1031 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1036 order_by: [desc: :id]
1039 Repo.all(q) ++ fetched
1042 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1043 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1045 statuses = status_search(user, query)
1047 tags_path = Web.base_url() <> "/tag/"
1053 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1054 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1055 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1058 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1060 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1067 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1068 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1070 statuses = status_search(user, query)
1076 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1077 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1080 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1082 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1089 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1090 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1092 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1097 def favourites(%{assigns: %{user: user}} = conn, params) do
1100 |> Map.put("type", "Create")
1101 |> Map.put("favorited_by", user.ap_id)
1102 |> Map.put("blocking_user", user)
1105 ActivityPub.fetch_activities([], params)
1109 |> add_link_headers(:favourites, activities)
1110 |> put_view(StatusView)
1111 |> render("index.json", %{activities: activities, for: user, as: :activity})
1114 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1115 with %User{} = user <- User.get_by_id(id),
1116 false <- user.info.hide_favorites do
1119 |> Map.put("type", "Create")
1120 |> Map.put("favorited_by", user.ap_id)
1121 |> Map.put("blocking_user", for_user)
1125 ["https://www.w3.org/ns/activitystreams#Public"] ++
1126 [for_user.ap_id | for_user.following]
1128 ["https://www.w3.org/ns/activitystreams#Public"]
1133 |> ActivityPub.fetch_activities(params)
1137 |> add_link_headers(:favourites, activities)
1138 |> put_view(StatusView)
1139 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1142 {:error, :not_found}
1147 |> json(%{error: "Can't get favorites"})
1151 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1152 user = User.get_cached_by_id(user.id)
1155 Bookmark.for_user_query(user.id)
1156 |> Pagination.fetch_paginated(params)
1160 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1163 |> add_link_headers(:bookmarks, bookmarks)
1164 |> put_view(StatusView)
1165 |> render("index.json", %{activities: activities, for: user, as: :activity})
1168 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1169 lists = Pleroma.List.for_user(user, opts)
1170 res = ListView.render("lists.json", lists: lists)
1174 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1175 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1176 res = ListView.render("list.json", list: list)
1182 |> json(%{error: "Record not found"})
1186 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1187 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1188 res = ListView.render("lists.json", lists: lists)
1192 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1193 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1194 {:ok, _list} <- Pleroma.List.delete(list) do
1202 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1203 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1204 res = ListView.render("list.json", list: list)
1209 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1211 |> Enum.each(fn account_id ->
1212 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1213 %User{} = followed <- User.get_cached_by_id(account_id) do
1214 Pleroma.List.follow(list, followed)
1221 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1223 |> Enum.each(fn account_id ->
1224 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1225 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1226 Pleroma.List.unfollow(list, followed)
1233 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1234 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1235 {:ok, users} = Pleroma.List.get_following(list) do
1237 |> put_view(AccountView)
1238 |> render("accounts.json", %{for: user, users: users, as: :user})
1242 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1243 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1244 {:ok, list} <- Pleroma.List.rename(list, title) do
1245 res = ListView.render("list.json", list: list)
1253 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1254 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1257 |> Map.put("type", "Create")
1258 |> Map.put("blocking_user", user)
1259 |> Map.put("muting_user", user)
1261 # we must filter the following list for the user to avoid leaking statuses the user
1262 # does not actually have permission to see (for more info, peruse security issue #270).
1265 |> Enum.filter(fn x -> x in user.following end)
1266 |> ActivityPub.fetch_activities_bounded(following, params)
1270 |> put_view(StatusView)
1271 |> render("index.json", %{activities: activities, for: user, as: :activity})
1276 |> json(%{error: "Error."})
1280 def index(%{assigns: %{user: user}} = conn, _params) do
1281 token = get_session(conn, :oauth_token)
1284 mastodon_emoji = mastodonized_emoji()
1286 limit = Config.get([:instance, :limit])
1289 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1291 flavour = get_user_flavour(user)
1296 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1297 access_token: token,
1299 domain: Pleroma.Web.Endpoint.host(),
1302 unfollow_modal: false,
1305 auto_play_gif: false,
1306 display_sensitive_media: false,
1307 reduce_motion: false,
1308 max_toot_chars: limit,
1309 mascot: "/images/pleroma-fox-tan-smol.png"
1312 delete_others_notice: present?(user.info.is_moderator),
1313 admin: present?(user.info.is_admin)
1317 default_privacy: user.info.default_scope,
1318 default_sensitive: false,
1319 allow_content_types: Config.get([:instance, :allowed_post_formats])
1321 media_attachments: %{
1322 accept_content_types: [
1338 user.info.settings ||
1368 push_subscription: nil,
1370 custom_emojis: mastodon_emoji,
1376 |> put_layout(false)
1377 |> put_view(MastodonView)
1378 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1381 |> put_session(:return_to, conn.request_path)
1382 |> redirect(to: "/web/login")
1386 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1387 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1389 with changeset <- Ecto.Changeset.change(user),
1390 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1391 {:ok, _user} <- User.update_and_set_cache(changeset) do
1396 |> put_resp_content_type("application/json")
1397 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1401 @supported_flavours ["glitch", "vanilla"]
1403 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1404 when flavour in @supported_flavours do
1405 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1407 with changeset <- Ecto.Changeset.change(user),
1408 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1409 {:ok, user} <- User.update_and_set_cache(changeset),
1410 flavour <- user.info.flavour do
1415 |> put_resp_content_type("application/json")
1416 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1420 def set_flavour(conn, _params) do
1423 |> json(%{error: "Unsupported flavour"})
1426 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1427 json(conn, get_user_flavour(user))
1430 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1434 defp get_user_flavour(_) do
1438 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1439 redirect(conn, to: local_mastodon_root_path(conn))
1442 @doc "Local Mastodon FE login init action"
1443 def login(conn, %{"code" => auth_token}) do
1444 with {:ok, app} <- get_or_make_app(),
1445 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1446 {:ok, token} <- Token.exchange_token(app, auth) do
1448 |> put_session(:oauth_token, token.token)
1449 |> redirect(to: local_mastodon_root_path(conn))
1453 @doc "Local Mastodon FE callback action"
1454 def login(conn, _) do
1455 with {:ok, app} <- get_or_make_app() do
1460 response_type: "code",
1461 client_id: app.client_id,
1463 scope: Enum.join(app.scopes, " ")
1466 redirect(conn, to: path)
1470 defp local_mastodon_root_path(conn) do
1471 case get_session(conn, :return_to) do
1473 mastodon_api_path(conn, :index, ["getting-started"])
1476 delete_session(conn, :return_to)
1481 defp get_or_make_app do
1482 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1483 scopes = ["read", "write", "follow", "push"]
1485 with %App{} = app <- Repo.get_by(App, find_attrs) do
1487 if app.scopes == scopes do
1491 |> Ecto.Changeset.change(%{scopes: scopes})
1499 App.register_changeset(
1501 Map.put(find_attrs, :scopes, scopes)
1508 def logout(conn, _) do
1511 |> redirect(to: "/")
1514 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1515 Logger.debug("Unimplemented, returning unmodified relationship")
1517 with %User{} = target <- User.get_cached_by_id(id) do
1519 |> put_view(AccountView)
1520 |> render("relationship.json", %{user: user, target: target})
1524 def empty_array(conn, _) do
1525 Logger.debug("Unimplemented, returning an empty array")
1529 def empty_object(conn, _) do
1530 Logger.debug("Unimplemented, returning an empty object")
1534 def get_filters(%{assigns: %{user: user}} = conn, _) do
1535 filters = Filter.get_filters(user)
1536 res = FilterView.render("filters.json", filters: filters)
1541 %{assigns: %{user: user}} = conn,
1542 %{"phrase" => phrase, "context" => context} = params
1548 hide: Map.get(params, "irreversible", false),
1549 whole_word: Map.get(params, "boolean", true)
1553 {:ok, response} = Filter.create(query)
1554 res = FilterView.render("filter.json", filter: response)
1558 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1559 filter = Filter.get(filter_id, user)
1560 res = FilterView.render("filter.json", filter: filter)
1565 %{assigns: %{user: user}} = conn,
1566 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1570 filter_id: filter_id,
1573 hide: Map.get(params, "irreversible", nil),
1574 whole_word: Map.get(params, "boolean", true)
1578 {:ok, response} = Filter.update(query)
1579 res = FilterView.render("filter.json", filter: response)
1583 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1586 filter_id: filter_id
1589 {:ok, _} = Filter.delete(query)
1595 def errors(conn, {:error, %Changeset{} = changeset}) do
1598 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1599 |> Enum.map_join(", ", fn {_k, v} -> v end)
1603 |> json(%{error: error_message})
1606 def errors(conn, {:error, :not_found}) do
1609 |> json(%{error: "Record not found"})
1612 def errors(conn, _) do
1615 |> json("Something went wrong")
1618 def suggestions(%{assigns: %{user: user}} = conn, _) do
1619 suggestions = Config.get(:suggestions)
1621 if Keyword.get(suggestions, :enabled, false) do
1622 api = Keyword.get(suggestions, :third_party_engine, "")
1623 timeout = Keyword.get(suggestions, :timeout, 5000)
1624 limit = Keyword.get(suggestions, :limit, 23)
1626 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1628 user = user.nickname
1632 |> String.replace("{{host}}", host)
1633 |> String.replace("{{user}}", user)
1635 with {:ok, %{status: 200, body: body}} <-
1640 recv_timeout: timeout,
1644 {:ok, data} <- Jason.decode(body) do
1647 |> Enum.slice(0, limit)
1652 case User.get_or_fetch(x["acct"]) do
1653 {:ok, %User{id: id}} -> id
1659 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1662 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1668 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1675 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1676 with %Activity{} = activity <- Activity.get_by_id(status_id),
1677 true <- Visibility.visible_for_user?(activity, user) do
1681 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1691 def reports(%{assigns: %{user: user}} = conn, params) do
1692 case CommonAPI.report(user, params) do
1695 |> put_view(ReportView)
1696 |> try_render("report.json", %{activity: activity})
1700 |> put_status(:bad_request)
1701 |> json(%{error: err})
1705 def account_register(
1706 %{assigns: %{app: app}} = conn,
1707 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1715 "captcha_answer_data",
1719 |> Map.put("nickname", nickname)
1720 |> Map.put("fullname", params["fullname"] || nickname)
1721 |> Map.put("bio", params["bio"] || "")
1722 |> Map.put("confirm", params["password"])
1724 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1725 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1727 token_type: "Bearer",
1728 access_token: token.token,
1730 created_at: Token.Utils.format_created_at(token)
1736 |> json(Jason.encode!(errors))
1740 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1743 |> json(%{error: "Missing parameters"})
1746 def account_register(conn, _) do
1749 |> json(%{error: "Invalid credentials"})
1752 def conversations(%{assigns: %{user: user}} = conn, params) do
1753 participations = Participation.for_user_with_last_activity_id(user, params)
1756 Enum.map(participations, fn participation ->
1757 ConversationView.render("participation.json", %{participation: participation, user: user})
1761 |> add_link_headers(:conversations, participations)
1762 |> json(conversations)
1765 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1766 with %Participation{} = participation <-
1767 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1768 {:ok, participation} <- Participation.mark_as_read(participation) do
1769 participation_view =
1770 ConversationView.render("participation.json", %{participation: participation, user: user})
1773 |> json(participation_view)
1777 def try_render(conn, target, params)
1778 when is_binary(target) do
1779 res = render(conn, target, params)
1784 |> json(%{error: "Can't display this activity"})
1790 def try_render(conn, _, _) do
1793 |> json(%{error: "Can't display this activity"})
1796 defp present?(nil), do: false
1797 defp present?(false), do: false
1798 defp present?(_), do: true