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 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
737 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
738 %{info: %{mascot: mascot}} = user
744 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
745 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
746 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
747 q = from(u in User, where: u.ap_id in ^likes)
751 |> put_view(AccountView)
752 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
758 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
759 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
760 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
761 q = from(u in User, where: u.ap_id in ^announces)
765 |> put_view(AccountView)
766 |> render("accounts.json", %{for: user, users: users, as: :user})
772 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
773 local_only = params["local"] in [true, "True", "true", "1"]
776 [params["tag"], params["any"]]
780 |> Enum.map(&String.downcase(&1))
785 |> Enum.map(&String.downcase(&1))
790 |> Enum.map(&String.downcase(&1))
794 |> Map.put("type", "Create")
795 |> Map.put("local_only", local_only)
796 |> Map.put("blocking_user", user)
797 |> Map.put("muting_user", user)
798 |> Map.put("tag", tags)
799 |> Map.put("tag_all", tag_all)
800 |> Map.put("tag_reject", tag_reject)
801 |> ActivityPub.fetch_public_activities()
805 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
806 |> put_view(StatusView)
807 |> render("index.json", %{activities: activities, for: user, as: :activity})
810 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
811 with %User{} = user <- User.get_cached_by_id(id),
812 followers <- MastodonAPI.get_followers(user, params) do
815 for_user && user.id == for_user.id -> followers
816 user.info.hide_followers -> []
821 |> add_link_headers(:followers, followers, user)
822 |> put_view(AccountView)
823 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
827 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
828 with %User{} = user <- User.get_cached_by_id(id),
829 followers <- MastodonAPI.get_friends(user, params) do
832 for_user && user.id == for_user.id -> followers
833 user.info.hide_follows -> []
838 |> add_link_headers(:following, followers, user)
839 |> put_view(AccountView)
840 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
844 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
845 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
847 |> put_view(AccountView)
848 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
852 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
853 with %User{} = follower <- User.get_cached_by_id(id),
854 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
856 |> put_view(AccountView)
857 |> render("relationship.json", %{user: followed, target: follower})
861 |> put_resp_content_type("application/json")
862 |> send_resp(403, Jason.encode!(%{"error" => message}))
866 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
867 with %User{} = follower <- User.get_cached_by_id(id),
868 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
870 |> put_view(AccountView)
871 |> render("relationship.json", %{user: followed, target: follower})
875 |> put_resp_content_type("application/json")
876 |> send_resp(403, Jason.encode!(%{"error" => message}))
880 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
881 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
882 {_, true} <- {:followed, follower.id != followed.id},
883 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
885 |> put_view(AccountView)
886 |> render("relationship.json", %{user: follower, target: followed})
893 |> put_resp_content_type("application/json")
894 |> send_resp(403, Jason.encode!(%{"error" => message}))
898 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
899 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
900 {_, true} <- {:followed, follower.id != followed.id},
901 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
903 |> put_view(AccountView)
904 |> render("account.json", %{user: followed, for: follower})
911 |> put_resp_content_type("application/json")
912 |> send_resp(403, Jason.encode!(%{"error" => message}))
916 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
917 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
918 {_, true} <- {:followed, follower.id != followed.id},
919 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
921 |> put_view(AccountView)
922 |> render("relationship.json", %{user: follower, target: followed})
932 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
933 with %User{} = muted <- User.get_cached_by_id(id),
934 {:ok, muter} <- User.mute(muter, muted) do
936 |> put_view(AccountView)
937 |> render("relationship.json", %{user: muter, target: muted})
941 |> put_resp_content_type("application/json")
942 |> send_resp(403, Jason.encode!(%{"error" => message}))
946 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
947 with %User{} = muted <- User.get_cached_by_id(id),
948 {:ok, muter} <- User.unmute(muter, muted) do
950 |> put_view(AccountView)
951 |> render("relationship.json", %{user: muter, target: muted})
955 |> put_resp_content_type("application/json")
956 |> send_resp(403, Jason.encode!(%{"error" => message}))
960 def mutes(%{assigns: %{user: user}} = conn, _) do
961 with muted_accounts <- User.muted_users(user) do
962 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
967 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
968 with %User{} = blocked <- User.get_cached_by_id(id),
969 {:ok, blocker} <- User.block(blocker, blocked),
970 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
972 |> put_view(AccountView)
973 |> render("relationship.json", %{user: blocker, target: blocked})
977 |> put_resp_content_type("application/json")
978 |> send_resp(403, Jason.encode!(%{"error" => message}))
982 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
983 with %User{} = blocked <- User.get_cached_by_id(id),
984 {:ok, blocker} <- User.unblock(blocker, blocked),
985 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
987 |> put_view(AccountView)
988 |> render("relationship.json", %{user: blocker, target: blocked})
992 |> put_resp_content_type("application/json")
993 |> send_resp(403, Jason.encode!(%{"error" => message}))
997 def blocks(%{assigns: %{user: user}} = conn, _) do
998 with blocked_accounts <- User.blocked_users(user) do
999 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1004 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1005 json(conn, info.domain_blocks || [])
1008 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1009 User.block_domain(blocker, domain)
1013 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1014 User.unblock_domain(blocker, domain)
1018 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1019 with %User{} = subscription_target <- User.get_cached_by_id(id),
1020 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1022 |> put_view(AccountView)
1023 |> render("relationship.json", %{user: user, target: subscription_target})
1025 {:error, message} ->
1027 |> put_resp_content_type("application/json")
1028 |> send_resp(403, Jason.encode!(%{"error" => message}))
1032 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1033 with %User{} = subscription_target <- User.get_cached_by_id(id),
1034 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1036 |> put_view(AccountView)
1037 |> render("relationship.json", %{user: user, target: subscription_target})
1039 {:error, message} ->
1041 |> put_resp_content_type("application/json")
1042 |> send_resp(403, Jason.encode!(%{"error" => message}))
1046 def status_search_query_with_gin(q, query) do
1050 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1054 order_by: [desc: :id]
1058 def status_search_query_with_rum(q, query) do
1062 "? @@ plainto_tsquery('english', ?)",
1066 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1070 def status_search(user, query) do
1072 if Regex.match?(~r/https?:/, query) do
1073 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1074 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1075 true <- Visibility.visible_for_user?(activity, user) do
1083 from([a, o] in Activity.with_preloaded_object(Activity),
1084 where: fragment("?->>'type' = 'Create'", a.data),
1085 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1090 if Pleroma.Config.get([:database, :rum_enabled]) do
1091 status_search_query_with_rum(q, query)
1093 status_search_query_with_gin(q, query)
1096 Repo.all(q) ++ fetched
1099 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1100 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1102 statuses = status_search(user, query)
1104 tags_path = Web.base_url() <> "/tag/"
1110 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1111 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1112 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1115 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1117 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1124 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1125 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1127 statuses = status_search(user, query)
1133 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1134 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1137 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1139 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1146 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1147 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1149 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1154 def favourites(%{assigns: %{user: user}} = conn, params) do
1157 |> Map.put("type", "Create")
1158 |> Map.put("favorited_by", user.ap_id)
1159 |> Map.put("blocking_user", user)
1162 ActivityPub.fetch_activities([], params)
1166 |> add_link_headers(:favourites, activities)
1167 |> put_view(StatusView)
1168 |> render("index.json", %{activities: activities, for: user, as: :activity})
1171 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1172 with %User{} = user <- User.get_by_id(id),
1173 false <- user.info.hide_favorites do
1176 |> Map.put("type", "Create")
1177 |> Map.put("favorited_by", user.ap_id)
1178 |> Map.put("blocking_user", for_user)
1182 ["https://www.w3.org/ns/activitystreams#Public"] ++
1183 [for_user.ap_id | for_user.following]
1185 ["https://www.w3.org/ns/activitystreams#Public"]
1190 |> ActivityPub.fetch_activities(params)
1194 |> add_link_headers(:favourites, activities)
1195 |> put_view(StatusView)
1196 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1199 {:error, :not_found}
1204 |> json(%{error: "Can't get favorites"})
1208 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1209 user = User.get_cached_by_id(user.id)
1212 Bookmark.for_user_query(user.id)
1213 |> Pagination.fetch_paginated(params)
1217 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1220 |> add_link_headers(:bookmarks, bookmarks)
1221 |> put_view(StatusView)
1222 |> render("index.json", %{activities: activities, for: user, as: :activity})
1225 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1226 lists = Pleroma.List.for_user(user, opts)
1227 res = ListView.render("lists.json", lists: lists)
1231 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1232 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1233 res = ListView.render("list.json", list: list)
1239 |> json(%{error: "Record not found"})
1243 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1244 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1245 res = ListView.render("lists.json", lists: lists)
1249 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1250 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1251 {:ok, _list} <- Pleroma.List.delete(list) do
1259 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1260 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1261 res = ListView.render("list.json", list: list)
1266 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1268 |> Enum.each(fn account_id ->
1269 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1270 %User{} = followed <- User.get_cached_by_id(account_id) do
1271 Pleroma.List.follow(list, followed)
1278 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1280 |> Enum.each(fn account_id ->
1281 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1282 %User{} = followed <- User.get_cached_by_id(account_id) do
1283 Pleroma.List.unfollow(list, followed)
1290 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1291 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1292 {:ok, users} = Pleroma.List.get_following(list) do
1294 |> put_view(AccountView)
1295 |> render("accounts.json", %{for: user, users: users, as: :user})
1299 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1300 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1301 {:ok, list} <- Pleroma.List.rename(list, title) do
1302 res = ListView.render("list.json", list: list)
1310 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1311 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1314 |> Map.put("type", "Create")
1315 |> Map.put("blocking_user", user)
1316 |> Map.put("muting_user", user)
1318 # we must filter the following list for the user to avoid leaking statuses the user
1319 # does not actually have permission to see (for more info, peruse security issue #270).
1322 |> Enum.filter(fn x -> x in user.following end)
1323 |> ActivityPub.fetch_activities_bounded(following, params)
1327 |> put_view(StatusView)
1328 |> render("index.json", %{activities: activities, for: user, as: :activity})
1333 |> json(%{error: "Error."})
1337 def index(%{assigns: %{user: user}} = conn, _params) do
1338 token = get_session(conn, :oauth_token)
1341 mastodon_emoji = mastodonized_emoji()
1343 limit = Config.get([:instance, :limit])
1346 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1348 flavour = get_user_flavour(user)
1353 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1354 access_token: token,
1356 domain: Pleroma.Web.Endpoint.host(),
1359 unfollow_modal: false,
1362 auto_play_gif: false,
1363 display_sensitive_media: false,
1364 reduce_motion: false,
1365 max_toot_chars: limit,
1366 mascot: Map.get(user.info.mascot, "url", "/images/pleroma-fox-tan-smol.png")
1369 delete_others_notice: present?(user.info.is_moderator),
1370 admin: present?(user.info.is_admin)
1374 default_privacy: user.info.default_scope,
1375 default_sensitive: false,
1376 allow_content_types: Config.get([:instance, :allowed_post_formats])
1378 media_attachments: %{
1379 accept_content_types: [
1395 user.info.settings ||
1425 push_subscription: nil,
1427 custom_emojis: mastodon_emoji,
1433 |> put_layout(false)
1434 |> put_view(MastodonView)
1435 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1438 |> put_session(:return_to, conn.request_path)
1439 |> redirect(to: "/web/login")
1443 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1444 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1446 with changeset <- Ecto.Changeset.change(user),
1447 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1448 {:ok, _user} <- User.update_and_set_cache(changeset) do
1453 |> put_resp_content_type("application/json")
1454 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1458 @supported_flavours ["glitch", "vanilla"]
1460 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1461 when flavour in @supported_flavours do
1462 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1464 with changeset <- Ecto.Changeset.change(user),
1465 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1466 {:ok, user} <- User.update_and_set_cache(changeset),
1467 flavour <- user.info.flavour do
1472 |> put_resp_content_type("application/json")
1473 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1477 def set_flavour(conn, _params) do
1480 |> json(%{error: "Unsupported flavour"})
1483 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1484 json(conn, get_user_flavour(user))
1487 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1491 defp get_user_flavour(_) do
1495 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1496 redirect(conn, to: local_mastodon_root_path(conn))
1499 @doc "Local Mastodon FE login init action"
1500 def login(conn, %{"code" => auth_token}) do
1501 with {:ok, app} <- get_or_make_app(),
1502 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1503 {:ok, token} <- Token.exchange_token(app, auth) do
1505 |> put_session(:oauth_token, token.token)
1506 |> redirect(to: local_mastodon_root_path(conn))
1510 @doc "Local Mastodon FE callback action"
1511 def login(conn, _) do
1512 with {:ok, app} <- get_or_make_app() do
1517 response_type: "code",
1518 client_id: app.client_id,
1520 scope: Enum.join(app.scopes, " ")
1523 redirect(conn, to: path)
1527 defp local_mastodon_root_path(conn) do
1528 case get_session(conn, :return_to) do
1530 mastodon_api_path(conn, :index, ["getting-started"])
1533 delete_session(conn, :return_to)
1538 defp get_or_make_app do
1539 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1540 scopes = ["read", "write", "follow", "push"]
1542 with %App{} = app <- Repo.get_by(App, find_attrs) do
1544 if app.scopes == scopes do
1548 |> Ecto.Changeset.change(%{scopes: scopes})
1556 App.register_changeset(
1558 Map.put(find_attrs, :scopes, scopes)
1565 def logout(conn, _) do
1568 |> redirect(to: "/")
1571 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1572 Logger.debug("Unimplemented, returning unmodified relationship")
1574 with %User{} = target <- User.get_cached_by_id(id) do
1576 |> put_view(AccountView)
1577 |> render("relationship.json", %{user: user, target: target})
1581 def empty_array(conn, _) do
1582 Logger.debug("Unimplemented, returning an empty array")
1586 def empty_object(conn, _) do
1587 Logger.debug("Unimplemented, returning an empty object")
1591 def get_filters(%{assigns: %{user: user}} = conn, _) do
1592 filters = Filter.get_filters(user)
1593 res = FilterView.render("filters.json", filters: filters)
1598 %{assigns: %{user: user}} = conn,
1599 %{"phrase" => phrase, "context" => context} = params
1605 hide: Map.get(params, "irreversible", false),
1606 whole_word: Map.get(params, "boolean", true)
1610 {:ok, response} = Filter.create(query)
1611 res = FilterView.render("filter.json", filter: response)
1615 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1616 filter = Filter.get(filter_id, user)
1617 res = FilterView.render("filter.json", filter: filter)
1622 %{assigns: %{user: user}} = conn,
1623 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1627 filter_id: filter_id,
1630 hide: Map.get(params, "irreversible", nil),
1631 whole_word: Map.get(params, "boolean", true)
1635 {:ok, response} = Filter.update(query)
1636 res = FilterView.render("filter.json", filter: response)
1640 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1643 filter_id: filter_id
1646 {:ok, _} = Filter.delete(query)
1652 def errors(conn, {:error, %Changeset{} = changeset}) do
1655 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1656 |> Enum.map_join(", ", fn {_k, v} -> v end)
1660 |> json(%{error: error_message})
1663 def errors(conn, {:error, :not_found}) do
1666 |> json(%{error: "Record not found"})
1669 def errors(conn, _) do
1672 |> json("Something went wrong")
1675 def suggestions(%{assigns: %{user: user}} = conn, _) do
1676 suggestions = Config.get(:suggestions)
1678 if Keyword.get(suggestions, :enabled, false) do
1679 api = Keyword.get(suggestions, :third_party_engine, "")
1680 timeout = Keyword.get(suggestions, :timeout, 5000)
1681 limit = Keyword.get(suggestions, :limit, 23)
1683 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1685 user = user.nickname
1689 |> String.replace("{{host}}", host)
1690 |> String.replace("{{user}}", user)
1692 with {:ok, %{status: 200, body: body}} <-
1697 recv_timeout: timeout,
1701 {:ok, data} <- Jason.decode(body) do
1704 |> Enum.slice(0, limit)
1709 case User.get_or_fetch(x["acct"]) do
1710 {:ok, %User{id: id}} -> id
1716 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1719 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1725 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1732 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1733 with %Activity{} = activity <- Activity.get_by_id(status_id),
1734 true <- Visibility.visible_for_user?(activity, user) do
1738 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1748 def reports(%{assigns: %{user: user}} = conn, params) do
1749 case CommonAPI.report(user, params) do
1752 |> put_view(ReportView)
1753 |> try_render("report.json", %{activity: activity})
1757 |> put_status(:bad_request)
1758 |> json(%{error: err})
1762 def account_register(
1763 %{assigns: %{app: app}} = conn,
1764 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1772 "captcha_answer_data",
1776 |> Map.put("nickname", nickname)
1777 |> Map.put("fullname", params["fullname"] || nickname)
1778 |> Map.put("bio", params["bio"] || "")
1779 |> Map.put("confirm", params["password"])
1781 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1782 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1784 token_type: "Bearer",
1785 access_token: token.token,
1787 created_at: Token.Utils.format_created_at(token)
1793 |> json(Jason.encode!(errors))
1797 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1800 |> json(%{error: "Missing parameters"})
1803 def account_register(conn, _) do
1806 |> json(%{error: "Invalid credentials"})
1809 def conversations(%{assigns: %{user: user}} = conn, params) do
1810 participations = Participation.for_user_with_last_activity_id(user, params)
1813 Enum.map(participations, fn participation ->
1814 ConversationView.render("participation.json", %{participation: participation, user: user})
1818 |> add_link_headers(:conversations, participations)
1819 |> json(conversations)
1822 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1823 with %Participation{} = participation <-
1824 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1825 {:ok, participation} <- Participation.mark_as_read(participation) do
1826 participation_view =
1827 ConversationView.render("participation.json", %{participation: participation, user: user})
1830 |> json(participation_view)
1834 def try_render(conn, target, params)
1835 when is_binary(target) do
1836 res = render(conn, target, params)
1841 |> json(%{error: "Can't display this activity"})
1847 def try_render(conn, _, _) do
1850 |> json(%{error: "Can't display this activity"})
1853 defp present?(nil), do: false
1854 defp present?(false), do: false
1855 defp present?(_), do: true