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),
201 poll_limits: Keyword.get(instance, :poll_limits)
207 def peers(conn, _params) do
208 json(conn, Stats.get_peers())
211 defp mastodonized_emoji do
212 Pleroma.Emoji.get_all()
213 |> Enum.map(fn {shortcode, relative_url, tags} ->
214 url = to_string(URI.merge(Web.base_url(), relative_url))
217 "shortcode" => shortcode,
219 "visible_in_picker" => true,
226 def custom_emojis(conn, _params) do
227 mastodon_emoji = mastodonized_emoji()
228 json(conn, mastodon_emoji)
231 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
234 |> Map.drop(["since_id", "max_id", "min_id"])
237 last = List.last(activities)
244 |> Map.get("limit", "20")
245 |> String.to_integer()
248 if length(activities) <= limit do
254 |> Enum.at(limit * -1)
258 {next_url, prev_url} =
262 Pleroma.Web.Endpoint,
265 Map.merge(params, %{max_id: max_id})
268 Pleroma.Web.Endpoint,
271 Map.merge(params, %{min_id: min_id})
277 Pleroma.Web.Endpoint,
279 Map.merge(params, %{max_id: max_id})
282 Pleroma.Web.Endpoint,
284 Map.merge(params, %{min_id: min_id})
290 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
296 def home_timeline(%{assigns: %{user: user}} = conn, params) do
299 |> Map.put("type", ["Create", "Announce"])
300 |> Map.put("blocking_user", user)
301 |> Map.put("muting_user", user)
302 |> Map.put("user", user)
305 [user.ap_id | user.following]
306 |> ActivityPub.fetch_activities(params)
310 |> add_link_headers(:home_timeline, activities)
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
315 def public_timeline(%{assigns: %{user: user}} = conn, params) do
316 local_only = params["local"] in [true, "True", "true", "1"]
320 |> Map.put("type", ["Create", "Announce"])
321 |> Map.put("local_only", local_only)
322 |> Map.put("blocking_user", user)
323 |> Map.put("muting_user", user)
324 |> ActivityPub.fetch_public_activities()
328 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
329 |> put_view(StatusView)
330 |> render("index.json", %{activities: activities, for: user, as: :activity})
333 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
334 with %User{} = user <- User.get_cached_by_id(params["id"]) do
335 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
338 |> add_link_headers(:user_statuses, activities, params["id"])
339 |> put_view(StatusView)
340 |> render("index.json", %{
341 activities: activities,
348 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
351 |> Map.put("type", "Create")
352 |> Map.put("blocking_user", user)
353 |> Map.put("user", user)
354 |> Map.put(:visibility, "direct")
358 |> ActivityPub.fetch_activities_query(params)
359 |> Pagination.fetch_paginated(params)
362 |> add_link_headers(:dm_timeline, activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
367 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
368 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
369 true <- Visibility.visible_for_user?(activity, user) do
371 |> put_view(StatusView)
372 |> try_render("status.json", %{activity: activity, for: user})
376 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with %Activity{} = activity <- Activity.get_by_id(id),
379 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
380 "blocking_user" => user,
384 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
386 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
387 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
393 activities: grouped_activities[true] || [],
397 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
402 activities: grouped_activities[false] || [],
406 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
413 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
414 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
416 |> add_link_headers(:scheduled_statuses, scheduled_activities)
417 |> put_view(ScheduledActivityView)
418 |> render("index.json", %{scheduled_activities: scheduled_activities})
422 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id) do
426 |> put_view(ScheduledActivityView)
427 |> render("show.json", %{scheduled_activity: scheduled_activity})
429 _ -> {:error, :not_found}
433 def update_scheduled_status(
434 %{assigns: %{user: user}} = conn,
435 %{"id" => scheduled_activity_id} = params
437 with %ScheduledActivity{} = scheduled_activity <-
438 ScheduledActivity.get(user, scheduled_activity_id),
439 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
441 |> put_view(ScheduledActivityView)
442 |> render("show.json", %{scheduled_activity: scheduled_activity})
444 nil -> {:error, :not_found}
449 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
450 with %ScheduledActivity{} = scheduled_activity <-
451 ScheduledActivity.get(user, scheduled_activity_id),
452 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
454 |> put_view(ScheduledActivityView)
455 |> render("show.json", %{scheduled_activity: scheduled_activity})
457 nil -> {:error, :not_found}
462 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
463 when length(media_ids) > 0 do
466 |> Map.put("status", ".")
468 post_status(conn, params)
471 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
474 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
477 case get_req_header(conn, "idempotency-key") do
479 _ -> Ecto.UUID.generate()
482 scheduled_at = params["scheduled_at"]
484 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
485 with {:ok, scheduled_activity} <-
486 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
488 |> put_view(ScheduledActivityView)
489 |> render("show.json", %{scheduled_activity: scheduled_activity})
492 params = Map.drop(params, ["scheduled_at"])
495 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
496 CommonAPI.post(user, params)
500 |> put_view(StatusView)
501 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
505 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
506 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
512 |> json(%{error: "Can't delete this post"})
516 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
517 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
518 %Activity{} = announce <- Activity.normalize(announce.data) do
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
525 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
526 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
527 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
529 |> put_view(StatusView)
530 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
534 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
535 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
536 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
538 |> put_view(StatusView)
539 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
543 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
544 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
545 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
547 |> put_view(StatusView)
548 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
552 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
553 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
555 |> put_view(StatusView)
556 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
560 |> put_resp_content_type("application/json")
561 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
565 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
566 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
568 |> put_view(StatusView)
569 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
573 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
574 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
575 %User{} = user <- User.get_cached_by_nickname(user.nickname),
576 true <- Visibility.visible_for_user?(activity, user),
577 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
579 |> put_view(StatusView)
580 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
584 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
585 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
586 %User{} = user <- User.get_cached_by_nickname(user.nickname),
587 true <- Visibility.visible_for_user?(activity, user),
588 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
590 |> put_view(StatusView)
591 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
595 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
596 activity = Activity.get_by_id(id)
598 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
600 |> put_view(StatusView)
601 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
605 |> put_resp_content_type("application/json")
606 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
610 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
611 activity = Activity.get_by_id(id)
613 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
615 |> put_view(StatusView)
616 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
620 def notifications(%{assigns: %{user: user}} = conn, params) do
621 notifications = MastodonAPI.get_notifications(user, params)
624 |> add_link_headers(:notifications, notifications)
625 |> put_view(NotificationView)
626 |> render("index.json", %{notifications: notifications, for: user})
629 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
630 with {:ok, notification} <- Notification.get(user, id) do
632 |> put_view(NotificationView)
633 |> render("show.json", %{notification: notification, for: user})
637 |> put_resp_content_type("application/json")
638 |> send_resp(403, Jason.encode!(%{"error" => reason}))
642 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
643 Notification.clear(user)
647 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
648 with {:ok, _notif} <- Notification.dismiss(user, id) do
653 |> put_resp_content_type("application/json")
654 |> send_resp(403, Jason.encode!(%{"error" => reason}))
658 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
659 Notification.destroy_multiple(user, ids)
663 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
665 q = from(u in User, where: u.id in ^id)
666 targets = Repo.all(q)
669 |> put_view(AccountView)
670 |> render("relationships.json", %{user: user, targets: targets})
673 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
674 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
676 def update_media(%{assigns: %{user: user}} = conn, data) do
677 with %Object{} = object <- Repo.get(Object, data["id"]),
678 true <- Object.authorize_mutation(object, user),
679 true <- is_binary(data["description"]),
680 description <- data["description"] do
681 new_data = %{object.data | "name" => description}
685 |> Object.change(%{data: new_data})
688 attachment_data = Map.put(new_data, "id", object.id)
691 |> put_view(StatusView)
692 |> render("attachment.json", %{attachment: attachment_data})
696 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
697 with {:ok, object} <-
700 actor: User.ap_id(user),
701 description: Map.get(data, "description")
703 attachment_data = Map.put(object.data, "id", object.id)
706 |> put_view(StatusView)
707 |> render("attachment.json", %{attachment: attachment_data})
711 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
712 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
713 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
714 q = from(u in User, where: u.ap_id in ^likes)
718 |> put_view(AccountView)
719 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
725 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
726 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
727 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
728 q = from(u in User, where: u.ap_id in ^announces)
732 |> put_view(AccountView)
733 |> render("accounts.json", %{for: user, users: users, as: :user})
739 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
740 local_only = params["local"] in [true, "True", "true", "1"]
743 [params["tag"], params["any"]]
747 |> Enum.map(&String.downcase(&1))
752 |> Enum.map(&String.downcase(&1))
757 |> Enum.map(&String.downcase(&1))
761 |> Map.put("type", "Create")
762 |> Map.put("local_only", local_only)
763 |> Map.put("blocking_user", user)
764 |> Map.put("muting_user", user)
765 |> Map.put("tag", tags)
766 |> Map.put("tag_all", tag_all)
767 |> Map.put("tag_reject", tag_reject)
768 |> ActivityPub.fetch_public_activities()
772 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
773 |> put_view(StatusView)
774 |> render("index.json", %{activities: activities, for: user, as: :activity})
777 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
778 with %User{} = user <- User.get_cached_by_id(id),
779 followers <- MastodonAPI.get_followers(user, params) do
782 for_user && user.id == for_user.id -> followers
783 user.info.hide_followers -> []
788 |> add_link_headers(:followers, followers, user)
789 |> put_view(AccountView)
790 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
794 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
795 with %User{} = user <- User.get_cached_by_id(id),
796 followers <- MastodonAPI.get_friends(user, params) do
799 for_user && user.id == for_user.id -> followers
800 user.info.hide_follows -> []
805 |> add_link_headers(:following, followers, user)
806 |> put_view(AccountView)
807 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
811 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
812 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
814 |> put_view(AccountView)
815 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
819 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
820 with %User{} = follower <- User.get_cached_by_id(id),
821 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
823 |> put_view(AccountView)
824 |> render("relationship.json", %{user: followed, target: follower})
828 |> put_resp_content_type("application/json")
829 |> send_resp(403, Jason.encode!(%{"error" => message}))
833 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
834 with %User{} = follower <- User.get_cached_by_id(id),
835 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
837 |> put_view(AccountView)
838 |> render("relationship.json", %{user: followed, target: follower})
842 |> put_resp_content_type("application/json")
843 |> send_resp(403, Jason.encode!(%{"error" => message}))
847 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
848 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
849 {_, true} <- {:followed, follower.id != followed.id},
850 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
852 |> put_view(AccountView)
853 |> render("relationship.json", %{user: follower, target: followed})
860 |> put_resp_content_type("application/json")
861 |> send_resp(403, Jason.encode!(%{"error" => message}))
865 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
866 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
867 {_, true} <- {:followed, follower.id != followed.id},
868 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
870 |> put_view(AccountView)
871 |> render("account.json", %{user: followed, for: follower})
878 |> put_resp_content_type("application/json")
879 |> send_resp(403, Jason.encode!(%{"error" => message}))
883 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
884 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
885 {_, true} <- {:followed, follower.id != followed.id},
886 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
888 |> put_view(AccountView)
889 |> render("relationship.json", %{user: follower, target: followed})
899 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
900 with %User{} = muted <- User.get_cached_by_id(id),
901 {:ok, muter} <- User.mute(muter, muted) do
903 |> put_view(AccountView)
904 |> render("relationship.json", %{user: muter, target: muted})
908 |> put_resp_content_type("application/json")
909 |> send_resp(403, Jason.encode!(%{"error" => message}))
913 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
914 with %User{} = muted <- User.get_cached_by_id(id),
915 {:ok, muter} <- User.unmute(muter, muted) do
917 |> put_view(AccountView)
918 |> render("relationship.json", %{user: muter, target: muted})
922 |> put_resp_content_type("application/json")
923 |> send_resp(403, Jason.encode!(%{"error" => message}))
927 def mutes(%{assigns: %{user: user}} = conn, _) do
928 with muted_accounts <- User.muted_users(user) do
929 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
934 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
935 with %User{} = blocked <- User.get_cached_by_id(id),
936 {:ok, blocker} <- User.block(blocker, blocked),
937 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
939 |> put_view(AccountView)
940 |> render("relationship.json", %{user: blocker, target: blocked})
944 |> put_resp_content_type("application/json")
945 |> send_resp(403, Jason.encode!(%{"error" => message}))
949 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
950 with %User{} = blocked <- User.get_cached_by_id(id),
951 {:ok, blocker} <- User.unblock(blocker, blocked),
952 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
954 |> put_view(AccountView)
955 |> render("relationship.json", %{user: blocker, target: blocked})
959 |> put_resp_content_type("application/json")
960 |> send_resp(403, Jason.encode!(%{"error" => message}))
964 def blocks(%{assigns: %{user: user}} = conn, _) do
965 with blocked_accounts <- User.blocked_users(user) do
966 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
971 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
972 json(conn, info.domain_blocks || [])
975 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
976 User.block_domain(blocker, domain)
980 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
981 User.unblock_domain(blocker, domain)
985 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
986 with %User{} = subscription_target <- User.get_cached_by_id(id),
987 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
989 |> put_view(AccountView)
990 |> render("relationship.json", %{user: user, target: subscription_target})
994 |> put_resp_content_type("application/json")
995 |> send_resp(403, Jason.encode!(%{"error" => message}))
999 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1000 with %User{} = subscription_target <- User.get_cached_by_id(id),
1001 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1003 |> put_view(AccountView)
1004 |> render("relationship.json", %{user: user, target: subscription_target})
1006 {:error, message} ->
1008 |> put_resp_content_type("application/json")
1009 |> send_resp(403, Jason.encode!(%{"error" => message}))
1013 def status_search_query_with_gin(q, query) do
1017 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1021 order_by: [desc: :id]
1025 def status_search_query_with_rum(q, query) do
1029 "? @@ plainto_tsquery('english', ?)",
1033 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1037 def status_search(user, query) do
1039 if Regex.match?(~r/https?:/, query) do
1040 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1041 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1042 true <- Visibility.visible_for_user?(activity, user) do
1050 from([a, o] in Activity.with_preloaded_object(Activity),
1051 where: fragment("?->>'type' = 'Create'", a.data),
1052 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1057 if Pleroma.Config.get([:database, :rum_enabled]) do
1058 status_search_query_with_rum(q, query)
1060 status_search_query_with_gin(q, query)
1063 Repo.all(q) ++ fetched
1066 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1067 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1069 statuses = status_search(user, query)
1071 tags_path = Web.base_url() <> "/tag/"
1077 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1078 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1079 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1082 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1084 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1091 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1092 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1094 statuses = status_search(user, query)
1100 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1101 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1104 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1106 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1113 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1114 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1116 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1121 def favourites(%{assigns: %{user: user}} = conn, params) do
1124 |> Map.put("type", "Create")
1125 |> Map.put("favorited_by", user.ap_id)
1126 |> Map.put("blocking_user", user)
1129 ActivityPub.fetch_activities([], params)
1133 |> add_link_headers(:favourites, activities)
1134 |> put_view(StatusView)
1135 |> render("index.json", %{activities: activities, for: user, as: :activity})
1138 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1139 with %User{} = user <- User.get_by_id(id),
1140 false <- user.info.hide_favorites do
1143 |> Map.put("type", "Create")
1144 |> Map.put("favorited_by", user.ap_id)
1145 |> Map.put("blocking_user", for_user)
1149 ["https://www.w3.org/ns/activitystreams#Public"] ++
1150 [for_user.ap_id | for_user.following]
1152 ["https://www.w3.org/ns/activitystreams#Public"]
1157 |> ActivityPub.fetch_activities(params)
1161 |> add_link_headers(:favourites, activities)
1162 |> put_view(StatusView)
1163 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1166 {:error, :not_found}
1171 |> json(%{error: "Can't get favorites"})
1175 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1176 user = User.get_cached_by_id(user.id)
1179 Bookmark.for_user_query(user.id)
1180 |> Pagination.fetch_paginated(params)
1184 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1187 |> add_link_headers(:bookmarks, bookmarks)
1188 |> put_view(StatusView)
1189 |> render("index.json", %{activities: activities, for: user, as: :activity})
1192 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1193 lists = Pleroma.List.for_user(user, opts)
1194 res = ListView.render("lists.json", lists: lists)
1198 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1199 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1200 res = ListView.render("list.json", list: list)
1206 |> json(%{error: "Record not found"})
1210 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1211 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1212 res = ListView.render("lists.json", lists: lists)
1216 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1217 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1218 {:ok, _list} <- Pleroma.List.delete(list) do
1226 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1227 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1228 res = ListView.render("list.json", list: list)
1233 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1235 |> Enum.each(fn account_id ->
1236 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1237 %User{} = followed <- User.get_cached_by_id(account_id) do
1238 Pleroma.List.follow(list, followed)
1245 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1247 |> Enum.each(fn account_id ->
1248 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1249 %User{} = followed <- User.get_cached_by_id(account_id) do
1250 Pleroma.List.unfollow(list, followed)
1257 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1258 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1259 {:ok, users} = Pleroma.List.get_following(list) do
1261 |> put_view(AccountView)
1262 |> render("accounts.json", %{for: user, users: users, as: :user})
1266 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1267 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1268 {:ok, list} <- Pleroma.List.rename(list, title) do
1269 res = ListView.render("list.json", list: list)
1277 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1278 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1281 |> Map.put("type", "Create")
1282 |> Map.put("blocking_user", user)
1283 |> Map.put("muting_user", user)
1285 # we must filter the following list for the user to avoid leaking statuses the user
1286 # does not actually have permission to see (for more info, peruse security issue #270).
1289 |> Enum.filter(fn x -> x in user.following end)
1290 |> ActivityPub.fetch_activities_bounded(following, params)
1294 |> put_view(StatusView)
1295 |> render("index.json", %{activities: activities, for: user, as: :activity})
1300 |> json(%{error: "Error."})
1304 def index(%{assigns: %{user: user}} = conn, _params) do
1305 token = get_session(conn, :oauth_token)
1308 mastodon_emoji = mastodonized_emoji()
1310 limit = Config.get([:instance, :limit])
1313 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1315 flavour = get_user_flavour(user)
1320 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1321 access_token: token,
1323 domain: Pleroma.Web.Endpoint.host(),
1326 unfollow_modal: false,
1329 auto_play_gif: false,
1330 display_sensitive_media: false,
1331 reduce_motion: false,
1332 max_toot_chars: limit,
1333 mascot: "/images/pleroma-fox-tan-smol.png"
1335 poll_limits: Config.get([:instance, :poll_limits]),
1337 delete_others_notice: present?(user.info.is_moderator),
1338 admin: present?(user.info.is_admin)
1342 default_privacy: user.info.default_scope,
1343 default_sensitive: false,
1344 allow_content_types: Config.get([:instance, :allowed_post_formats])
1346 media_attachments: %{
1347 accept_content_types: [
1363 user.info.settings ||
1393 push_subscription: nil,
1395 custom_emojis: mastodon_emoji,
1401 |> put_layout(false)
1402 |> put_view(MastodonView)
1403 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1406 |> put_session(:return_to, conn.request_path)
1407 |> redirect(to: "/web/login")
1411 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1412 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1414 with changeset <- Ecto.Changeset.change(user),
1415 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1416 {:ok, _user} <- User.update_and_set_cache(changeset) do
1421 |> put_resp_content_type("application/json")
1422 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1426 @supported_flavours ["glitch", "vanilla"]
1428 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1429 when flavour in @supported_flavours do
1430 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1432 with changeset <- Ecto.Changeset.change(user),
1433 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1434 {:ok, user} <- User.update_and_set_cache(changeset),
1435 flavour <- user.info.flavour do
1440 |> put_resp_content_type("application/json")
1441 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1445 def set_flavour(conn, _params) do
1448 |> json(%{error: "Unsupported flavour"})
1451 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1452 json(conn, get_user_flavour(user))
1455 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1459 defp get_user_flavour(_) do
1463 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1464 redirect(conn, to: local_mastodon_root_path(conn))
1467 @doc "Local Mastodon FE login init action"
1468 def login(conn, %{"code" => auth_token}) do
1469 with {:ok, app} <- get_or_make_app(),
1470 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1471 {:ok, token} <- Token.exchange_token(app, auth) do
1473 |> put_session(:oauth_token, token.token)
1474 |> redirect(to: local_mastodon_root_path(conn))
1478 @doc "Local Mastodon FE callback action"
1479 def login(conn, _) do
1480 with {:ok, app} <- get_or_make_app() do
1485 response_type: "code",
1486 client_id: app.client_id,
1488 scope: Enum.join(app.scopes, " ")
1491 redirect(conn, to: path)
1495 defp local_mastodon_root_path(conn) do
1496 case get_session(conn, :return_to) do
1498 mastodon_api_path(conn, :index, ["getting-started"])
1501 delete_session(conn, :return_to)
1506 defp get_or_make_app do
1507 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1508 scopes = ["read", "write", "follow", "push"]
1510 with %App{} = app <- Repo.get_by(App, find_attrs) do
1512 if app.scopes == scopes do
1516 |> Ecto.Changeset.change(%{scopes: scopes})
1524 App.register_changeset(
1526 Map.put(find_attrs, :scopes, scopes)
1533 def logout(conn, _) do
1536 |> redirect(to: "/")
1539 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1540 Logger.debug("Unimplemented, returning unmodified relationship")
1542 with %User{} = target <- User.get_cached_by_id(id) do
1544 |> put_view(AccountView)
1545 |> render("relationship.json", %{user: user, target: target})
1549 def empty_array(conn, _) do
1550 Logger.debug("Unimplemented, returning an empty array")
1554 def empty_object(conn, _) do
1555 Logger.debug("Unimplemented, returning an empty object")
1559 def get_filters(%{assigns: %{user: user}} = conn, _) do
1560 filters = Filter.get_filters(user)
1561 res = FilterView.render("filters.json", filters: filters)
1566 %{assigns: %{user: user}} = conn,
1567 %{"phrase" => phrase, "context" => context} = params
1573 hide: Map.get(params, "irreversible", false),
1574 whole_word: Map.get(params, "boolean", true)
1578 {:ok, response} = Filter.create(query)
1579 res = FilterView.render("filter.json", filter: response)
1583 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1584 filter = Filter.get(filter_id, user)
1585 res = FilterView.render("filter.json", filter: filter)
1590 %{assigns: %{user: user}} = conn,
1591 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1595 filter_id: filter_id,
1598 hide: Map.get(params, "irreversible", nil),
1599 whole_word: Map.get(params, "boolean", true)
1603 {:ok, response} = Filter.update(query)
1604 res = FilterView.render("filter.json", filter: response)
1608 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1611 filter_id: filter_id
1614 {:ok, _} = Filter.delete(query)
1620 def errors(conn, {:error, %Changeset{} = changeset}) do
1623 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1624 |> Enum.map_join(", ", fn {_k, v} -> v end)
1628 |> json(%{error: error_message})
1631 def errors(conn, {:error, :not_found}) do
1634 |> json(%{error: "Record not found"})
1637 def errors(conn, _) do
1640 |> json("Something went wrong")
1643 def suggestions(%{assigns: %{user: user}} = conn, _) do
1644 suggestions = Config.get(:suggestions)
1646 if Keyword.get(suggestions, :enabled, false) do
1647 api = Keyword.get(suggestions, :third_party_engine, "")
1648 timeout = Keyword.get(suggestions, :timeout, 5000)
1649 limit = Keyword.get(suggestions, :limit, 23)
1651 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1653 user = user.nickname
1657 |> String.replace("{{host}}", host)
1658 |> String.replace("{{user}}", user)
1660 with {:ok, %{status: 200, body: body}} <-
1665 recv_timeout: timeout,
1669 {:ok, data} <- Jason.decode(body) do
1672 |> Enum.slice(0, limit)
1677 case User.get_or_fetch(x["acct"]) do
1678 {:ok, %User{id: id}} -> id
1684 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1687 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1693 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1700 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1701 with %Activity{} = activity <- Activity.get_by_id(status_id),
1702 true <- Visibility.visible_for_user?(activity, user) do
1706 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1716 def reports(%{assigns: %{user: user}} = conn, params) do
1717 case CommonAPI.report(user, params) do
1720 |> put_view(ReportView)
1721 |> try_render("report.json", %{activity: activity})
1725 |> put_status(:bad_request)
1726 |> json(%{error: err})
1730 def account_register(
1731 %{assigns: %{app: app}} = conn,
1732 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1740 "captcha_answer_data",
1744 |> Map.put("nickname", nickname)
1745 |> Map.put("fullname", params["fullname"] || nickname)
1746 |> Map.put("bio", params["bio"] || "")
1747 |> Map.put("confirm", params["password"])
1749 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1750 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1752 token_type: "Bearer",
1753 access_token: token.token,
1755 created_at: Token.Utils.format_created_at(token)
1761 |> json(Jason.encode!(errors))
1765 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1768 |> json(%{error: "Missing parameters"})
1771 def account_register(conn, _) do
1774 |> json(%{error: "Invalid credentials"})
1777 def conversations(%{assigns: %{user: user}} = conn, params) do
1778 participations = Participation.for_user_with_last_activity_id(user, params)
1781 Enum.map(participations, fn participation ->
1782 ConversationView.render("participation.json", %{participation: participation, user: user})
1786 |> add_link_headers(:conversations, participations)
1787 |> json(conversations)
1790 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1791 with %Participation{} = participation <-
1792 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1793 {:ok, participation} <- Participation.mark_as_read(participation) do
1794 participation_view =
1795 ConversationView.render("participation.json", %{participation: participation, user: user})
1798 |> json(participation_view)
1802 def try_render(conn, target, params)
1803 when is_binary(target) do
1804 res = render(conn, target, params)
1809 |> json(%{error: "Can't display this activity"})
1815 def try_render(conn, _, _) do
1818 |> json(%{error: "Can't display this activity"})
1821 defp present?(nil), do: false
1822 defp present?(false), do: false
1823 defp present?(_), do: true