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"])
476 scheduled_at = params["scheduled_at"]
478 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
479 with {:ok, scheduled_activity} <-
480 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
482 |> put_view(ScheduledActivityView)
483 |> render("show.json", %{scheduled_activity: scheduled_activity})
486 params = Map.drop(params, ["scheduled_at"])
488 case get_cached_status_or_post(conn, params) do
489 {:ignore, message} ->
492 |> json(%{error: message})
497 |> json(%{error: message})
501 |> put_view(StatusView)
502 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
507 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
509 case get_req_header(conn, "idempotency-key") do
511 _ -> Ecto.UUID.generate()
514 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
515 case CommonAPI.post(user, params) do
516 {:ok, activity} -> activity
517 {:error, message} -> {:ignore, message}
522 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
523 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
529 |> json(%{error: "Can't delete this post"})
533 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
534 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
535 %Activity{} = announce <- Activity.normalize(announce.data) do
537 |> put_view(StatusView)
538 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
542 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
543 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
544 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
546 |> put_view(StatusView)
547 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
551 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
552 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
553 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
555 |> put_view(StatusView)
556 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
560 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
561 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
562 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
564 |> put_view(StatusView)
565 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
569 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
570 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
577 |> put_resp_content_type("application/json")
578 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
582 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
583 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
585 |> put_view(StatusView)
586 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
590 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
591 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
592 %User{} = user <- User.get_cached_by_nickname(user.nickname),
593 true <- Visibility.visible_for_user?(activity, user),
594 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
596 |> put_view(StatusView)
597 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
601 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
602 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
603 %User{} = user <- User.get_cached_by_nickname(user.nickname),
604 true <- Visibility.visible_for_user?(activity, user),
605 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
607 |> put_view(StatusView)
608 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
612 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
613 activity = Activity.get_by_id(id)
615 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
617 |> put_view(StatusView)
618 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
622 |> put_resp_content_type("application/json")
623 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
627 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
628 activity = Activity.get_by_id(id)
630 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
632 |> put_view(StatusView)
633 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
637 def notifications(%{assigns: %{user: user}} = conn, params) do
638 notifications = MastodonAPI.get_notifications(user, params)
641 |> add_link_headers(:notifications, notifications)
642 |> put_view(NotificationView)
643 |> render("index.json", %{notifications: notifications, for: user})
646 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
647 with {:ok, notification} <- Notification.get(user, id) do
649 |> put_view(NotificationView)
650 |> render("show.json", %{notification: notification, for: user})
654 |> put_resp_content_type("application/json")
655 |> send_resp(403, Jason.encode!(%{"error" => reason}))
659 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
660 Notification.clear(user)
664 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
665 with {:ok, _notif} <- Notification.dismiss(user, id) do
670 |> put_resp_content_type("application/json")
671 |> send_resp(403, Jason.encode!(%{"error" => reason}))
675 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
676 Notification.destroy_multiple(user, ids)
680 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
682 q = from(u in User, where: u.id in ^id)
683 targets = Repo.all(q)
686 |> put_view(AccountView)
687 |> render("relationships.json", %{user: user, targets: targets})
690 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
691 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
693 def update_media(%{assigns: %{user: user}} = conn, data) do
694 with %Object{} = object <- Repo.get(Object, data["id"]),
695 true <- Object.authorize_mutation(object, user),
696 true <- is_binary(data["description"]),
697 description <- data["description"] do
698 new_data = %{object.data | "name" => description}
702 |> Object.change(%{data: new_data})
705 attachment_data = Map.put(new_data, "id", object.id)
708 |> put_view(StatusView)
709 |> render("attachment.json", %{attachment: attachment_data})
713 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
714 with {:ok, object} <-
717 actor: User.ap_id(user),
718 description: Map.get(data, "description")
720 attachment_data = Map.put(object.data, "id", object.id)
723 |> put_view(StatusView)
724 |> render("attachment.json", %{attachment: attachment_data})
728 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
729 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
730 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
731 q = from(u in User, where: u.ap_id in ^likes)
735 |> put_view(AccountView)
736 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
742 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
743 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
744 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
745 q = from(u in User, where: u.ap_id in ^announces)
749 |> put_view(AccountView)
750 |> render("accounts.json", %{for: user, users: users, as: :user})
756 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
757 local_only = params["local"] in [true, "True", "true", "1"]
760 [params["tag"], params["any"]]
764 |> Enum.map(&String.downcase(&1))
769 |> Enum.map(&String.downcase(&1))
774 |> Enum.map(&String.downcase(&1))
778 |> Map.put("type", "Create")
779 |> Map.put("local_only", local_only)
780 |> Map.put("blocking_user", user)
781 |> Map.put("muting_user", user)
782 |> Map.put("tag", tags)
783 |> Map.put("tag_all", tag_all)
784 |> Map.put("tag_reject", tag_reject)
785 |> ActivityPub.fetch_public_activities()
789 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
790 |> put_view(StatusView)
791 |> render("index.json", %{activities: activities, for: user, as: :activity})
794 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
795 with %User{} = user <- User.get_cached_by_id(id),
796 followers <- MastodonAPI.get_followers(user, params) do
799 for_user && user.id == for_user.id -> followers
800 user.info.hide_followers -> []
805 |> add_link_headers(:followers, followers, user)
806 |> put_view(AccountView)
807 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
811 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
812 with %User{} = user <- User.get_cached_by_id(id),
813 followers <- MastodonAPI.get_friends(user, params) do
816 for_user && user.id == for_user.id -> followers
817 user.info.hide_follows -> []
822 |> add_link_headers(:following, followers, user)
823 |> put_view(AccountView)
824 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
828 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
829 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
831 |> put_view(AccountView)
832 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
836 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
837 with %User{} = follower <- User.get_cached_by_id(id),
838 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
840 |> put_view(AccountView)
841 |> render("relationship.json", %{user: followed, target: follower})
845 |> put_resp_content_type("application/json")
846 |> send_resp(403, Jason.encode!(%{"error" => message}))
850 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
851 with %User{} = follower <- User.get_cached_by_id(id),
852 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
854 |> put_view(AccountView)
855 |> render("relationship.json", %{user: followed, target: follower})
859 |> put_resp_content_type("application/json")
860 |> send_resp(403, Jason.encode!(%{"error" => message}))
864 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
865 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
866 {_, true} <- {:followed, follower.id != followed.id},
867 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
869 |> put_view(AccountView)
870 |> render("relationship.json", %{user: follower, target: followed})
877 |> put_resp_content_type("application/json")
878 |> send_resp(403, Jason.encode!(%{"error" => message}))
882 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
883 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
884 {_, true} <- {:followed, follower.id != followed.id},
885 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
887 |> put_view(AccountView)
888 |> render("account.json", %{user: followed, for: follower})
895 |> put_resp_content_type("application/json")
896 |> send_resp(403, Jason.encode!(%{"error" => message}))
900 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
901 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
902 {_, true} <- {:followed, follower.id != followed.id},
903 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
905 |> put_view(AccountView)
906 |> render("relationship.json", %{user: follower, target: followed})
916 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
917 with %User{} = muted <- User.get_cached_by_id(id),
918 {:ok, muter} <- User.mute(muter, muted) do
920 |> put_view(AccountView)
921 |> render("relationship.json", %{user: muter, target: muted})
925 |> put_resp_content_type("application/json")
926 |> send_resp(403, Jason.encode!(%{"error" => message}))
930 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
931 with %User{} = muted <- User.get_cached_by_id(id),
932 {:ok, muter} <- User.unmute(muter, muted) do
934 |> put_view(AccountView)
935 |> render("relationship.json", %{user: muter, target: muted})
939 |> put_resp_content_type("application/json")
940 |> send_resp(403, Jason.encode!(%{"error" => message}))
944 def mutes(%{assigns: %{user: user}} = conn, _) do
945 with muted_accounts <- User.muted_users(user) do
946 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
951 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
952 with %User{} = blocked <- User.get_cached_by_id(id),
953 {:ok, blocker} <- User.block(blocker, blocked),
954 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
956 |> put_view(AccountView)
957 |> render("relationship.json", %{user: blocker, target: blocked})
961 |> put_resp_content_type("application/json")
962 |> send_resp(403, Jason.encode!(%{"error" => message}))
966 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
967 with %User{} = blocked <- User.get_cached_by_id(id),
968 {:ok, blocker} <- User.unblock(blocker, blocked),
969 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
971 |> put_view(AccountView)
972 |> render("relationship.json", %{user: blocker, target: blocked})
976 |> put_resp_content_type("application/json")
977 |> send_resp(403, Jason.encode!(%{"error" => message}))
981 def blocks(%{assigns: %{user: user}} = conn, _) do
982 with blocked_accounts <- User.blocked_users(user) do
983 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
988 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
989 json(conn, info.domain_blocks || [])
992 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
993 User.block_domain(blocker, domain)
997 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
998 User.unblock_domain(blocker, domain)
1002 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1003 with %User{} = subscription_target <- User.get_cached_by_id(id),
1004 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1006 |> put_view(AccountView)
1007 |> render("relationship.json", %{user: user, target: subscription_target})
1009 {:error, message} ->
1011 |> put_resp_content_type("application/json")
1012 |> send_resp(403, Jason.encode!(%{"error" => message}))
1016 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1017 with %User{} = subscription_target <- User.get_cached_by_id(id),
1018 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1020 |> put_view(AccountView)
1021 |> render("relationship.json", %{user: user, target: subscription_target})
1023 {:error, message} ->
1025 |> put_resp_content_type("application/json")
1026 |> send_resp(403, Jason.encode!(%{"error" => message}))
1030 def status_search_query_with_gin(q, query) do
1034 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1038 order_by: [desc: :id]
1042 def status_search_query_with_rum(q, query) do
1046 "? @@ plainto_tsquery('english', ?)",
1050 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1054 def status_search(user, query) do
1056 if Regex.match?(~r/https?:/, query) do
1057 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1058 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1059 true <- Visibility.visible_for_user?(activity, user) do
1067 from([a, o] in Activity.with_preloaded_object(Activity),
1068 where: fragment("?->>'type' = 'Create'", a.data),
1069 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1074 if Pleroma.Config.get([:database, :rum_enabled]) do
1075 status_search_query_with_rum(q, query)
1077 status_search_query_with_gin(q, query)
1080 Repo.all(q) ++ fetched
1083 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1084 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1086 statuses = status_search(user, query)
1088 tags_path = Web.base_url() <> "/tag/"
1094 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1095 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1096 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1099 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1101 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1108 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1109 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1111 statuses = status_search(user, query)
1117 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1118 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1121 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1123 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1130 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1131 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1133 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1138 def favourites(%{assigns: %{user: user}} = conn, params) do
1141 |> Map.put("type", "Create")
1142 |> Map.put("favorited_by", user.ap_id)
1143 |> Map.put("blocking_user", user)
1146 ActivityPub.fetch_activities([], params)
1150 |> add_link_headers(:favourites, activities)
1151 |> put_view(StatusView)
1152 |> render("index.json", %{activities: activities, for: user, as: :activity})
1155 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1156 with %User{} = user <- User.get_by_id(id),
1157 false <- user.info.hide_favorites do
1160 |> Map.put("type", "Create")
1161 |> Map.put("favorited_by", user.ap_id)
1162 |> Map.put("blocking_user", for_user)
1166 ["https://www.w3.org/ns/activitystreams#Public"] ++
1167 [for_user.ap_id | for_user.following]
1169 ["https://www.w3.org/ns/activitystreams#Public"]
1174 |> ActivityPub.fetch_activities(params)
1178 |> add_link_headers(:favourites, activities)
1179 |> put_view(StatusView)
1180 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1183 {:error, :not_found}
1188 |> json(%{error: "Can't get favorites"})
1192 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1193 user = User.get_cached_by_id(user.id)
1196 Bookmark.for_user_query(user.id)
1197 |> Pagination.fetch_paginated(params)
1201 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1204 |> add_link_headers(:bookmarks, bookmarks)
1205 |> put_view(StatusView)
1206 |> render("index.json", %{activities: activities, for: user, as: :activity})
1209 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1210 lists = Pleroma.List.for_user(user, opts)
1211 res = ListView.render("lists.json", lists: lists)
1215 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1216 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1217 res = ListView.render("list.json", list: list)
1223 |> json(%{error: "Record not found"})
1227 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1228 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1229 res = ListView.render("lists.json", lists: lists)
1233 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1234 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1235 {:ok, _list} <- Pleroma.List.delete(list) do
1243 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1244 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1245 res = ListView.render("list.json", list: list)
1250 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1252 |> Enum.each(fn account_id ->
1253 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1254 %User{} = followed <- User.get_cached_by_id(account_id) do
1255 Pleroma.List.follow(list, followed)
1262 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1264 |> Enum.each(fn account_id ->
1265 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1266 %User{} = followed <- User.get_cached_by_id(account_id) do
1267 Pleroma.List.unfollow(list, followed)
1274 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1275 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1276 {:ok, users} = Pleroma.List.get_following(list) do
1278 |> put_view(AccountView)
1279 |> render("accounts.json", %{for: user, users: users, as: :user})
1283 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1284 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1285 {:ok, list} <- Pleroma.List.rename(list, title) do
1286 res = ListView.render("list.json", list: list)
1294 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1295 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1298 |> Map.put("type", "Create")
1299 |> Map.put("blocking_user", user)
1300 |> Map.put("muting_user", user)
1302 # we must filter the following list for the user to avoid leaking statuses the user
1303 # does not actually have permission to see (for more info, peruse security issue #270).
1306 |> Enum.filter(fn x -> x in user.following end)
1307 |> ActivityPub.fetch_activities_bounded(following, params)
1311 |> put_view(StatusView)
1312 |> render("index.json", %{activities: activities, for: user, as: :activity})
1317 |> json(%{error: "Error."})
1321 def index(%{assigns: %{user: user}} = conn, _params) do
1322 token = get_session(conn, :oauth_token)
1325 mastodon_emoji = mastodonized_emoji()
1327 limit = Config.get([:instance, :limit])
1330 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1332 flavour = get_user_flavour(user)
1337 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1338 access_token: token,
1340 domain: Pleroma.Web.Endpoint.host(),
1343 unfollow_modal: false,
1346 auto_play_gif: false,
1347 display_sensitive_media: false,
1348 reduce_motion: false,
1349 max_toot_chars: limit,
1350 mascot: "/images/pleroma-fox-tan-smol.png"
1352 poll_limits: Config.get([:instance, :poll_limits]),
1354 delete_others_notice: present?(user.info.is_moderator),
1355 admin: present?(user.info.is_admin)
1359 default_privacy: user.info.default_scope,
1360 default_sensitive: false,
1361 allow_content_types: Config.get([:instance, :allowed_post_formats])
1363 media_attachments: %{
1364 accept_content_types: [
1380 user.info.settings ||
1410 push_subscription: nil,
1412 custom_emojis: mastodon_emoji,
1418 |> put_layout(false)
1419 |> put_view(MastodonView)
1420 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1423 |> put_session(:return_to, conn.request_path)
1424 |> redirect(to: "/web/login")
1428 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1429 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1431 with changeset <- Ecto.Changeset.change(user),
1432 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1433 {:ok, _user} <- User.update_and_set_cache(changeset) do
1438 |> put_resp_content_type("application/json")
1439 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1443 @supported_flavours ["glitch", "vanilla"]
1445 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1446 when flavour in @supported_flavours do
1447 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1449 with changeset <- Ecto.Changeset.change(user),
1450 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1451 {:ok, user} <- User.update_and_set_cache(changeset),
1452 flavour <- user.info.flavour do
1457 |> put_resp_content_type("application/json")
1458 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1462 def set_flavour(conn, _params) do
1465 |> json(%{error: "Unsupported flavour"})
1468 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1469 json(conn, get_user_flavour(user))
1472 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1476 defp get_user_flavour(_) do
1480 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1481 redirect(conn, to: local_mastodon_root_path(conn))
1484 @doc "Local Mastodon FE login init action"
1485 def login(conn, %{"code" => auth_token}) do
1486 with {:ok, app} <- get_or_make_app(),
1487 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1488 {:ok, token} <- Token.exchange_token(app, auth) do
1490 |> put_session(:oauth_token, token.token)
1491 |> redirect(to: local_mastodon_root_path(conn))
1495 @doc "Local Mastodon FE callback action"
1496 def login(conn, _) do
1497 with {:ok, app} <- get_or_make_app() do
1502 response_type: "code",
1503 client_id: app.client_id,
1505 scope: Enum.join(app.scopes, " ")
1508 redirect(conn, to: path)
1512 defp local_mastodon_root_path(conn) do
1513 case get_session(conn, :return_to) do
1515 mastodon_api_path(conn, :index, ["getting-started"])
1518 delete_session(conn, :return_to)
1523 defp get_or_make_app do
1524 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1525 scopes = ["read", "write", "follow", "push"]
1527 with %App{} = app <- Repo.get_by(App, find_attrs) do
1529 if app.scopes == scopes do
1533 |> Ecto.Changeset.change(%{scopes: scopes})
1541 App.register_changeset(
1543 Map.put(find_attrs, :scopes, scopes)
1550 def logout(conn, _) do
1553 |> redirect(to: "/")
1556 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1557 Logger.debug("Unimplemented, returning unmodified relationship")
1559 with %User{} = target <- User.get_cached_by_id(id) do
1561 |> put_view(AccountView)
1562 |> render("relationship.json", %{user: user, target: target})
1566 def empty_array(conn, _) do
1567 Logger.debug("Unimplemented, returning an empty array")
1571 def empty_object(conn, _) do
1572 Logger.debug("Unimplemented, returning an empty object")
1576 def get_filters(%{assigns: %{user: user}} = conn, _) do
1577 filters = Filter.get_filters(user)
1578 res = FilterView.render("filters.json", filters: filters)
1583 %{assigns: %{user: user}} = conn,
1584 %{"phrase" => phrase, "context" => context} = params
1590 hide: Map.get(params, "irreversible", false),
1591 whole_word: Map.get(params, "boolean", true)
1595 {:ok, response} = Filter.create(query)
1596 res = FilterView.render("filter.json", filter: response)
1600 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1601 filter = Filter.get(filter_id, user)
1602 res = FilterView.render("filter.json", filter: filter)
1607 %{assigns: %{user: user}} = conn,
1608 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1612 filter_id: filter_id,
1615 hide: Map.get(params, "irreversible", nil),
1616 whole_word: Map.get(params, "boolean", true)
1620 {:ok, response} = Filter.update(query)
1621 res = FilterView.render("filter.json", filter: response)
1625 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1628 filter_id: filter_id
1631 {:ok, _} = Filter.delete(query)
1637 def errors(conn, {:error, %Changeset{} = changeset}) do
1640 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1641 |> Enum.map_join(", ", fn {_k, v} -> v end)
1645 |> json(%{error: error_message})
1648 def errors(conn, {:error, :not_found}) do
1651 |> json(%{error: "Record not found"})
1654 def errors(conn, _) do
1657 |> json("Something went wrong")
1660 def suggestions(%{assigns: %{user: user}} = conn, _) do
1661 suggestions = Config.get(:suggestions)
1663 if Keyword.get(suggestions, :enabled, false) do
1664 api = Keyword.get(suggestions, :third_party_engine, "")
1665 timeout = Keyword.get(suggestions, :timeout, 5000)
1666 limit = Keyword.get(suggestions, :limit, 23)
1668 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1670 user = user.nickname
1674 |> String.replace("{{host}}", host)
1675 |> String.replace("{{user}}", user)
1677 with {:ok, %{status: 200, body: body}} <-
1682 recv_timeout: timeout,
1686 {:ok, data} <- Jason.decode(body) do
1689 |> Enum.slice(0, limit)
1694 case User.get_or_fetch(x["acct"]) do
1695 {:ok, %User{id: id}} -> id
1701 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1704 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1710 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1717 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1718 with %Activity{} = activity <- Activity.get_by_id(status_id),
1719 true <- Visibility.visible_for_user?(activity, user) do
1723 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1733 def reports(%{assigns: %{user: user}} = conn, params) do
1734 case CommonAPI.report(user, params) do
1737 |> put_view(ReportView)
1738 |> try_render("report.json", %{activity: activity})
1742 |> put_status(:bad_request)
1743 |> json(%{error: err})
1747 def account_register(
1748 %{assigns: %{app: app}} = conn,
1749 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1757 "captcha_answer_data",
1761 |> Map.put("nickname", nickname)
1762 |> Map.put("fullname", params["fullname"] || nickname)
1763 |> Map.put("bio", params["bio"] || "")
1764 |> Map.put("confirm", params["password"])
1766 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1767 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1769 token_type: "Bearer",
1770 access_token: token.token,
1772 created_at: Token.Utils.format_created_at(token)
1778 |> json(Jason.encode!(errors))
1782 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1785 |> json(%{error: "Missing parameters"})
1788 def account_register(conn, _) do
1791 |> json(%{error: "Invalid credentials"})
1794 def conversations(%{assigns: %{user: user}} = conn, params) do
1795 participations = Participation.for_user_with_last_activity_id(user, params)
1798 Enum.map(participations, fn participation ->
1799 ConversationView.render("participation.json", %{participation: participation, user: user})
1803 |> add_link_headers(:conversations, participations)
1804 |> json(conversations)
1807 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1808 with %Participation{} = participation <-
1809 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1810 {:ok, participation} <- Participation.mark_as_read(participation) do
1811 participation_view =
1812 ConversationView.render("participation.json", %{participation: participation, user: user})
1815 |> json(participation_view)
1819 def try_render(conn, target, params)
1820 when is_binary(target) do
1821 res = render(conn, target, params)
1826 |> json(%{error: "Can't display this activity"})
1832 def try_render(conn, _, _) do
1835 |> json(%{error: "Can't display this activity"})
1838 defp present?(nil), do: false
1839 defp present?(false), do: false
1840 defp present?(_), do: true