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
43 alias Pleroma.Web.ControllerHelper
48 @httpoison Application.get_env(:pleroma, :httpoison)
49 @local_mastodon_name "Mastodon-Local"
51 action_fallback(:errors)
53 def create_app(conn, params) do
54 scopes = Scopes.fetch_scopes(params, ["read"])
58 |> Map.drop(["scope", "scopes"])
59 |> Map.put("scopes", scopes)
61 with cs <- App.register_changeset(%App{}, app_attrs),
62 false <- cs.changes[:client_name] == @local_mastodon_name,
63 {:ok, app} <- Repo.insert(cs) do
66 |> render("show.json", %{app: app})
75 value_function \\ fn x -> {:ok, x} end
77 if Map.has_key?(params, params_field) do
78 case value_function.(params[params_field]) do
79 {:ok, new_value} -> Map.put(map, map_field, new_value)
87 def update_credentials(%{assigns: %{user: user}} = conn, params) do
92 |> add_if_present(params, "display_name", :name)
93 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
94 |> add_if_present(params, "avatar", :avatar, fn value ->
95 with %Plug.Upload{} <- value,
96 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
103 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
106 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
110 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
111 |> Enum.reduce(%{}, fn key, acc ->
112 add_if_present(acc, params, to_string(key), key, fn value ->
113 {:ok, ControllerHelper.truthy_param?(value)}
116 |> add_if_present(params, "default_scope", :default_scope)
117 |> add_if_present(params, "header", :banner, fn value ->
118 with %Plug.Upload{} <- value,
119 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
125 |> Map.put(:emoji, user_info_emojis)
127 info_cng = User.Info.profile_update(user.info, info_params)
129 with changeset <- User.update_changeset(user, user_params),
130 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
131 {:ok, user} <- User.update_and_set_cache(changeset) do
132 if original_user != user do
133 CommonAPI.update(user)
136 json(conn, AccountView.render("account.json", %{user: user, for: user}))
141 |> json(%{error: "Invalid request"})
145 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
146 account = AccountView.render("account.json", %{user: user, for: user})
150 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
151 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
154 |> render("short.json", %{app: app})
158 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
159 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
160 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
161 account = AccountView.render("account.json", %{user: user, for: for_user})
167 |> json(%{error: "Can't find user"})
171 @mastodon_api_level "2.6.5"
173 def masto_instance(conn, _params) do
174 instance = Config.get(:instance)
178 title: Keyword.get(instance, :name),
179 description: Keyword.get(instance, :description),
180 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
181 email: Keyword.get(instance, :email),
183 streaming_api: Pleroma.Web.Endpoint.websocket_url()
185 stats: Stats.get_stats(),
186 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
188 registrations: Pleroma.Config.get([:instance, :registrations_open]),
189 # Extra (not present in Mastodon):
190 max_toot_chars: Keyword.get(instance, :limit)
196 def peers(conn, _params) do
197 json(conn, Stats.get_peers())
200 defp mastodonized_emoji do
201 Pleroma.Emoji.get_all()
202 |> Enum.map(fn {shortcode, relative_url, tags} ->
203 url = to_string(URI.merge(Web.base_url(), relative_url))
206 "shortcode" => shortcode,
208 "visible_in_picker" => true,
215 def custom_emojis(conn, _params) do
216 mastodon_emoji = mastodonized_emoji()
217 json(conn, mastodon_emoji)
220 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
223 |> Map.drop(["since_id", "max_id", "min_id"])
226 last = List.last(activities)
233 |> Map.get("limit", "20")
234 |> String.to_integer()
237 if length(activities) <= limit do
243 |> Enum.at(limit * -1)
247 {next_url, prev_url} =
251 Pleroma.Web.Endpoint,
254 Map.merge(params, %{max_id: max_id})
257 Pleroma.Web.Endpoint,
260 Map.merge(params, %{min_id: min_id})
266 Pleroma.Web.Endpoint,
268 Map.merge(params, %{max_id: max_id})
271 Pleroma.Web.Endpoint,
273 Map.merge(params, %{min_id: min_id})
279 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
285 def home_timeline(%{assigns: %{user: user}} = conn, params) do
288 |> Map.put("type", ["Create", "Announce"])
289 |> Map.put("blocking_user", user)
290 |> Map.put("muting_user", user)
291 |> Map.put("user", user)
294 [user.ap_id | user.following]
295 |> ActivityPub.fetch_activities(params)
296 |> ActivityPub.contain_timeline(user)
300 |> add_link_headers(:home_timeline, activities)
301 |> put_view(StatusView)
302 |> render("index.json", %{activities: activities, for: user, as: :activity})
305 def public_timeline(%{assigns: %{user: user}} = conn, params) do
306 local_only = params["local"] in [true, "True", "true", "1"]
310 |> Map.put("type", ["Create", "Announce"])
311 |> Map.put("local_only", local_only)
312 |> Map.put("blocking_user", user)
313 |> Map.put("muting_user", user)
314 |> ActivityPub.fetch_public_activities()
318 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
319 |> put_view(StatusView)
320 |> render("index.json", %{activities: activities, for: user, as: :activity})
323 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
324 with %User{} = user <- User.get_cached_by_id(params["id"]) do
325 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
328 |> add_link_headers(:user_statuses, activities, params["id"])
329 |> put_view(StatusView)
330 |> render("index.json", %{
331 activities: activities,
338 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
341 |> Map.put("type", "Create")
342 |> Map.put("blocking_user", user)
343 |> Map.put("user", user)
344 |> Map.put(:visibility, "direct")
348 |> ActivityPub.fetch_activities_query(params)
349 |> Pagination.fetch_paginated(params)
352 |> add_link_headers(:dm_timeline, activities)
353 |> put_view(StatusView)
354 |> render("index.json", %{activities: activities, for: user, as: :activity})
357 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
358 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
359 true <- Visibility.visible_for_user?(activity, user) do
361 |> put_view(StatusView)
362 |> try_render("status.json", %{activity: activity, for: user})
366 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
367 with %Activity{} = activity <- Activity.get_by_id(id),
369 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
370 "blocking_user" => user,
374 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
376 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
377 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
383 activities: grouped_activities[true] || [],
387 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
392 activities: grouped_activities[false] || [],
396 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
403 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
404 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
406 |> add_link_headers(:scheduled_statuses, scheduled_activities)
407 |> put_view(ScheduledActivityView)
408 |> render("index.json", %{scheduled_activities: scheduled_activities})
412 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
413 with %ScheduledActivity{} = scheduled_activity <-
414 ScheduledActivity.get(user, scheduled_activity_id) do
416 |> put_view(ScheduledActivityView)
417 |> render("show.json", %{scheduled_activity: scheduled_activity})
419 _ -> {:error, :not_found}
423 def update_scheduled_status(
424 %{assigns: %{user: user}} = conn,
425 %{"id" => scheduled_activity_id} = params
427 with %ScheduledActivity{} = scheduled_activity <-
428 ScheduledActivity.get(user, scheduled_activity_id),
429 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
431 |> put_view(ScheduledActivityView)
432 |> render("show.json", %{scheduled_activity: scheduled_activity})
434 nil -> {:error, :not_found}
439 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
440 with %ScheduledActivity{} = scheduled_activity <-
441 ScheduledActivity.get(user, scheduled_activity_id),
442 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
444 |> put_view(ScheduledActivityView)
445 |> render("show.json", %{scheduled_activity: scheduled_activity})
447 nil -> {:error, :not_found}
452 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
453 when length(media_ids) > 0 do
456 |> Map.put("status", ".")
458 post_status(conn, params)
461 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
464 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
467 case get_req_header(conn, "idempotency-key") do
469 _ -> Ecto.UUID.generate()
472 scheduled_at = params["scheduled_at"]
474 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
475 with {:ok, scheduled_activity} <-
476 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
478 |> put_view(ScheduledActivityView)
479 |> render("show.json", %{scheduled_activity: scheduled_activity})
482 params = Map.drop(params, ["scheduled_at"])
485 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
486 CommonAPI.post(user, params)
490 |> put_view(StatusView)
491 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
495 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
496 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
502 |> json(%{error: "Can't delete this post"})
506 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
507 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
508 %Activity{} = announce <- Activity.normalize(announce.data) do
510 |> put_view(StatusView)
511 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
515 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
524 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
526 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
528 |> put_view(StatusView)
529 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
533 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
534 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
535 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
537 |> put_view(StatusView)
538 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
542 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
543 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
545 |> put_view(StatusView)
546 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
550 |> put_resp_content_type("application/json")
551 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
555 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
556 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
558 |> put_view(StatusView)
559 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
563 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
564 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
565 %User{} = user <- User.get_cached_by_nickname(user.nickname),
566 true <- Visibility.visible_for_user?(activity, user),
567 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
569 |> put_view(StatusView)
570 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
574 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
575 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
576 %User{} = user <- User.get_cached_by_nickname(user.nickname),
577 true <- Visibility.visible_for_user?(activity, user),
578 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
580 |> put_view(StatusView)
581 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
585 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
586 activity = Activity.get_by_id(id)
588 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
590 |> put_view(StatusView)
591 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
595 |> put_resp_content_type("application/json")
596 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
600 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
601 activity = Activity.get_by_id(id)
603 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
605 |> put_view(StatusView)
606 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
610 def notifications(%{assigns: %{user: user}} = conn, params) do
611 notifications = MastodonAPI.get_notifications(user, params)
614 |> add_link_headers(:notifications, notifications)
615 |> put_view(NotificationView)
616 |> render("index.json", %{notifications: notifications, for: user})
619 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
620 with {:ok, notification} <- Notification.get(user, id) do
622 |> put_view(NotificationView)
623 |> render("show.json", %{notification: notification, for: user})
627 |> put_resp_content_type("application/json")
628 |> send_resp(403, Jason.encode!(%{"error" => reason}))
632 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
633 Notification.clear(user)
637 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
638 with {:ok, _notif} <- Notification.dismiss(user, id) do
643 |> put_resp_content_type("application/json")
644 |> send_resp(403, Jason.encode!(%{"error" => reason}))
648 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
649 Notification.destroy_multiple(user, ids)
653 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
655 q = from(u in User, where: u.id in ^id)
656 targets = Repo.all(q)
659 |> put_view(AccountView)
660 |> render("relationships.json", %{user: user, targets: targets})
663 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
664 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
666 def update_media(%{assigns: %{user: user}} = conn, data) do
667 with %Object{} = object <- Repo.get(Object, data["id"]),
668 true <- Object.authorize_mutation(object, user),
669 true <- is_binary(data["description"]),
670 description <- data["description"] do
671 new_data = %{object.data | "name" => description}
675 |> Object.change(%{data: new_data})
678 attachment_data = Map.put(new_data, "id", object.id)
681 |> put_view(StatusView)
682 |> render("attachment.json", %{attachment: attachment_data})
686 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
687 with {:ok, object} <-
690 actor: User.ap_id(user),
691 description: Map.get(data, "description")
693 attachment_data = Map.put(object.data, "id", object.id)
696 |> put_view(StatusView)
697 |> render("attachment.json", %{attachment: attachment_data})
701 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
702 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
703 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
704 q = from(u in User, where: u.ap_id in ^likes)
708 |> put_view(AccountView)
709 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
715 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
716 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
717 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
718 q = from(u in User, where: u.ap_id in ^announces)
722 |> put_view(AccountView)
723 |> render("accounts.json", %{for: user, users: users, as: :user})
729 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
730 local_only = params["local"] in [true, "True", "true", "1"]
733 [params["tag"], params["any"]]
737 |> Enum.map(&String.downcase(&1))
742 |> Enum.map(&String.downcase(&1))
747 |> Enum.map(&String.downcase(&1))
751 |> Map.put("type", "Create")
752 |> Map.put("local_only", local_only)
753 |> Map.put("blocking_user", user)
754 |> Map.put("muting_user", user)
755 |> Map.put("tag", tags)
756 |> Map.put("tag_all", tag_all)
757 |> Map.put("tag_reject", tag_reject)
758 |> ActivityPub.fetch_public_activities()
762 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
763 |> put_view(StatusView)
764 |> render("index.json", %{activities: activities, for: user, as: :activity})
767 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
768 with %User{} = user <- User.get_cached_by_id(id),
769 followers <- MastodonAPI.get_followers(user, params) do
772 for_user && user.id == for_user.id -> followers
773 user.info.hide_followers -> []
778 |> add_link_headers(:followers, followers, user)
779 |> put_view(AccountView)
780 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
784 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
785 with %User{} = user <- User.get_cached_by_id(id),
786 followers <- MastodonAPI.get_friends(user, params) do
789 for_user && user.id == for_user.id -> followers
790 user.info.hide_follows -> []
795 |> add_link_headers(:following, followers, user)
796 |> put_view(AccountView)
797 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
801 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
802 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
804 |> put_view(AccountView)
805 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
809 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
810 with %User{} = follower <- User.get_cached_by_id(id),
811 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
813 |> put_view(AccountView)
814 |> render("relationship.json", %{user: followed, target: follower})
818 |> put_resp_content_type("application/json")
819 |> send_resp(403, Jason.encode!(%{"error" => message}))
823 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
824 with %User{} = follower <- User.get_cached_by_id(id),
825 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
827 |> put_view(AccountView)
828 |> render("relationship.json", %{user: followed, target: follower})
832 |> put_resp_content_type("application/json")
833 |> send_resp(403, Jason.encode!(%{"error" => message}))
837 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
838 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
839 {_, true} <- {:followed, follower.id != followed.id},
840 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
842 |> put_view(AccountView)
843 |> render("relationship.json", %{user: follower, target: followed})
850 |> put_resp_content_type("application/json")
851 |> send_resp(403, Jason.encode!(%{"error" => message}))
855 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
856 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
857 {_, true} <- {:followed, follower.id != followed.id},
858 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
860 |> put_view(AccountView)
861 |> render("account.json", %{user: followed, for: follower})
868 |> put_resp_content_type("application/json")
869 |> send_resp(403, Jason.encode!(%{"error" => message}))
873 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
874 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
875 {_, true} <- {:followed, follower.id != followed.id},
876 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
878 |> put_view(AccountView)
879 |> render("relationship.json", %{user: follower, target: followed})
889 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
890 with %User{} = muted <- User.get_cached_by_id(id),
891 {:ok, muter} <- User.mute(muter, muted) do
893 |> put_view(AccountView)
894 |> render("relationship.json", %{user: muter, target: muted})
898 |> put_resp_content_type("application/json")
899 |> send_resp(403, Jason.encode!(%{"error" => message}))
903 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
904 with %User{} = muted <- User.get_cached_by_id(id),
905 {:ok, muter} <- User.unmute(muter, muted) do
907 |> put_view(AccountView)
908 |> render("relationship.json", %{user: muter, target: muted})
912 |> put_resp_content_type("application/json")
913 |> send_resp(403, Jason.encode!(%{"error" => message}))
917 def mutes(%{assigns: %{user: user}} = conn, _) do
918 with muted_accounts <- User.muted_users(user) do
919 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
924 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
925 with %User{} = blocked <- User.get_cached_by_id(id),
926 {:ok, blocker} <- User.block(blocker, blocked),
927 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
929 |> put_view(AccountView)
930 |> render("relationship.json", %{user: blocker, target: blocked})
934 |> put_resp_content_type("application/json")
935 |> send_resp(403, Jason.encode!(%{"error" => message}))
939 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
940 with %User{} = blocked <- User.get_cached_by_id(id),
941 {:ok, blocker} <- User.unblock(blocker, blocked),
942 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
944 |> put_view(AccountView)
945 |> render("relationship.json", %{user: blocker, target: blocked})
949 |> put_resp_content_type("application/json")
950 |> send_resp(403, Jason.encode!(%{"error" => message}))
954 def blocks(%{assigns: %{user: user}} = conn, _) do
955 with blocked_accounts <- User.blocked_users(user) do
956 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
961 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
962 json(conn, info.domain_blocks || [])
965 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
966 User.block_domain(blocker, domain)
970 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
971 User.unblock_domain(blocker, domain)
975 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
976 with %User{} = subscription_target <- User.get_cached_by_id(id),
977 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
979 |> put_view(AccountView)
980 |> render("relationship.json", %{user: user, target: subscription_target})
984 |> put_resp_content_type("application/json")
985 |> send_resp(403, Jason.encode!(%{"error" => message}))
989 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
990 with %User{} = subscription_target <- User.get_cached_by_id(id),
991 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
993 |> put_view(AccountView)
994 |> render("relationship.json", %{user: user, target: subscription_target})
998 |> put_resp_content_type("application/json")
999 |> send_resp(403, Jason.encode!(%{"error" => message}))
1003 def status_search_query_with_gin(q, query) do
1007 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1011 order_by: [desc: :id]
1015 def status_search_query_with_rum(q, query) do
1019 "? @@ plainto_tsquery('english', ?)",
1023 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1027 def status_search(user, query) do
1029 if Regex.match?(~r/https?:/, query) do
1030 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1031 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1032 true <- Visibility.visible_for_user?(activity, user) do
1040 from([a, o] in Activity.with_preloaded_object(Activity),
1041 where: fragment("?->>'type' = 'Create'", a.data),
1042 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1047 if Pleroma.Config.get([:database, :rum_enabled]) do
1048 status_search_query_with_rum(q, query)
1050 status_search_query_with_gin(q, query)
1053 Repo.all(q) ++ fetched
1056 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1057 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1059 statuses = status_search(user, query)
1061 tags_path = Web.base_url() <> "/tag/"
1067 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1068 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1069 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1072 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1074 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1081 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1082 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1084 statuses = status_search(user, query)
1090 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1091 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1094 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1096 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1103 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1104 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1106 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1111 def favourites(%{assigns: %{user: user}} = conn, params) do
1114 |> Map.put("type", "Create")
1115 |> Map.put("favorited_by", user.ap_id)
1116 |> Map.put("blocking_user", user)
1119 ActivityPub.fetch_activities([], params)
1123 |> add_link_headers(:favourites, activities)
1124 |> put_view(StatusView)
1125 |> render("index.json", %{activities: activities, for: user, as: :activity})
1128 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1129 with %User{} = user <- User.get_by_id(id),
1130 false <- user.info.hide_favorites do
1133 |> Map.put("type", "Create")
1134 |> Map.put("favorited_by", user.ap_id)
1135 |> Map.put("blocking_user", for_user)
1139 ["https://www.w3.org/ns/activitystreams#Public"] ++
1140 [for_user.ap_id | for_user.following]
1142 ["https://www.w3.org/ns/activitystreams#Public"]
1147 |> ActivityPub.fetch_activities(params)
1151 |> add_link_headers(:favourites, activities)
1152 |> put_view(StatusView)
1153 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1156 {:error, :not_found}
1161 |> json(%{error: "Can't get favorites"})
1165 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1166 user = User.get_cached_by_id(user.id)
1169 Bookmark.for_user_query(user.id)
1170 |> Pagination.fetch_paginated(params)
1174 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1177 |> add_link_headers(:bookmarks, bookmarks)
1178 |> put_view(StatusView)
1179 |> render("index.json", %{activities: activities, for: user, as: :activity})
1182 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1183 lists = Pleroma.List.for_user(user, opts)
1184 res = ListView.render("lists.json", lists: lists)
1188 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1189 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1190 res = ListView.render("list.json", list: list)
1196 |> json(%{error: "Record not found"})
1200 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1201 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1202 res = ListView.render("lists.json", lists: lists)
1206 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1207 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1208 {:ok, _list} <- Pleroma.List.delete(list) do
1216 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1217 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1218 res = ListView.render("list.json", list: list)
1223 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1225 |> Enum.each(fn account_id ->
1226 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1227 %User{} = followed <- User.get_cached_by_id(account_id) do
1228 Pleroma.List.follow(list, followed)
1235 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1237 |> Enum.each(fn account_id ->
1238 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1239 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1240 Pleroma.List.unfollow(list, followed)
1247 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1248 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1249 {:ok, users} = Pleroma.List.get_following(list) do
1251 |> put_view(AccountView)
1252 |> render("accounts.json", %{for: user, users: users, as: :user})
1256 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1257 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1258 {:ok, list} <- Pleroma.List.rename(list, title) do
1259 res = ListView.render("list.json", list: list)
1267 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1268 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1271 |> Map.put("type", "Create")
1272 |> Map.put("blocking_user", user)
1273 |> Map.put("muting_user", user)
1275 # we must filter the following list for the user to avoid leaking statuses the user
1276 # does not actually have permission to see (for more info, peruse security issue #270).
1279 |> Enum.filter(fn x -> x in user.following end)
1280 |> ActivityPub.fetch_activities_bounded(following, params)
1284 |> put_view(StatusView)
1285 |> render("index.json", %{activities: activities, for: user, as: :activity})
1290 |> json(%{error: "Error."})
1294 def index(%{assigns: %{user: user}} = conn, _params) do
1295 token = get_session(conn, :oauth_token)
1298 mastodon_emoji = mastodonized_emoji()
1300 limit = Config.get([:instance, :limit])
1303 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1305 flavour = get_user_flavour(user)
1310 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1311 access_token: token,
1313 domain: Pleroma.Web.Endpoint.host(),
1316 unfollow_modal: false,
1319 auto_play_gif: false,
1320 display_sensitive_media: false,
1321 reduce_motion: false,
1322 max_toot_chars: limit,
1323 mascot: "/images/pleroma-fox-tan-smol.png"
1326 delete_others_notice: present?(user.info.is_moderator),
1327 admin: present?(user.info.is_admin)
1331 default_privacy: user.info.default_scope,
1332 default_sensitive: false,
1333 allow_content_types: Config.get([:instance, :allowed_post_formats])
1335 media_attachments: %{
1336 accept_content_types: [
1352 user.info.settings ||
1382 push_subscription: nil,
1384 custom_emojis: mastodon_emoji,
1390 |> put_layout(false)
1391 |> put_view(MastodonView)
1392 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1395 |> put_session(:return_to, conn.request_path)
1396 |> redirect(to: "/web/login")
1400 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1401 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1403 with changeset <- Ecto.Changeset.change(user),
1404 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1405 {:ok, _user} <- User.update_and_set_cache(changeset) do
1410 |> put_resp_content_type("application/json")
1411 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1415 @supported_flavours ["glitch", "vanilla"]
1417 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1418 when flavour in @supported_flavours do
1419 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1421 with changeset <- Ecto.Changeset.change(user),
1422 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1423 {:ok, user} <- User.update_and_set_cache(changeset),
1424 flavour <- user.info.flavour do
1429 |> put_resp_content_type("application/json")
1430 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1434 def set_flavour(conn, _params) do
1437 |> json(%{error: "Unsupported flavour"})
1440 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1441 json(conn, get_user_flavour(user))
1444 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1448 defp get_user_flavour(_) do
1452 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1453 redirect(conn, to: local_mastodon_root_path(conn))
1456 @doc "Local Mastodon FE login init action"
1457 def login(conn, %{"code" => auth_token}) do
1458 with {:ok, app} <- get_or_make_app(),
1459 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1460 {:ok, token} <- Token.exchange_token(app, auth) do
1462 |> put_session(:oauth_token, token.token)
1463 |> redirect(to: local_mastodon_root_path(conn))
1467 @doc "Local Mastodon FE callback action"
1468 def login(conn, _) do
1469 with {:ok, app} <- get_or_make_app() do
1474 response_type: "code",
1475 client_id: app.client_id,
1477 scope: Enum.join(app.scopes, " ")
1480 redirect(conn, to: path)
1484 defp local_mastodon_root_path(conn) do
1485 case get_session(conn, :return_to) do
1487 mastodon_api_path(conn, :index, ["getting-started"])
1490 delete_session(conn, :return_to)
1495 defp get_or_make_app do
1496 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1497 scopes = ["read", "write", "follow", "push"]
1499 with %App{} = app <- Repo.get_by(App, find_attrs) do
1501 if app.scopes == scopes do
1505 |> Ecto.Changeset.change(%{scopes: scopes})
1513 App.register_changeset(
1515 Map.put(find_attrs, :scopes, scopes)
1522 def logout(conn, _) do
1525 |> redirect(to: "/")
1528 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1529 Logger.debug("Unimplemented, returning unmodified relationship")
1531 with %User{} = target <- User.get_cached_by_id(id) do
1533 |> put_view(AccountView)
1534 |> render("relationship.json", %{user: user, target: target})
1538 def empty_array(conn, _) do
1539 Logger.debug("Unimplemented, returning an empty array")
1543 def empty_object(conn, _) do
1544 Logger.debug("Unimplemented, returning an empty object")
1548 def get_filters(%{assigns: %{user: user}} = conn, _) do
1549 filters = Filter.get_filters(user)
1550 res = FilterView.render("filters.json", filters: filters)
1555 %{assigns: %{user: user}} = conn,
1556 %{"phrase" => phrase, "context" => context} = params
1562 hide: Map.get(params, "irreversible", nil),
1563 whole_word: Map.get(params, "boolean", true)
1567 {:ok, response} = Filter.create(query)
1568 res = FilterView.render("filter.json", filter: response)
1572 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1573 filter = Filter.get(filter_id, user)
1574 res = FilterView.render("filter.json", filter: filter)
1579 %{assigns: %{user: user}} = conn,
1580 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1584 filter_id: filter_id,
1587 hide: Map.get(params, "irreversible", nil),
1588 whole_word: Map.get(params, "boolean", true)
1592 {:ok, response} = Filter.update(query)
1593 res = FilterView.render("filter.json", filter: response)
1597 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1600 filter_id: filter_id
1603 {:ok, _} = Filter.delete(query)
1609 def errors(conn, {:error, %Changeset{} = changeset}) do
1612 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1613 |> Enum.map_join(", ", fn {_k, v} -> v end)
1617 |> json(%{error: error_message})
1620 def errors(conn, {:error, :not_found}) do
1623 |> json(%{error: "Record not found"})
1626 def errors(conn, _) do
1629 |> json("Something went wrong")
1632 def suggestions(%{assigns: %{user: user}} = conn, _) do
1633 suggestions = Config.get(:suggestions)
1635 if Keyword.get(suggestions, :enabled, false) do
1636 api = Keyword.get(suggestions, :third_party_engine, "")
1637 timeout = Keyword.get(suggestions, :timeout, 5000)
1638 limit = Keyword.get(suggestions, :limit, 23)
1640 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1642 user = user.nickname
1646 |> String.replace("{{host}}", host)
1647 |> String.replace("{{user}}", user)
1649 with {:ok, %{status: 200, body: body}} <-
1654 recv_timeout: timeout,
1658 {:ok, data} <- Jason.decode(body) do
1661 |> Enum.slice(0, limit)
1666 case User.get_or_fetch(x["acct"]) do
1667 {:ok, %User{id: id}} -> id
1673 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1676 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1682 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1689 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1690 with %Activity{} = activity <- Activity.get_by_id(status_id),
1691 true <- Visibility.visible_for_user?(activity, user) do
1695 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1705 def reports(%{assigns: %{user: user}} = conn, params) do
1706 case CommonAPI.report(user, params) do
1709 |> put_view(ReportView)
1710 |> try_render("report.json", %{activity: activity})
1714 |> put_status(:bad_request)
1715 |> json(%{error: err})
1719 def conversations(%{assigns: %{user: user}} = conn, params) do
1720 participations = Participation.for_user_with_last_activity_id(user, params)
1723 Enum.map(participations, fn participation ->
1724 ConversationView.render("participation.json", %{participation: participation, user: user})
1728 |> add_link_headers(:conversations, participations)
1729 |> json(conversations)
1732 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1733 with %Participation{} = participation <-
1734 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1735 {:ok, participation} <- Participation.mark_as_read(participation) do
1736 participation_view =
1737 ConversationView.render("participation.json", %{participation: participation, user: user})
1740 |> json(participation_view)
1744 def try_render(conn, target, params)
1745 when is_binary(target) do
1746 res = render(conn, target, params)
1751 |> json(%{error: "Can't display this activity"})
1757 def try_render(conn, _, _) do
1760 |> json(%{error: "Can't display this activity"})
1763 defp present?(nil), do: false
1764 defp present?(false), do: false
1765 defp present?(_), do: true