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
12 alias Pleroma.Notification
14 alias Pleroma.Object.Fetcher
15 alias Pleroma.Pagination
17 alias Pleroma.ScheduledActivity
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.ActivityPub.Visibility
23 alias Pleroma.Web.CommonAPI
24 alias Pleroma.Web.MastodonAPI.AccountView
25 alias Pleroma.Web.MastodonAPI.AppView
26 alias Pleroma.Web.MastodonAPI.FilterView
27 alias Pleroma.Web.MastodonAPI.ListView
28 alias Pleroma.Web.MastodonAPI.MastodonAPI
29 alias Pleroma.Web.MastodonAPI.MastodonView
30 alias Pleroma.Web.MastodonAPI.NotificationView
31 alias Pleroma.Web.MastodonAPI.ReportView
32 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
33 alias Pleroma.Web.MastodonAPI.StatusView
34 alias Pleroma.Web.MediaProxy
35 alias Pleroma.Web.OAuth.App
36 alias Pleroma.Web.OAuth.Authorization
37 alias Pleroma.Web.OAuth.Token
39 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
44 @httpoison Application.get_env(:pleroma, :httpoison)
45 @local_mastodon_name "Mastodon-Local"
47 action_fallback(:errors)
49 def create_app(conn, params) do
50 scopes = oauth_scopes(params, ["read"])
54 |> Map.drop(["scope", "scopes"])
55 |> Map.put("scopes", scopes)
57 with cs <- App.register_changeset(%App{}, app_attrs),
58 false <- cs.changes[:client_name] == @local_mastodon_name,
59 {:ok, app} <- Repo.insert(cs) do
62 |> render("show.json", %{app: app})
71 value_function \\ fn x -> {:ok, x} end
73 if Map.has_key?(params, params_field) do
74 case value_function.(params[params_field]) do
75 {:ok, new_value} -> Map.put(map, map_field, new_value)
83 def update_credentials(%{assigns: %{user: user}} = conn, params) do
88 |> add_if_present(params, "display_name", :name)
89 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
90 |> add_if_present(params, "avatar", :avatar, fn value ->
91 with %Plug.Upload{} <- value,
92 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
101 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
102 |> add_if_present(params, "header", :banner, fn value ->
103 with %Plug.Upload{} <- value,
104 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
111 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
113 with changeset <- User.update_changeset(user, user_params),
114 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
115 {:ok, user} <- User.update_and_set_cache(changeset) do
116 if original_user != user do
117 CommonAPI.update(user)
120 json(conn, AccountView.render("account.json", %{user: user, for: user}))
125 |> json(%{error: "Invalid request"})
129 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
130 account = AccountView.render("account.json", %{user: user, for: user})
134 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
135 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
138 |> render("short.json", %{app: app})
142 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
143 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
144 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
145 account = AccountView.render("account.json", %{user: user, for: for_user})
151 |> json(%{error: "Can't find user"})
155 @mastodon_api_level "2.5.0"
157 def masto_instance(conn, _params) do
158 instance = Config.get(:instance)
162 title: Keyword.get(instance, :name),
163 description: Keyword.get(instance, :description),
164 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
165 email: Keyword.get(instance, :email),
167 streaming_api: Pleroma.Web.Endpoint.websocket_url()
169 stats: Stats.get_stats(),
170 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
172 registrations: Pleroma.Config.get([:instance, :registrations_open]),
173 # Extra (not present in Mastodon):
174 max_toot_chars: Keyword.get(instance, :limit)
180 def peers(conn, _params) do
181 json(conn, Stats.get_peers())
184 defp mastodonized_emoji do
185 Pleroma.Emoji.get_all()
186 |> Enum.map(fn {shortcode, relative_url, tags} ->
187 url = to_string(URI.merge(Web.base_url(), relative_url))
190 "shortcode" => shortcode,
192 "visible_in_picker" => true,
199 def custom_emojis(conn, _params) do
200 mastodon_emoji = mastodonized_emoji()
201 json(conn, mastodon_emoji)
204 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
207 |> Map.drop(["since_id", "max_id", "min_id"])
210 last = List.last(activities)
217 |> Map.get("limit", "20")
218 |> String.to_integer()
221 if length(activities) <= limit do
227 |> Enum.at(limit * -1)
231 {next_url, prev_url} =
235 Pleroma.Web.Endpoint,
238 Map.merge(params, %{max_id: max_id})
241 Pleroma.Web.Endpoint,
244 Map.merge(params, %{min_id: min_id})
250 Pleroma.Web.Endpoint,
252 Map.merge(params, %{max_id: max_id})
255 Pleroma.Web.Endpoint,
257 Map.merge(params, %{min_id: min_id})
263 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
269 def home_timeline(%{assigns: %{user: user}} = conn, params) do
272 |> Map.put("type", ["Create", "Announce"])
273 |> Map.put("blocking_user", user)
274 |> Map.put("muting_user", user)
275 |> Map.put("user", user)
278 [user.ap_id | user.following]
279 |> ActivityPub.fetch_activities(params)
280 |> ActivityPub.contain_timeline(user)
283 user = Repo.preload(user, bookmarks: :activity)
286 |> add_link_headers(:home_timeline, activities)
287 |> put_view(StatusView)
288 |> render("index.json", %{activities: activities, for: user, as: :activity})
291 def public_timeline(%{assigns: %{user: user}} = conn, params) do
292 local_only = params["local"] in [true, "True", "true", "1"]
296 |> Map.put("type", ["Create", "Announce"])
297 |> Map.put("local_only", local_only)
298 |> Map.put("blocking_user", user)
299 |> Map.put("muting_user", user)
300 |> ActivityPub.fetch_public_activities()
303 user = Repo.preload(user, bookmarks: :activity)
306 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
307 |> put_view(StatusView)
308 |> render("index.json", %{activities: activities, for: user, as: :activity})
311 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
312 with %User{} = user <- User.get_cached_by_id(params["id"]),
313 reading_user <- Repo.preload(reading_user, :bookmarks) do
314 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
317 |> add_link_headers(:user_statuses, activities, params["id"])
318 |> put_view(StatusView)
319 |> render("index.json", %{
320 activities: activities,
327 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
330 |> Map.put("type", "Create")
331 |> Map.put("blocking_user", user)
332 |> Map.put("user", user)
333 |> Map.put(:visibility, "direct")
337 |> ActivityPub.fetch_activities_query(params)
338 |> Pagination.fetch_paginated(params)
340 user = Repo.preload(user, bookmarks: :activity)
343 |> add_link_headers(:dm_timeline, activities)
344 |> put_view(StatusView)
345 |> render("index.json", %{activities: activities, for: user, as: :activity})
348 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
349 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
350 true <- Visibility.visible_for_user?(activity, user) do
351 user = Repo.preload(user, bookmarks: :activity)
354 |> put_view(StatusView)
355 |> try_render("status.json", %{activity: activity, for: user})
359 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
360 with %Activity{} = activity <- Activity.get_by_id(id),
362 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
363 "blocking_user" => user,
367 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
369 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
370 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
376 activities: grouped_activities[true] || [],
380 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
385 activities: grouped_activities[false] || [],
389 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
396 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
397 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
399 |> add_link_headers(:scheduled_statuses, scheduled_activities)
400 |> put_view(ScheduledActivityView)
401 |> render("index.json", %{scheduled_activities: scheduled_activities})
405 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
406 with %ScheduledActivity{} = scheduled_activity <-
407 ScheduledActivity.get(user, scheduled_activity_id) do
409 |> put_view(ScheduledActivityView)
410 |> render("show.json", %{scheduled_activity: scheduled_activity})
412 _ -> {:error, :not_found}
416 def update_scheduled_status(
417 %{assigns: %{user: user}} = conn,
418 %{"id" => scheduled_activity_id} = params
420 with %ScheduledActivity{} = scheduled_activity <-
421 ScheduledActivity.get(user, scheduled_activity_id),
422 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
424 |> put_view(ScheduledActivityView)
425 |> render("show.json", %{scheduled_activity: scheduled_activity})
427 nil -> {:error, :not_found}
432 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
433 with %ScheduledActivity{} = scheduled_activity <-
434 ScheduledActivity.get(user, scheduled_activity_id),
435 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
437 |> put_view(ScheduledActivityView)
438 |> render("show.json", %{scheduled_activity: scheduled_activity})
440 nil -> {:error, :not_found}
445 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
446 when length(media_ids) > 0 do
449 |> Map.put("status", ".")
451 post_status(conn, params)
454 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
457 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
460 case get_req_header(conn, "idempotency-key") do
462 _ -> Ecto.UUID.generate()
465 scheduled_at = params["scheduled_at"]
467 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
468 with {:ok, scheduled_activity} <-
469 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
471 |> put_view(ScheduledActivityView)
472 |> render("show.json", %{scheduled_activity: scheduled_activity})
475 params = Map.drop(params, ["scheduled_at"])
478 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
479 CommonAPI.post(user, params)
483 |> put_view(StatusView)
484 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
488 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
489 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
495 |> json(%{error: "Can't delete this post"})
499 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
500 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
501 %Activity{} = announce <- Activity.normalize(announce.data) do
502 user = Repo.preload(user, bookmarks: :activity)
505 |> put_view(StatusView)
506 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
510 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
511 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
512 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
513 user = Repo.preload(user, bookmarks: :activity)
516 |> put_view(StatusView)
517 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
521 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
522 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
523 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
525 |> put_view(StatusView)
526 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
530 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
531 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
532 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
534 |> put_view(StatusView)
535 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
539 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
540 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
542 |> put_view(StatusView)
543 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
547 |> put_resp_content_type("application/json")
548 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
552 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
553 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
555 |> put_view(StatusView)
556 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
560 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
561 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
562 %User{} = user <- User.get_cached_by_nickname(user.nickname),
563 true <- Visibility.visible_for_user?(activity, user),
564 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
565 user = Repo.preload(user, bookmarks: :activity)
568 |> put_view(StatusView)
569 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
573 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
574 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
575 %User{} = user <- User.get_cached_by_nickname(user.nickname),
576 true <- Visibility.visible_for_user?(activity, user),
577 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
578 user = Repo.preload(user, bookmarks: :activity)
581 |> put_view(StatusView)
582 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
586 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
587 activity = Activity.get_by_id(id)
589 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
591 |> put_view(StatusView)
592 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
596 |> put_resp_content_type("application/json")
597 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
601 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
602 activity = Activity.get_by_id(id)
604 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
606 |> put_view(StatusView)
607 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
611 def notifications(%{assigns: %{user: user}} = conn, params) do
612 notifications = MastodonAPI.get_notifications(user, params)
615 |> add_link_headers(:notifications, notifications)
616 |> put_view(NotificationView)
617 |> render("index.json", %{notifications: notifications, for: user})
620 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
621 with {:ok, notification} <- Notification.get(user, id) do
623 |> put_view(NotificationView)
624 |> render("show.json", %{notification: notification, for: user})
628 |> put_resp_content_type("application/json")
629 |> send_resp(403, Jason.encode!(%{"error" => reason}))
633 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
634 Notification.clear(user)
638 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
639 with {:ok, _notif} <- Notification.dismiss(user, id) do
644 |> put_resp_content_type("application/json")
645 |> send_resp(403, Jason.encode!(%{"error" => reason}))
649 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
650 Notification.destroy_multiple(user, ids)
654 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
656 q = from(u in User, where: u.id in ^id)
657 targets = Repo.all(q)
660 |> put_view(AccountView)
661 |> render("relationships.json", %{user: user, targets: targets})
664 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
665 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
667 def update_media(%{assigns: %{user: user}} = conn, data) do
668 with %Object{} = object <- Repo.get(Object, data["id"]),
669 true <- Object.authorize_mutation(object, user),
670 true <- is_binary(data["description"]),
671 description <- data["description"] do
672 new_data = %{object.data | "name" => description}
676 |> Object.change(%{data: new_data})
679 attachment_data = Map.put(new_data, "id", object.id)
682 |> put_view(StatusView)
683 |> render("attachment.json", %{attachment: attachment_data})
687 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
688 with {:ok, object} <-
691 actor: User.ap_id(user),
692 description: Map.get(data, "description")
694 attachment_data = Map.put(object.data, "id", object.id)
697 |> put_view(StatusView)
698 |> render("attachment.json", %{attachment: attachment_data})
702 def favourited_by(conn, %{"id" => id}) do
703 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
704 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
705 q = from(u in User, where: u.ap_id in ^likes)
709 |> put_view(AccountView)
710 |> render(AccountView, "accounts.json", %{users: users, as: :user})
716 def reblogged_by(conn, %{"id" => id}) do
717 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
718 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
719 q = from(u in User, where: u.ap_id in ^announces)
723 |> put_view(AccountView)
724 |> render("accounts.json", %{users: users, as: :user})
730 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
731 local_only = params["local"] in [true, "True", "true", "1"]
734 [params["tag"], params["any"]]
738 |> Enum.map(&String.downcase(&1))
743 |> Enum.map(&String.downcase(&1))
748 |> Enum.map(&String.downcase(&1))
752 |> Map.put("type", "Create")
753 |> Map.put("local_only", local_only)
754 |> Map.put("blocking_user", user)
755 |> Map.put("muting_user", user)
756 |> Map.put("tag", tags)
757 |> Map.put("tag_all", tag_all)
758 |> Map.put("tag_reject", tag_reject)
759 |> ActivityPub.fetch_public_activities()
763 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
764 |> put_view(StatusView)
765 |> render("index.json", %{activities: activities, for: user, as: :activity})
768 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
769 with %User{} = user <- User.get_cached_by_id(id),
770 followers <- MastodonAPI.get_followers(user, params) do
773 for_user && user.id == for_user.id -> followers
774 user.info.hide_followers -> []
779 |> add_link_headers(:followers, followers, user)
780 |> put_view(AccountView)
781 |> render("accounts.json", %{users: followers, as: :user})
785 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
786 with %User{} = user <- User.get_cached_by_id(id),
787 followers <- MastodonAPI.get_friends(user, params) do
790 for_user && user.id == for_user.id -> followers
791 user.info.hide_follows -> []
796 |> add_link_headers(:following, followers, user)
797 |> put_view(AccountView)
798 |> render("accounts.json", %{users: followers, as: :user})
802 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
803 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
805 |> put_view(AccountView)
806 |> render("accounts.json", %{users: follow_requests, as: :user})
810 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
811 with %User{} = follower <- User.get_cached_by_id(id),
812 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
814 |> put_view(AccountView)
815 |> render("relationship.json", %{user: followed, target: follower})
819 |> put_resp_content_type("application/json")
820 |> send_resp(403, Jason.encode!(%{"error" => message}))
824 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
825 with %User{} = follower <- User.get_cached_by_id(id),
826 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
828 |> put_view(AccountView)
829 |> render("relationship.json", %{user: followed, target: follower})
833 |> put_resp_content_type("application/json")
834 |> send_resp(403, Jason.encode!(%{"error" => message}))
838 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
839 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
840 {_, true} <- {:followed, follower.id != followed.id},
841 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
843 |> put_view(AccountView)
844 |> render("relationship.json", %{user: follower, target: followed})
851 |> put_resp_content_type("application/json")
852 |> send_resp(403, Jason.encode!(%{"error" => message}))
856 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
857 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
858 {_, true} <- {:followed, follower.id != followed.id},
859 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
861 |> put_view(AccountView)
862 |> render("account.json", %{user: followed, for: follower})
869 |> put_resp_content_type("application/json")
870 |> send_resp(403, Jason.encode!(%{"error" => message}))
874 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
875 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
876 {_, true} <- {:followed, follower.id != followed.id},
877 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
879 |> put_view(AccountView)
880 |> render("relationship.json", %{user: follower, target: followed})
890 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
891 with %User{} = muted <- User.get_cached_by_id(id),
892 {:ok, muter} <- User.mute(muter, muted) do
894 |> put_view(AccountView)
895 |> render("relationship.json", %{user: muter, target: muted})
899 |> put_resp_content_type("application/json")
900 |> send_resp(403, Jason.encode!(%{"error" => message}))
904 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
905 with %User{} = muted <- User.get_cached_by_id(id),
906 {:ok, muter} <- User.unmute(muter, muted) do
908 |> put_view(AccountView)
909 |> render("relationship.json", %{user: muter, target: muted})
913 |> put_resp_content_type("application/json")
914 |> send_resp(403, Jason.encode!(%{"error" => message}))
918 def mutes(%{assigns: %{user: user}} = conn, _) do
919 with muted_accounts <- User.muted_users(user) do
920 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
925 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
926 with %User{} = blocked <- User.get_cached_by_id(id),
927 {:ok, blocker} <- User.block(blocker, blocked),
928 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
930 |> put_view(AccountView)
931 |> render("relationship.json", %{user: blocker, target: blocked})
935 |> put_resp_content_type("application/json")
936 |> send_resp(403, Jason.encode!(%{"error" => message}))
940 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
941 with %User{} = blocked <- User.get_cached_by_id(id),
942 {:ok, blocker} <- User.unblock(blocker, blocked),
943 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
945 |> put_view(AccountView)
946 |> render("relationship.json", %{user: blocker, target: blocked})
950 |> put_resp_content_type("application/json")
951 |> send_resp(403, Jason.encode!(%{"error" => message}))
955 def blocks(%{assigns: %{user: user}} = conn, _) do
956 with blocked_accounts <- User.blocked_users(user) do
957 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
962 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
963 json(conn, info.domain_blocks || [])
966 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
967 User.block_domain(blocker, domain)
971 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
972 User.unblock_domain(blocker, domain)
976 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
977 with %User{} = subscription_target <- User.get_cached_by_id(id),
978 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
980 |> put_view(AccountView)
981 |> render("relationship.json", %{user: user, target: subscription_target})
985 |> put_resp_content_type("application/json")
986 |> send_resp(403, Jason.encode!(%{"error" => message}))
990 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
991 with %User{} = subscription_target <- User.get_cached_by_id(id),
992 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
994 |> put_view(AccountView)
995 |> render("relationship.json", %{user: user, target: subscription_target})
999 |> put_resp_content_type("application/json")
1000 |> send_resp(403, Jason.encode!(%{"error" => message}))
1004 def status_search(user, query) do
1006 if Regex.match?(~r/https?:/, query) do
1007 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1008 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1009 true <- Visibility.visible_for_user?(activity, user) do
1018 [a, o] in Activity.with_preloaded_object(Activity),
1019 where: fragment("?->>'type' = 'Create'", a.data),
1020 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1023 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1028 order_by: [desc: :id]
1031 Repo.all(q) ++ fetched
1034 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1035 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1037 statuses = status_search(user, query)
1039 tags_path = Web.base_url() <> "/tag/"
1045 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1046 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1047 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1050 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1052 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1059 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1060 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1062 statuses = status_search(user, query)
1068 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1069 |> Enum.map(fn tag -> String.slice(tag, 1..-1) 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 account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1082 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1084 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1089 def favourites(%{assigns: %{user: user}} = conn, params) do
1092 |> Map.put("type", "Create")
1093 |> Map.put("favorited_by", user.ap_id)
1094 |> Map.put("blocking_user", user)
1097 ActivityPub.fetch_activities([], params)
1100 user = Repo.preload(user, bookmarks: :activity)
1103 |> add_link_headers(:favourites, activities)
1104 |> put_view(StatusView)
1105 |> render("index.json", %{activities: activities, for: user, as: :activity})
1108 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1109 with %User{} = user <- User.get_by_id(id),
1110 false <- user.info.hide_favorites do
1113 |> Map.put("type", "Create")
1114 |> Map.put("favorited_by", user.ap_id)
1115 |> Map.put("blocking_user", for_user)
1119 ["https://www.w3.org/ns/activitystreams#Public"] ++
1120 [for_user.ap_id | for_user.following]
1122 ["https://www.w3.org/ns/activitystreams#Public"]
1127 |> ActivityPub.fetch_activities(params)
1131 |> add_link_headers(:favourites, activities)
1132 |> put_view(StatusView)
1133 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1136 {:error, :not_found}
1141 |> json(%{error: "Can't get favorites"})
1145 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1146 user = User.get_cached_by_id(user.id)
1147 user = Repo.preload(user, bookmarks: :activity)
1150 Bookmark.for_user_query(user.id)
1151 |> Pagination.fetch_paginated(params)
1155 |> Enum.map(fn b -> b.activity end)
1158 |> add_link_headers(:bookmarks, bookmarks)
1159 |> put_view(StatusView)
1160 |> render("index.json", %{activities: activities, for: user, as: :activity})
1163 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1164 lists = Pleroma.List.for_user(user, opts)
1165 res = ListView.render("lists.json", lists: lists)
1169 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1170 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1171 res = ListView.render("list.json", list: list)
1177 |> json(%{error: "Record not found"})
1181 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1182 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1183 res = ListView.render("lists.json", lists: lists)
1187 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1188 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1189 {:ok, _list} <- Pleroma.List.delete(list) do
1197 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1198 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1199 res = ListView.render("list.json", list: list)
1204 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1206 |> Enum.each(fn account_id ->
1207 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1208 %User{} = followed <- User.get_cached_by_id(account_id) do
1209 Pleroma.List.follow(list, followed)
1216 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1218 |> Enum.each(fn account_id ->
1219 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1220 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1221 Pleroma.List.unfollow(list, followed)
1228 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1229 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1230 {:ok, users} = Pleroma.List.get_following(list) do
1232 |> put_view(AccountView)
1233 |> render("accounts.json", %{users: users, as: :user})
1237 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1238 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1239 {:ok, list} <- Pleroma.List.rename(list, title) do
1240 res = ListView.render("list.json", list: list)
1248 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1249 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1252 |> Map.put("type", "Create")
1253 |> Map.put("blocking_user", user)
1254 |> Map.put("muting_user", user)
1256 # we must filter the following list for the user to avoid leaking statuses the user
1257 # does not actually have permission to see (for more info, peruse security issue #270).
1260 |> Enum.filter(fn x -> x in user.following end)
1261 |> ActivityPub.fetch_activities_bounded(following, params)
1264 user = Repo.preload(user, bookmarks: :activity)
1267 |> put_view(StatusView)
1268 |> render("index.json", %{activities: activities, for: user, as: :activity})
1273 |> json(%{error: "Error."})
1277 def index(%{assigns: %{user: user}} = conn, _params) do
1278 token = get_session(conn, :oauth_token)
1281 mastodon_emoji = mastodonized_emoji()
1283 limit = Config.get([:instance, :limit])
1286 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1288 flavour = get_user_flavour(user)
1293 streaming_api_base_url:
1294 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1295 access_token: token,
1297 domain: Pleroma.Web.Endpoint.host(),
1300 unfollow_modal: false,
1303 auto_play_gif: false,
1304 display_sensitive_media: false,
1305 reduce_motion: false,
1306 max_toot_chars: limit,
1307 mascot: "/images/pleroma-fox-tan-smol.png"
1310 delete_others_notice: present?(user.info.is_moderator),
1311 admin: present?(user.info.is_admin)
1315 default_privacy: user.info.default_scope,
1316 default_sensitive: false,
1317 allow_content_types: Config.get([:instance, :allowed_post_formats])
1319 media_attachments: %{
1320 accept_content_types: [
1336 user.info.settings ||
1366 push_subscription: nil,
1368 custom_emojis: mastodon_emoji,
1374 |> put_layout(false)
1375 |> put_view(MastodonView)
1376 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1379 |> put_session(:return_to, conn.request_path)
1380 |> redirect(to: "/web/login")
1384 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1385 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1387 with changeset <- Ecto.Changeset.change(user),
1388 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1389 {:ok, _user} <- User.update_and_set_cache(changeset) do
1394 |> put_resp_content_type("application/json")
1395 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1399 @supported_flavours ["glitch", "vanilla"]
1401 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1402 when flavour in @supported_flavours do
1403 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1405 with changeset <- Ecto.Changeset.change(user),
1406 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1407 {:ok, user} <- User.update_and_set_cache(changeset),
1408 flavour <- user.info.flavour do
1413 |> put_resp_content_type("application/json")
1414 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1418 def set_flavour(conn, _params) do
1421 |> json(%{error: "Unsupported flavour"})
1424 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1425 json(conn, get_user_flavour(user))
1428 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1432 defp get_user_flavour(_) do
1436 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1437 redirect(conn, to: local_mastodon_root_path(conn))
1440 @doc "Local Mastodon FE login init action"
1441 def login(conn, %{"code" => auth_token}) do
1442 with {:ok, app} <- get_or_make_app(),
1443 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1444 {:ok, token} <- Token.exchange_token(app, auth) do
1446 |> put_session(:oauth_token, token.token)
1447 |> redirect(to: local_mastodon_root_path(conn))
1451 @doc "Local Mastodon FE callback action"
1452 def login(conn, _) do
1453 with {:ok, app} <- get_or_make_app() do
1458 response_type: "code",
1459 client_id: app.client_id,
1461 scope: Enum.join(app.scopes, " ")
1464 redirect(conn, to: path)
1468 defp local_mastodon_root_path(conn) do
1469 case get_session(conn, :return_to) do
1471 mastodon_api_path(conn, :index, ["getting-started"])
1474 delete_session(conn, :return_to)
1479 defp get_or_make_app do
1480 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1481 scopes = ["read", "write", "follow", "push"]
1483 with %App{} = app <- Repo.get_by(App, find_attrs) do
1485 if app.scopes == scopes do
1489 |> Ecto.Changeset.change(%{scopes: scopes})
1497 App.register_changeset(
1499 Map.put(find_attrs, :scopes, scopes)
1506 def logout(conn, _) do
1509 |> redirect(to: "/")
1512 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1513 Logger.debug("Unimplemented, returning unmodified relationship")
1515 with %User{} = target <- User.get_cached_by_id(id) do
1517 |> put_view(AccountView)
1518 |> render("relationship.json", %{user: user, target: target})
1522 def empty_array(conn, _) do
1523 Logger.debug("Unimplemented, returning an empty array")
1527 def empty_object(conn, _) do
1528 Logger.debug("Unimplemented, returning an empty object")
1532 def get_filters(%{assigns: %{user: user}} = conn, _) do
1533 filters = Filter.get_filters(user)
1534 res = FilterView.render("filters.json", filters: filters)
1539 %{assigns: %{user: user}} = conn,
1540 %{"phrase" => phrase, "context" => context} = params
1546 hide: Map.get(params, "irreversible", nil),
1547 whole_word: Map.get(params, "boolean", true)
1551 {:ok, response} = Filter.create(query)
1552 res = FilterView.render("filter.json", filter: response)
1556 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1557 filter = Filter.get(filter_id, user)
1558 res = FilterView.render("filter.json", filter: filter)
1563 %{assigns: %{user: user}} = conn,
1564 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1568 filter_id: filter_id,
1571 hide: Map.get(params, "irreversible", nil),
1572 whole_word: Map.get(params, "boolean", true)
1576 {:ok, response} = Filter.update(query)
1577 res = FilterView.render("filter.json", filter: response)
1581 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1584 filter_id: filter_id
1587 {:ok, _} = Filter.delete(query)
1593 def errors(conn, {:error, %Changeset{} = changeset}) do
1596 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1597 |> Enum.map_join(", ", fn {_k, v} -> v end)
1601 |> json(%{error: error_message})
1604 def errors(conn, {:error, :not_found}) do
1607 |> json(%{error: "Record not found"})
1610 def errors(conn, _) do
1613 |> json("Something went wrong")
1616 def suggestions(%{assigns: %{user: user}} = conn, _) do
1617 suggestions = Config.get(:suggestions)
1619 if Keyword.get(suggestions, :enabled, false) do
1620 api = Keyword.get(suggestions, :third_party_engine, "")
1621 timeout = Keyword.get(suggestions, :timeout, 5000)
1622 limit = Keyword.get(suggestions, :limit, 23)
1624 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1626 user = user.nickname
1630 |> String.replace("{{host}}", host)
1631 |> String.replace("{{user}}", user)
1633 with {:ok, %{status: 200, body: body}} <-
1638 recv_timeout: timeout,
1642 {:ok, data} <- Jason.decode(body) do
1645 |> Enum.slice(0, limit)
1650 case User.get_or_fetch(x["acct"]) do
1657 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1660 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1666 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1673 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1674 with %Activity{} = activity <- Activity.get_by_id(status_id),
1675 true <- Visibility.visible_for_user?(activity, user) do
1679 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1689 def reports(%{assigns: %{user: user}} = conn, params) do
1690 case CommonAPI.report(user, params) do
1693 |> put_view(ReportView)
1694 |> try_render("report.json", %{activity: activity})
1698 |> put_status(:bad_request)
1699 |> json(%{error: err})
1703 def try_render(conn, target, params)
1704 when is_binary(target) do
1705 res = render(conn, target, params)
1710 |> json(%{error: "Can't display this activity"})
1716 def try_render(conn, _, _) do
1719 |> json(%{error: "Can't display this activity"})
1722 defp present?(nil), do: false
1723 defp present?(false), do: false
1724 defp present?(_), do: true