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 get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
414 with %Object{} = object <- Object.get_by_id(id),
415 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
416 true <- Visibility.visible_for_user?(activity, user) do
418 |> put_view(StatusView)
419 |> try_render("poll.json", %{object: object, for: user})
424 |> json(%{error: "Record not found"})
429 |> json(%{error: "Record not found"})
433 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
434 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
436 |> add_link_headers(:scheduled_statuses, scheduled_activities)
437 |> put_view(ScheduledActivityView)
438 |> render("index.json", %{scheduled_activities: scheduled_activities})
442 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
443 with %ScheduledActivity{} = scheduled_activity <-
444 ScheduledActivity.get(user, scheduled_activity_id) do
446 |> put_view(ScheduledActivityView)
447 |> render("show.json", %{scheduled_activity: scheduled_activity})
449 _ -> {:error, :not_found}
453 def update_scheduled_status(
454 %{assigns: %{user: user}} = conn,
455 %{"id" => scheduled_activity_id} = params
457 with %ScheduledActivity{} = scheduled_activity <-
458 ScheduledActivity.get(user, scheduled_activity_id),
459 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
461 |> put_view(ScheduledActivityView)
462 |> render("show.json", %{scheduled_activity: scheduled_activity})
464 nil -> {:error, :not_found}
469 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
470 with %ScheduledActivity{} = scheduled_activity <-
471 ScheduledActivity.get(user, scheduled_activity_id),
472 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
474 |> put_view(ScheduledActivityView)
475 |> render("show.json", %{scheduled_activity: scheduled_activity})
477 nil -> {:error, :not_found}
482 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
483 when length(media_ids) > 0 do
486 |> Map.put("status", ".")
488 post_status(conn, params)
491 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
494 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
496 scheduled_at = params["scheduled_at"]
498 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
499 with {:ok, scheduled_activity} <-
500 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
502 |> put_view(ScheduledActivityView)
503 |> render("show.json", %{scheduled_activity: scheduled_activity})
506 params = Map.drop(params, ["scheduled_at"])
508 case get_cached_status_or_post(conn, params) do
509 {:ignore, message} ->
512 |> json(%{error: message})
517 |> json(%{error: message})
521 |> put_view(StatusView)
522 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
527 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
529 case get_req_header(conn, "idempotency-key") do
531 _ -> Ecto.UUID.generate()
534 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
535 case CommonAPI.post(user, params) do
536 {:ok, activity} -> activity
537 {:error, message} -> {:ignore, message}
542 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
543 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
549 |> json(%{error: "Can't delete this post"})
553 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
554 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
555 %Activity{} = announce <- Activity.normalize(announce.data) do
557 |> put_view(StatusView)
558 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
562 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
563 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
564 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
566 |> put_view(StatusView)
567 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
571 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
572 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
573 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
575 |> put_view(StatusView)
576 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
580 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
581 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
582 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
584 |> put_view(StatusView)
585 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
589 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
590 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
592 |> put_view(StatusView)
593 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
597 |> put_resp_content_type("application/json")
598 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
602 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
603 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
605 |> put_view(StatusView)
606 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
610 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
611 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
612 %User{} = user <- User.get_cached_by_nickname(user.nickname),
613 true <- Visibility.visible_for_user?(activity, user),
614 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
616 |> put_view(StatusView)
617 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
621 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
622 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
623 %User{} = user <- User.get_cached_by_nickname(user.nickname),
624 true <- Visibility.visible_for_user?(activity, user),
625 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
627 |> put_view(StatusView)
628 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
632 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
633 activity = Activity.get_by_id(id)
635 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
637 |> put_view(StatusView)
638 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
642 |> put_resp_content_type("application/json")
643 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
647 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
648 activity = Activity.get_by_id(id)
650 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def notifications(%{assigns: %{user: user}} = conn, params) do
658 notifications = MastodonAPI.get_notifications(user, params)
661 |> add_link_headers(:notifications, notifications)
662 |> put_view(NotificationView)
663 |> render("index.json", %{notifications: notifications, for: user})
666 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
667 with {:ok, notification} <- Notification.get(user, id) do
669 |> put_view(NotificationView)
670 |> render("show.json", %{notification: notification, for: user})
674 |> put_resp_content_type("application/json")
675 |> send_resp(403, Jason.encode!(%{"error" => reason}))
679 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
680 Notification.clear(user)
684 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
685 with {:ok, _notif} <- Notification.dismiss(user, id) do
690 |> put_resp_content_type("application/json")
691 |> send_resp(403, Jason.encode!(%{"error" => reason}))
695 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
696 Notification.destroy_multiple(user, ids)
700 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
702 q = from(u in User, where: u.id in ^id)
703 targets = Repo.all(q)
706 |> put_view(AccountView)
707 |> render("relationships.json", %{user: user, targets: targets})
710 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
711 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
713 def update_media(%{assigns: %{user: user}} = conn, data) do
714 with %Object{} = object <- Repo.get(Object, data["id"]),
715 true <- Object.authorize_mutation(object, user),
716 true <- is_binary(data["description"]),
717 description <- data["description"] do
718 new_data = %{object.data | "name" => description}
722 |> Object.change(%{data: new_data})
725 attachment_data = Map.put(new_data, "id", object.id)
728 |> put_view(StatusView)
729 |> render("attachment.json", %{attachment: attachment_data})
733 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
734 with {:ok, object} <-
737 actor: User.ap_id(user),
738 description: Map.get(data, "description")
740 attachment_data = Map.put(object.data, "id", object.id)
743 |> put_view(StatusView)
744 |> render("attachment.json", %{attachment: attachment_data})
748 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
749 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
750 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
751 q = from(u in User, where: u.ap_id in ^likes)
755 |> put_view(AccountView)
756 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
762 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
763 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
764 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
765 q = from(u in User, where: u.ap_id in ^announces)
769 |> put_view(AccountView)
770 |> render("accounts.json", %{for: user, users: users, as: :user})
776 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
777 local_only = params["local"] in [true, "True", "true", "1"]
780 [params["tag"], params["any"]]
784 |> Enum.map(&String.downcase(&1))
789 |> Enum.map(&String.downcase(&1))
794 |> Enum.map(&String.downcase(&1))
798 |> Map.put("type", "Create")
799 |> Map.put("local_only", local_only)
800 |> Map.put("blocking_user", user)
801 |> Map.put("muting_user", user)
802 |> Map.put("tag", tags)
803 |> Map.put("tag_all", tag_all)
804 |> Map.put("tag_reject", tag_reject)
805 |> ActivityPub.fetch_public_activities()
809 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
810 |> put_view(StatusView)
811 |> render("index.json", %{activities: activities, for: user, as: :activity})
814 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
815 with %User{} = user <- User.get_cached_by_id(id),
816 followers <- MastodonAPI.get_followers(user, params) do
819 for_user && user.id == for_user.id -> followers
820 user.info.hide_followers -> []
825 |> add_link_headers(:followers, followers, user)
826 |> put_view(AccountView)
827 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
831 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
832 with %User{} = user <- User.get_cached_by_id(id),
833 followers <- MastodonAPI.get_friends(user, params) do
836 for_user && user.id == for_user.id -> followers
837 user.info.hide_follows -> []
842 |> add_link_headers(:following, followers, user)
843 |> put_view(AccountView)
844 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
848 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
849 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
851 |> put_view(AccountView)
852 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
856 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
857 with %User{} = follower <- User.get_cached_by_id(id),
858 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
860 |> put_view(AccountView)
861 |> render("relationship.json", %{user: followed, target: follower})
865 |> put_resp_content_type("application/json")
866 |> send_resp(403, Jason.encode!(%{"error" => message}))
870 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
871 with %User{} = follower <- User.get_cached_by_id(id),
872 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
874 |> put_view(AccountView)
875 |> render("relationship.json", %{user: followed, target: follower})
879 |> put_resp_content_type("application/json")
880 |> send_resp(403, Jason.encode!(%{"error" => message}))
884 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
885 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
886 {_, true} <- {:followed, follower.id != followed.id},
887 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
889 |> put_view(AccountView)
890 |> render("relationship.json", %{user: follower, target: followed})
897 |> put_resp_content_type("application/json")
898 |> send_resp(403, Jason.encode!(%{"error" => message}))
902 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
903 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
904 {_, true} <- {:followed, follower.id != followed.id},
905 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
907 |> put_view(AccountView)
908 |> render("account.json", %{user: followed, for: follower})
915 |> put_resp_content_type("application/json")
916 |> send_resp(403, Jason.encode!(%{"error" => message}))
920 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
921 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
922 {_, true} <- {:followed, follower.id != followed.id},
923 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
925 |> put_view(AccountView)
926 |> render("relationship.json", %{user: follower, target: followed})
936 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
937 with %User{} = muted <- User.get_cached_by_id(id),
938 {:ok, muter} <- User.mute(muter, muted) do
940 |> put_view(AccountView)
941 |> render("relationship.json", %{user: muter, target: muted})
945 |> put_resp_content_type("application/json")
946 |> send_resp(403, Jason.encode!(%{"error" => message}))
950 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
951 with %User{} = muted <- User.get_cached_by_id(id),
952 {:ok, muter} <- User.unmute(muter, muted) do
954 |> put_view(AccountView)
955 |> render("relationship.json", %{user: muter, target: muted})
959 |> put_resp_content_type("application/json")
960 |> send_resp(403, Jason.encode!(%{"error" => message}))
964 def mutes(%{assigns: %{user: user}} = conn, _) do
965 with muted_accounts <- User.muted_users(user) do
966 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
971 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
972 with %User{} = blocked <- User.get_cached_by_id(id),
973 {:ok, blocker} <- User.block(blocker, blocked),
974 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: blocker, target: blocked})
981 |> put_resp_content_type("application/json")
982 |> send_resp(403, Jason.encode!(%{"error" => message}))
986 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
987 with %User{} = blocked <- User.get_cached_by_id(id),
988 {:ok, blocker} <- User.unblock(blocker, blocked),
989 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
991 |> put_view(AccountView)
992 |> render("relationship.json", %{user: blocker, target: blocked})
996 |> put_resp_content_type("application/json")
997 |> send_resp(403, Jason.encode!(%{"error" => message}))
1001 def blocks(%{assigns: %{user: user}} = conn, _) do
1002 with blocked_accounts <- User.blocked_users(user) do
1003 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1008 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1009 json(conn, info.domain_blocks || [])
1012 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1013 User.block_domain(blocker, domain)
1017 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1018 User.unblock_domain(blocker, domain)
1022 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1023 with %User{} = subscription_target <- User.get_cached_by_id(id),
1024 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1026 |> put_view(AccountView)
1027 |> render("relationship.json", %{user: user, target: subscription_target})
1029 {:error, message} ->
1031 |> put_resp_content_type("application/json")
1032 |> send_resp(403, Jason.encode!(%{"error" => message}))
1036 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1037 with %User{} = subscription_target <- User.get_cached_by_id(id),
1038 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1040 |> put_view(AccountView)
1041 |> render("relationship.json", %{user: user, target: subscription_target})
1043 {:error, message} ->
1045 |> put_resp_content_type("application/json")
1046 |> send_resp(403, Jason.encode!(%{"error" => message}))
1050 def status_search_query_with_gin(q, query) do
1054 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1058 order_by: [desc: :id]
1062 def status_search_query_with_rum(q, query) do
1066 "? @@ plainto_tsquery('english', ?)",
1070 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1074 def status_search(user, query) do
1076 if Regex.match?(~r/https?:/, query) do
1077 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1078 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1079 true <- Visibility.visible_for_user?(activity, user) do
1087 from([a, o] in Activity.with_preloaded_object(Activity),
1088 where: fragment("?->>'type' = 'Create'", a.data),
1089 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1094 if Pleroma.Config.get([:database, :rum_enabled]) do
1095 status_search_query_with_rum(q, query)
1097 status_search_query_with_gin(q, query)
1100 Repo.all(q) ++ fetched
1103 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1104 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1106 statuses = status_search(user, query)
1108 tags_path = Web.base_url() <> "/tag/"
1114 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1115 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1116 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1119 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1121 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1128 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1129 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1131 statuses = status_search(user, query)
1137 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1138 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1141 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1143 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1150 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1151 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1153 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1158 def favourites(%{assigns: %{user: user}} = conn, params) do
1161 |> Map.put("type", "Create")
1162 |> Map.put("favorited_by", user.ap_id)
1163 |> Map.put("blocking_user", user)
1166 ActivityPub.fetch_activities([], params)
1170 |> add_link_headers(:favourites, activities)
1171 |> put_view(StatusView)
1172 |> render("index.json", %{activities: activities, for: user, as: :activity})
1175 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1176 with %User{} = user <- User.get_by_id(id),
1177 false <- user.info.hide_favorites do
1180 |> Map.put("type", "Create")
1181 |> Map.put("favorited_by", user.ap_id)
1182 |> Map.put("blocking_user", for_user)
1186 ["https://www.w3.org/ns/activitystreams#Public"] ++
1187 [for_user.ap_id | for_user.following]
1189 ["https://www.w3.org/ns/activitystreams#Public"]
1194 |> ActivityPub.fetch_activities(params)
1198 |> add_link_headers(:favourites, activities)
1199 |> put_view(StatusView)
1200 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1203 {:error, :not_found}
1208 |> json(%{error: "Can't get favorites"})
1212 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1213 user = User.get_cached_by_id(user.id)
1216 Bookmark.for_user_query(user.id)
1217 |> Pagination.fetch_paginated(params)
1221 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1224 |> add_link_headers(:bookmarks, bookmarks)
1225 |> put_view(StatusView)
1226 |> render("index.json", %{activities: activities, for: user, as: :activity})
1229 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1230 lists = Pleroma.List.for_user(user, opts)
1231 res = ListView.render("lists.json", lists: lists)
1235 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1236 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1237 res = ListView.render("list.json", list: list)
1243 |> json(%{error: "Record not found"})
1247 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1248 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1249 res = ListView.render("lists.json", lists: lists)
1253 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1254 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1255 {:ok, _list} <- Pleroma.List.delete(list) do
1263 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1264 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1265 res = ListView.render("list.json", list: list)
1270 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1272 |> Enum.each(fn account_id ->
1273 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1274 %User{} = followed <- User.get_cached_by_id(account_id) do
1275 Pleroma.List.follow(list, followed)
1282 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1284 |> Enum.each(fn account_id ->
1285 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1286 %User{} = followed <- User.get_cached_by_id(account_id) do
1287 Pleroma.List.unfollow(list, followed)
1294 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1295 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1296 {:ok, users} = Pleroma.List.get_following(list) do
1298 |> put_view(AccountView)
1299 |> render("accounts.json", %{for: user, users: users, as: :user})
1303 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1304 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1305 {:ok, list} <- Pleroma.List.rename(list, title) do
1306 res = ListView.render("list.json", list: list)
1314 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1315 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1318 |> Map.put("type", "Create")
1319 |> Map.put("blocking_user", user)
1320 |> Map.put("muting_user", user)
1322 # we must filter the following list for the user to avoid leaking statuses the user
1323 # does not actually have permission to see (for more info, peruse security issue #270).
1326 |> Enum.filter(fn x -> x in user.following end)
1327 |> ActivityPub.fetch_activities_bounded(following, params)
1331 |> put_view(StatusView)
1332 |> render("index.json", %{activities: activities, for: user, as: :activity})
1337 |> json(%{error: "Error."})
1341 def index(%{assigns: %{user: user}} = conn, _params) do
1342 token = get_session(conn, :oauth_token)
1345 mastodon_emoji = mastodonized_emoji()
1347 limit = Config.get([:instance, :limit])
1350 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1352 flavour = get_user_flavour(user)
1357 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1358 access_token: token,
1360 domain: Pleroma.Web.Endpoint.host(),
1363 unfollow_modal: false,
1366 auto_play_gif: false,
1367 display_sensitive_media: false,
1368 reduce_motion: false,
1369 max_toot_chars: limit,
1370 mascot: "/images/pleroma-fox-tan-smol.png"
1372 poll_limits: Config.get([:instance, :poll_limits]),
1374 delete_others_notice: present?(user.info.is_moderator),
1375 admin: present?(user.info.is_admin)
1379 default_privacy: user.info.default_scope,
1380 default_sensitive: false,
1381 allow_content_types: Config.get([:instance, :allowed_post_formats])
1383 media_attachments: %{
1384 accept_content_types: [
1400 user.info.settings ||
1430 push_subscription: nil,
1432 custom_emojis: mastodon_emoji,
1438 |> put_layout(false)
1439 |> put_view(MastodonView)
1440 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1443 |> put_session(:return_to, conn.request_path)
1444 |> redirect(to: "/web/login")
1448 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1449 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1451 with changeset <- Ecto.Changeset.change(user),
1452 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1453 {:ok, _user} <- User.update_and_set_cache(changeset) do
1458 |> put_resp_content_type("application/json")
1459 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1463 @supported_flavours ["glitch", "vanilla"]
1465 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1466 when flavour in @supported_flavours do
1467 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1469 with changeset <- Ecto.Changeset.change(user),
1470 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1471 {:ok, user} <- User.update_and_set_cache(changeset),
1472 flavour <- user.info.flavour do
1477 |> put_resp_content_type("application/json")
1478 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1482 def set_flavour(conn, _params) do
1485 |> json(%{error: "Unsupported flavour"})
1488 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1489 json(conn, get_user_flavour(user))
1492 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1496 defp get_user_flavour(_) do
1500 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1501 redirect(conn, to: local_mastodon_root_path(conn))
1504 @doc "Local Mastodon FE login init action"
1505 def login(conn, %{"code" => auth_token}) do
1506 with {:ok, app} <- get_or_make_app(),
1507 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1508 {:ok, token} <- Token.exchange_token(app, auth) do
1510 |> put_session(:oauth_token, token.token)
1511 |> redirect(to: local_mastodon_root_path(conn))
1515 @doc "Local Mastodon FE callback action"
1516 def login(conn, _) do
1517 with {:ok, app} <- get_or_make_app() do
1522 response_type: "code",
1523 client_id: app.client_id,
1525 scope: Enum.join(app.scopes, " ")
1528 redirect(conn, to: path)
1532 defp local_mastodon_root_path(conn) do
1533 case get_session(conn, :return_to) do
1535 mastodon_api_path(conn, :index, ["getting-started"])
1538 delete_session(conn, :return_to)
1543 defp get_or_make_app do
1544 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1545 scopes = ["read", "write", "follow", "push"]
1547 with %App{} = app <- Repo.get_by(App, find_attrs) do
1549 if app.scopes == scopes do
1553 |> Ecto.Changeset.change(%{scopes: scopes})
1561 App.register_changeset(
1563 Map.put(find_attrs, :scopes, scopes)
1570 def logout(conn, _) do
1573 |> redirect(to: "/")
1576 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1577 Logger.debug("Unimplemented, returning unmodified relationship")
1579 with %User{} = target <- User.get_cached_by_id(id) do
1581 |> put_view(AccountView)
1582 |> render("relationship.json", %{user: user, target: target})
1586 def empty_array(conn, _) do
1587 Logger.debug("Unimplemented, returning an empty array")
1591 def empty_object(conn, _) do
1592 Logger.debug("Unimplemented, returning an empty object")
1596 def get_filters(%{assigns: %{user: user}} = conn, _) do
1597 filters = Filter.get_filters(user)
1598 res = FilterView.render("filters.json", filters: filters)
1603 %{assigns: %{user: user}} = conn,
1604 %{"phrase" => phrase, "context" => context} = params
1610 hide: Map.get(params, "irreversible", false),
1611 whole_word: Map.get(params, "boolean", true)
1615 {:ok, response} = Filter.create(query)
1616 res = FilterView.render("filter.json", filter: response)
1620 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1621 filter = Filter.get(filter_id, user)
1622 res = FilterView.render("filter.json", filter: filter)
1627 %{assigns: %{user: user}} = conn,
1628 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1632 filter_id: filter_id,
1635 hide: Map.get(params, "irreversible", nil),
1636 whole_word: Map.get(params, "boolean", true)
1640 {:ok, response} = Filter.update(query)
1641 res = FilterView.render("filter.json", filter: response)
1645 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1648 filter_id: filter_id
1651 {:ok, _} = Filter.delete(query)
1657 def errors(conn, {:error, %Changeset{} = changeset}) do
1660 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1661 |> Enum.map_join(", ", fn {_k, v} -> v end)
1665 |> json(%{error: error_message})
1668 def errors(conn, {:error, :not_found}) do
1671 |> json(%{error: "Record not found"})
1674 def errors(conn, _) do
1677 |> json("Something went wrong")
1680 def suggestions(%{assigns: %{user: user}} = conn, _) do
1681 suggestions = Config.get(:suggestions)
1683 if Keyword.get(suggestions, :enabled, false) do
1684 api = Keyword.get(suggestions, :third_party_engine, "")
1685 timeout = Keyword.get(suggestions, :timeout, 5000)
1686 limit = Keyword.get(suggestions, :limit, 23)
1688 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1690 user = user.nickname
1694 |> String.replace("{{host}}", host)
1695 |> String.replace("{{user}}", user)
1697 with {:ok, %{status: 200, body: body}} <-
1702 recv_timeout: timeout,
1706 {:ok, data} <- Jason.decode(body) do
1709 |> Enum.slice(0, limit)
1714 case User.get_or_fetch(x["acct"]) do
1715 {:ok, %User{id: id}} -> id
1721 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1724 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1730 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1737 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1738 with %Activity{} = activity <- Activity.get_by_id(status_id),
1739 true <- Visibility.visible_for_user?(activity, user) do
1743 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1753 def reports(%{assigns: %{user: user}} = conn, params) do
1754 case CommonAPI.report(user, params) do
1757 |> put_view(ReportView)
1758 |> try_render("report.json", %{activity: activity})
1762 |> put_status(:bad_request)
1763 |> json(%{error: err})
1767 def account_register(
1768 %{assigns: %{app: app}} = conn,
1769 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1777 "captcha_answer_data",
1781 |> Map.put("nickname", nickname)
1782 |> Map.put("fullname", params["fullname"] || nickname)
1783 |> Map.put("bio", params["bio"] || "")
1784 |> Map.put("confirm", params["password"])
1786 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1787 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1789 token_type: "Bearer",
1790 access_token: token.token,
1792 created_at: Token.Utils.format_created_at(token)
1798 |> json(Jason.encode!(errors))
1802 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1805 |> json(%{error: "Missing parameters"})
1808 def account_register(conn, _) do
1811 |> json(%{error: "Invalid credentials"})
1814 def conversations(%{assigns: %{user: user}} = conn, params) do
1815 participations = Participation.for_user_with_last_activity_id(user, params)
1818 Enum.map(participations, fn participation ->
1819 ConversationView.render("participation.json", %{participation: participation, user: user})
1823 |> add_link_headers(:conversations, participations)
1824 |> json(conversations)
1827 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1828 with %Participation{} = participation <-
1829 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1830 {:ok, participation} <- Participation.mark_as_read(participation) do
1831 participation_view =
1832 ConversationView.render("participation.json", %{participation: participation, user: user})
1835 |> json(participation_view)
1839 def try_render(conn, target, params)
1840 when is_binary(target) do
1841 res = render(conn, target, params)
1846 |> json(%{error: "Can't display this activity"})
1852 def try_render(conn, _, _) do
1855 |> json(%{error: "Can't display this activity"})
1858 defp present?(nil), do: false
1859 defp present?(false), do: false
1860 defp present?(_), do: true