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 alias Pleroma.Web.ControllerHelper
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 = ControllerHelper.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
100 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
101 |> Enum.reduce(%{}, fn key, acc ->
102 add_if_present(acc, params, to_string(key), key, fn value ->
103 {:ok, ControllerHelper.truthy_param?(value)}
106 |> add_if_present(params, "header", :banner, fn value ->
107 with %Plug.Upload{} <- value,
108 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
115 info_cng = User.Info.profile_update(user.info, info_params)
117 with changeset <- User.update_changeset(user, user_params),
118 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
119 {:ok, user} <- User.update_and_set_cache(changeset) do
120 if original_user != user do
121 CommonAPI.update(user)
124 json(conn, AccountView.render("account.json", %{user: user, for: user}))
129 |> json(%{error: "Invalid request"})
133 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
134 account = AccountView.render("account.json", %{user: user, for: user})
138 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
139 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
142 |> render("short.json", %{app: app})
146 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
147 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
148 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
149 account = AccountView.render("account.json", %{user: user, for: for_user})
155 |> json(%{error: "Can't find user"})
159 @mastodon_api_level "2.5.0"
161 def masto_instance(conn, _params) do
162 instance = Config.get(:instance)
166 title: Keyword.get(instance, :name),
167 description: Keyword.get(instance, :description),
168 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
169 email: Keyword.get(instance, :email),
171 streaming_api: Pleroma.Web.Endpoint.websocket_url()
173 stats: Stats.get_stats(),
174 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
176 registrations: Pleroma.Config.get([:instance, :registrations_open]),
177 # Extra (not present in Mastodon):
178 max_toot_chars: Keyword.get(instance, :limit)
184 def peers(conn, _params) do
185 json(conn, Stats.get_peers())
188 defp mastodonized_emoji do
189 Pleroma.Emoji.get_all()
190 |> Enum.map(fn {shortcode, relative_url, tags} ->
191 url = to_string(URI.merge(Web.base_url(), relative_url))
194 "shortcode" => shortcode,
196 "visible_in_picker" => true,
203 def custom_emojis(conn, _params) do
204 mastodon_emoji = mastodonized_emoji()
205 json(conn, mastodon_emoji)
208 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
211 |> Map.drop(["since_id", "max_id", "min_id"])
214 last = List.last(activities)
221 |> Map.get("limit", "20")
222 |> String.to_integer()
225 if length(activities) <= limit do
231 |> Enum.at(limit * -1)
235 {next_url, prev_url} =
239 Pleroma.Web.Endpoint,
242 Map.merge(params, %{max_id: max_id})
245 Pleroma.Web.Endpoint,
248 Map.merge(params, %{min_id: min_id})
254 Pleroma.Web.Endpoint,
256 Map.merge(params, %{max_id: max_id})
259 Pleroma.Web.Endpoint,
261 Map.merge(params, %{min_id: min_id})
267 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
273 def home_timeline(%{assigns: %{user: user}} = conn, params) do
276 |> Map.put("type", ["Create", "Announce"])
277 |> Map.put("blocking_user", user)
278 |> Map.put("muting_user", user)
279 |> Map.put("user", user)
282 [user.ap_id | user.following]
283 |> ActivityPub.fetch_activities(params)
284 |> ActivityPub.contain_timeline(user)
287 user = Repo.preload(user, bookmarks: :activity)
290 |> add_link_headers(:home_timeline, activities)
291 |> put_view(StatusView)
292 |> render("index.json", %{activities: activities, for: user, as: :activity})
295 def public_timeline(%{assigns: %{user: user}} = conn, params) do
296 local_only = params["local"] in [true, "True", "true", "1"]
300 |> Map.put("type", ["Create", "Announce"])
301 |> Map.put("local_only", local_only)
302 |> Map.put("blocking_user", user)
303 |> Map.put("muting_user", user)
304 |> ActivityPub.fetch_public_activities()
307 user = Repo.preload(user, bookmarks: :activity)
310 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
311 |> put_view(StatusView)
312 |> render("index.json", %{activities: activities, for: user, as: :activity})
315 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
316 with %User{} = user <- User.get_cached_by_id(params["id"]),
317 reading_user <- Repo.preload(reading_user, :bookmarks) do
318 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
321 |> add_link_headers(:user_statuses, activities, params["id"])
322 |> put_view(StatusView)
323 |> render("index.json", %{
324 activities: activities,
331 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
334 |> Map.put("type", "Create")
335 |> Map.put("blocking_user", user)
336 |> Map.put("user", user)
337 |> Map.put(:visibility, "direct")
341 |> ActivityPub.fetch_activities_query(params)
342 |> Pagination.fetch_paginated(params)
344 user = Repo.preload(user, bookmarks: :activity)
347 |> add_link_headers(:dm_timeline, activities)
348 |> put_view(StatusView)
349 |> render("index.json", %{activities: activities, for: user, as: :activity})
352 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
353 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
354 true <- Visibility.visible_for_user?(activity, user) do
355 user = Repo.preload(user, bookmarks: :activity)
358 |> put_view(StatusView)
359 |> try_render("status.json", %{activity: activity, for: user})
363 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
364 with %Activity{} = activity <- Activity.get_by_id(id),
366 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
367 "blocking_user" => user,
371 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
373 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
374 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
380 activities: grouped_activities[true] || [],
384 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
389 activities: grouped_activities[false] || [],
393 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
400 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
401 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
403 |> add_link_headers(:scheduled_statuses, scheduled_activities)
404 |> put_view(ScheduledActivityView)
405 |> render("index.json", %{scheduled_activities: scheduled_activities})
409 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
410 with %ScheduledActivity{} = scheduled_activity <-
411 ScheduledActivity.get(user, scheduled_activity_id) do
413 |> put_view(ScheduledActivityView)
414 |> render("show.json", %{scheduled_activity: scheduled_activity})
416 _ -> {:error, :not_found}
420 def update_scheduled_status(
421 %{assigns: %{user: user}} = conn,
422 %{"id" => scheduled_activity_id} = params
424 with %ScheduledActivity{} = scheduled_activity <-
425 ScheduledActivity.get(user, scheduled_activity_id),
426 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
428 |> put_view(ScheduledActivityView)
429 |> render("show.json", %{scheduled_activity: scheduled_activity})
431 nil -> {:error, :not_found}
436 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
437 with %ScheduledActivity{} = scheduled_activity <-
438 ScheduledActivity.get(user, scheduled_activity_id),
439 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
441 |> put_view(ScheduledActivityView)
442 |> render("show.json", %{scheduled_activity: scheduled_activity})
444 nil -> {:error, :not_found}
449 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
450 when length(media_ids) > 0 do
453 |> Map.put("status", ".")
455 post_status(conn, params)
458 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
461 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
464 case get_req_header(conn, "idempotency-key") do
466 _ -> Ecto.UUID.generate()
469 scheduled_at = params["scheduled_at"]
471 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
472 with {:ok, scheduled_activity} <-
473 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
475 |> put_view(ScheduledActivityView)
476 |> render("show.json", %{scheduled_activity: scheduled_activity})
479 params = Map.drop(params, ["scheduled_at"])
482 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
483 CommonAPI.post(user, params)
487 |> put_view(StatusView)
488 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
492 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
493 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
499 |> json(%{error: "Can't delete this post"})
503 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
504 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
505 %Activity{} = announce <- Activity.normalize(announce.data) do
506 user = Repo.preload(user, bookmarks: :activity)
509 |> put_view(StatusView)
510 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
514 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
515 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
516 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
517 user = Repo.preload(user, bookmarks: :activity)
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
525 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
526 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
527 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
529 |> put_view(StatusView)
530 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
534 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
535 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
536 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
538 |> put_view(StatusView)
539 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
543 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
544 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
546 |> put_view(StatusView)
547 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
551 |> put_resp_content_type("application/json")
552 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
556 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
557 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
559 |> put_view(StatusView)
560 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
564 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
565 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
566 %User{} = user <- User.get_cached_by_nickname(user.nickname),
567 true <- Visibility.visible_for_user?(activity, user),
568 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
569 user = Repo.preload(user, bookmarks: :activity)
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
577 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
578 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
579 %User{} = user <- User.get_cached_by_nickname(user.nickname),
580 true <- Visibility.visible_for_user?(activity, user),
581 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
582 user = Repo.preload(user, bookmarks: :activity)
585 |> put_view(StatusView)
586 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
590 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
591 activity = Activity.get_by_id(id)
593 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
595 |> put_view(StatusView)
596 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
600 |> put_resp_content_type("application/json")
601 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
605 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
606 activity = Activity.get_by_id(id)
608 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
610 |> put_view(StatusView)
611 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
615 def notifications(%{assigns: %{user: user}} = conn, params) do
616 notifications = MastodonAPI.get_notifications(user, params)
619 |> add_link_headers(:notifications, notifications)
620 |> put_view(NotificationView)
621 |> render("index.json", %{notifications: notifications, for: user})
624 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
625 with {:ok, notification} <- Notification.get(user, id) do
627 |> put_view(NotificationView)
628 |> render("show.json", %{notification: notification, for: user})
632 |> put_resp_content_type("application/json")
633 |> send_resp(403, Jason.encode!(%{"error" => reason}))
637 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
638 Notification.clear(user)
642 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
643 with {:ok, _notif} <- Notification.dismiss(user, id) do
648 |> put_resp_content_type("application/json")
649 |> send_resp(403, Jason.encode!(%{"error" => reason}))
653 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
654 Notification.destroy_multiple(user, ids)
658 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
660 q = from(u in User, where: u.id in ^id)
661 targets = Repo.all(q)
664 |> put_view(AccountView)
665 |> render("relationships.json", %{user: user, targets: targets})
668 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
669 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
671 def update_media(%{assigns: %{user: user}} = conn, data) do
672 with %Object{} = object <- Repo.get(Object, data["id"]),
673 true <- Object.authorize_mutation(object, user),
674 true <- is_binary(data["description"]),
675 description <- data["description"] do
676 new_data = %{object.data | "name" => description}
680 |> Object.change(%{data: new_data})
683 attachment_data = Map.put(new_data, "id", object.id)
686 |> put_view(StatusView)
687 |> render("attachment.json", %{attachment: attachment_data})
691 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
692 with {:ok, object} <-
695 actor: User.ap_id(user),
696 description: Map.get(data, "description")
698 attachment_data = Map.put(object.data, "id", object.id)
701 |> put_view(StatusView)
702 |> render("attachment.json", %{attachment: attachment_data})
706 def favourited_by(conn, %{"id" => id}) do
707 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
708 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
709 q = from(u in User, where: u.ap_id in ^likes)
713 |> put_view(AccountView)
714 |> render(AccountView, "accounts.json", %{users: users, as: :user})
720 def reblogged_by(conn, %{"id" => id}) do
721 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
722 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
723 q = from(u in User, where: u.ap_id in ^announces)
727 |> put_view(AccountView)
728 |> render("accounts.json", %{users: users, as: :user})
734 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
735 local_only = params["local"] in [true, "True", "true", "1"]
738 [params["tag"], params["any"]]
742 |> Enum.map(&String.downcase(&1))
747 |> Enum.map(&String.downcase(&1))
752 |> Enum.map(&String.downcase(&1))
756 |> Map.put("type", "Create")
757 |> Map.put("local_only", local_only)
758 |> Map.put("blocking_user", user)
759 |> Map.put("muting_user", user)
760 |> Map.put("tag", tags)
761 |> Map.put("tag_all", tag_all)
762 |> Map.put("tag_reject", tag_reject)
763 |> ActivityPub.fetch_public_activities()
767 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
768 |> put_view(StatusView)
769 |> render("index.json", %{activities: activities, for: user, as: :activity})
772 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
773 with %User{} = user <- User.get_cached_by_id(id),
774 followers <- MastodonAPI.get_followers(user, params) do
777 for_user && user.id == for_user.id -> followers
778 user.info.hide_followers -> []
783 |> add_link_headers(:followers, followers, user)
784 |> put_view(AccountView)
785 |> render("accounts.json", %{users: followers, as: :user})
789 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
790 with %User{} = user <- User.get_cached_by_id(id),
791 followers <- MastodonAPI.get_friends(user, params) do
794 for_user && user.id == for_user.id -> followers
795 user.info.hide_follows -> []
800 |> add_link_headers(:following, followers, user)
801 |> put_view(AccountView)
802 |> render("accounts.json", %{users: followers, as: :user})
806 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
807 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
809 |> put_view(AccountView)
810 |> render("accounts.json", %{users: follow_requests, as: :user})
814 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
815 with %User{} = follower <- User.get_cached_by_id(id),
816 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
818 |> put_view(AccountView)
819 |> render("relationship.json", %{user: followed, target: follower})
823 |> put_resp_content_type("application/json")
824 |> send_resp(403, Jason.encode!(%{"error" => message}))
828 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
829 with %User{} = follower <- User.get_cached_by_id(id),
830 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
832 |> put_view(AccountView)
833 |> render("relationship.json", %{user: followed, target: follower})
837 |> put_resp_content_type("application/json")
838 |> send_resp(403, Jason.encode!(%{"error" => message}))
842 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
843 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
844 {_, true} <- {:followed, follower.id != followed.id},
845 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
847 |> put_view(AccountView)
848 |> render("relationship.json", %{user: follower, target: followed})
855 |> put_resp_content_type("application/json")
856 |> send_resp(403, Jason.encode!(%{"error" => message}))
860 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
861 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
862 {_, true} <- {:followed, follower.id != followed.id},
863 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
865 |> put_view(AccountView)
866 |> render("account.json", %{user: followed, for: follower})
873 |> put_resp_content_type("application/json")
874 |> send_resp(403, Jason.encode!(%{"error" => message}))
878 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
879 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
880 {_, true} <- {:followed, follower.id != followed.id},
881 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
883 |> put_view(AccountView)
884 |> render("relationship.json", %{user: follower, target: followed})
894 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
895 with %User{} = muted <- User.get_cached_by_id(id),
896 {:ok, muter} <- User.mute(muter, muted) do
898 |> put_view(AccountView)
899 |> render("relationship.json", %{user: muter, target: muted})
903 |> put_resp_content_type("application/json")
904 |> send_resp(403, Jason.encode!(%{"error" => message}))
908 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
909 with %User{} = muted <- User.get_cached_by_id(id),
910 {:ok, muter} <- User.unmute(muter, muted) do
912 |> put_view(AccountView)
913 |> render("relationship.json", %{user: muter, target: muted})
917 |> put_resp_content_type("application/json")
918 |> send_resp(403, Jason.encode!(%{"error" => message}))
922 def mutes(%{assigns: %{user: user}} = conn, _) do
923 with muted_accounts <- User.muted_users(user) do
924 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
929 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
930 with %User{} = blocked <- User.get_cached_by_id(id),
931 {:ok, blocker} <- User.block(blocker, blocked),
932 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
934 |> put_view(AccountView)
935 |> render("relationship.json", %{user: blocker, target: blocked})
939 |> put_resp_content_type("application/json")
940 |> send_resp(403, Jason.encode!(%{"error" => message}))
944 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
945 with %User{} = blocked <- User.get_cached_by_id(id),
946 {:ok, blocker} <- User.unblock(blocker, blocked),
947 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
949 |> put_view(AccountView)
950 |> render("relationship.json", %{user: blocker, target: blocked})
954 |> put_resp_content_type("application/json")
955 |> send_resp(403, Jason.encode!(%{"error" => message}))
959 def blocks(%{assigns: %{user: user}} = conn, _) do
960 with blocked_accounts <- User.blocked_users(user) do
961 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
966 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
967 json(conn, info.domain_blocks || [])
970 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
971 User.block_domain(blocker, domain)
975 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
976 User.unblock_domain(blocker, domain)
980 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
981 with %User{} = subscription_target <- User.get_cached_by_id(id),
982 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
984 |> put_view(AccountView)
985 |> render("relationship.json", %{user: user, target: subscription_target})
989 |> put_resp_content_type("application/json")
990 |> send_resp(403, Jason.encode!(%{"error" => message}))
994 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
995 with %User{} = subscription_target <- User.get_cached_by_id(id),
996 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
998 |> put_view(AccountView)
999 |> render("relationship.json", %{user: user, target: subscription_target})
1001 {:error, message} ->
1003 |> put_resp_content_type("application/json")
1004 |> send_resp(403, Jason.encode!(%{"error" => message}))
1008 def status_search(user, query) do
1010 if Regex.match?(~r/https?:/, query) do
1011 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1012 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1013 true <- Visibility.visible_for_user?(activity, user) do
1022 [a, o] in Activity.with_preloaded_object(Activity),
1023 where: fragment("?->>'type' = 'Create'", a.data),
1024 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1027 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1032 order_by: [desc: :id]
1035 Repo.all(q) ++ fetched
1038 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1039 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1041 statuses = status_search(user, query)
1043 tags_path = Web.base_url() <> "/tag/"
1049 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1050 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1051 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1054 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1056 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1063 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1064 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1066 statuses = status_search(user, query)
1072 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1073 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1076 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1078 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1085 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1086 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1088 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1093 def favourites(%{assigns: %{user: user}} = conn, params) do
1096 |> Map.put("type", "Create")
1097 |> Map.put("favorited_by", user.ap_id)
1098 |> Map.put("blocking_user", user)
1101 ActivityPub.fetch_activities([], params)
1104 user = Repo.preload(user, bookmarks: :activity)
1107 |> add_link_headers(:favourites, activities)
1108 |> put_view(StatusView)
1109 |> render("index.json", %{activities: activities, for: user, as: :activity})
1112 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1113 with %User{} = user <- User.get_by_id(id),
1114 false <- user.info.hide_favorites do
1117 |> Map.put("type", "Create")
1118 |> Map.put("favorited_by", user.ap_id)
1119 |> Map.put("blocking_user", for_user)
1123 ["https://www.w3.org/ns/activitystreams#Public"] ++
1124 [for_user.ap_id | for_user.following]
1126 ["https://www.w3.org/ns/activitystreams#Public"]
1131 |> ActivityPub.fetch_activities(params)
1135 |> add_link_headers(:favourites, activities)
1136 |> put_view(StatusView)
1137 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1140 {:error, :not_found}
1145 |> json(%{error: "Can't get favorites"})
1149 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1150 user = User.get_cached_by_id(user.id)
1151 user = Repo.preload(user, bookmarks: :activity)
1154 Bookmark.for_user_query(user.id)
1155 |> Pagination.fetch_paginated(params)
1159 |> Enum.map(fn b -> b.activity end)
1162 |> add_link_headers(:bookmarks, bookmarks)
1163 |> put_view(StatusView)
1164 |> render("index.json", %{activities: activities, for: user, as: :activity})
1167 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1168 lists = Pleroma.List.for_user(user, opts)
1169 res = ListView.render("lists.json", lists: lists)
1173 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1174 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1175 res = ListView.render("list.json", list: list)
1181 |> json(%{error: "Record not found"})
1185 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1186 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1187 res = ListView.render("lists.json", lists: lists)
1191 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1192 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1193 {:ok, _list} <- Pleroma.List.delete(list) do
1201 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1202 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1203 res = ListView.render("list.json", list: list)
1208 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1210 |> Enum.each(fn account_id ->
1211 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1212 %User{} = followed <- User.get_cached_by_id(account_id) do
1213 Pleroma.List.follow(list, followed)
1220 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1222 |> Enum.each(fn account_id ->
1223 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1224 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1225 Pleroma.List.unfollow(list, followed)
1232 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1233 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1234 {:ok, users} = Pleroma.List.get_following(list) do
1236 |> put_view(AccountView)
1237 |> render("accounts.json", %{users: users, as: :user})
1241 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1242 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1243 {:ok, list} <- Pleroma.List.rename(list, title) do
1244 res = ListView.render("list.json", list: list)
1252 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1253 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1256 |> Map.put("type", "Create")
1257 |> Map.put("blocking_user", user)
1258 |> Map.put("muting_user", user)
1260 # we must filter the following list for the user to avoid leaking statuses the user
1261 # does not actually have permission to see (for more info, peruse security issue #270).
1264 |> Enum.filter(fn x -> x in user.following end)
1265 |> ActivityPub.fetch_activities_bounded(following, params)
1268 user = Repo.preload(user, bookmarks: :activity)
1271 |> put_view(StatusView)
1272 |> render("index.json", %{activities: activities, for: user, as: :activity})
1277 |> json(%{error: "Error."})
1281 def index(%{assigns: %{user: user}} = conn, _params) do
1282 token = get_session(conn, :oauth_token)
1285 mastodon_emoji = mastodonized_emoji()
1287 limit = Config.get([:instance, :limit])
1290 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1292 flavour = get_user_flavour(user)
1297 streaming_api_base_url:
1298 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1299 access_token: token,
1301 domain: Pleroma.Web.Endpoint.host(),
1304 unfollow_modal: false,
1307 auto_play_gif: false,
1308 display_sensitive_media: false,
1309 reduce_motion: false,
1310 max_toot_chars: limit,
1311 mascot: "/images/pleroma-fox-tan-smol.png"
1314 delete_others_notice: present?(user.info.is_moderator),
1315 admin: present?(user.info.is_admin)
1319 default_privacy: user.info.default_scope,
1320 default_sensitive: false,
1321 allow_content_types: Config.get([:instance, :allowed_post_formats])
1323 media_attachments: %{
1324 accept_content_types: [
1340 user.info.settings ||
1370 push_subscription: nil,
1372 custom_emojis: mastodon_emoji,
1378 |> put_layout(false)
1379 |> put_view(MastodonView)
1380 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1383 |> put_session(:return_to, conn.request_path)
1384 |> redirect(to: "/web/login")
1388 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1389 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1391 with changeset <- Ecto.Changeset.change(user),
1392 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1393 {:ok, _user} <- User.update_and_set_cache(changeset) do
1398 |> put_resp_content_type("application/json")
1399 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1403 @supported_flavours ["glitch", "vanilla"]
1405 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1406 when flavour in @supported_flavours do
1407 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1409 with changeset <- Ecto.Changeset.change(user),
1410 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1411 {:ok, user} <- User.update_and_set_cache(changeset),
1412 flavour <- user.info.flavour do
1417 |> put_resp_content_type("application/json")
1418 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1422 def set_flavour(conn, _params) do
1425 |> json(%{error: "Unsupported flavour"})
1428 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1429 json(conn, get_user_flavour(user))
1432 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1436 defp get_user_flavour(_) do
1440 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1441 redirect(conn, to: local_mastodon_root_path(conn))
1444 @doc "Local Mastodon FE login init action"
1445 def login(conn, %{"code" => auth_token}) do
1446 with {:ok, app} <- get_or_make_app(),
1447 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1448 {:ok, token} <- Token.exchange_token(app, auth) do
1450 |> put_session(:oauth_token, token.token)
1451 |> redirect(to: local_mastodon_root_path(conn))
1455 @doc "Local Mastodon FE callback action"
1456 def login(conn, _) do
1457 with {:ok, app} <- get_or_make_app() do
1462 response_type: "code",
1463 client_id: app.client_id,
1465 scope: Enum.join(app.scopes, " ")
1468 redirect(conn, to: path)
1472 defp local_mastodon_root_path(conn) do
1473 case get_session(conn, :return_to) do
1475 mastodon_api_path(conn, :index, ["getting-started"])
1478 delete_session(conn, :return_to)
1483 defp get_or_make_app do
1484 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1485 scopes = ["read", "write", "follow", "push"]
1487 with %App{} = app <- Repo.get_by(App, find_attrs) do
1489 if app.scopes == scopes do
1493 |> Ecto.Changeset.change(%{scopes: scopes})
1501 App.register_changeset(
1503 Map.put(find_attrs, :scopes, scopes)
1510 def logout(conn, _) do
1513 |> redirect(to: "/")
1516 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1517 Logger.debug("Unimplemented, returning unmodified relationship")
1519 with %User{} = target <- User.get_cached_by_id(id) do
1521 |> put_view(AccountView)
1522 |> render("relationship.json", %{user: user, target: target})
1526 def empty_array(conn, _) do
1527 Logger.debug("Unimplemented, returning an empty array")
1531 def empty_object(conn, _) do
1532 Logger.debug("Unimplemented, returning an empty object")
1536 def get_filters(%{assigns: %{user: user}} = conn, _) do
1537 filters = Filter.get_filters(user)
1538 res = FilterView.render("filters.json", filters: filters)
1543 %{assigns: %{user: user}} = conn,
1544 %{"phrase" => phrase, "context" => context} = params
1550 hide: Map.get(params, "irreversible", nil),
1551 whole_word: Map.get(params, "boolean", true)
1555 {:ok, response} = Filter.create(query)
1556 res = FilterView.render("filter.json", filter: response)
1560 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1561 filter = Filter.get(filter_id, user)
1562 res = FilterView.render("filter.json", filter: filter)
1567 %{assigns: %{user: user}} = conn,
1568 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1572 filter_id: filter_id,
1575 hide: Map.get(params, "irreversible", nil),
1576 whole_word: Map.get(params, "boolean", true)
1580 {:ok, response} = Filter.update(query)
1581 res = FilterView.render("filter.json", filter: response)
1585 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1588 filter_id: filter_id
1591 {:ok, _} = Filter.delete(query)
1597 def errors(conn, {:error, %Changeset{} = changeset}) do
1600 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1601 |> Enum.map_join(", ", fn {_k, v} -> v end)
1605 |> json(%{error: error_message})
1608 def errors(conn, {:error, :not_found}) do
1611 |> json(%{error: "Record not found"})
1614 def errors(conn, _) do
1617 |> json("Something went wrong")
1620 def suggestions(%{assigns: %{user: user}} = conn, _) do
1621 suggestions = Config.get(:suggestions)
1623 if Keyword.get(suggestions, :enabled, false) do
1624 api = Keyword.get(suggestions, :third_party_engine, "")
1625 timeout = Keyword.get(suggestions, :timeout, 5000)
1626 limit = Keyword.get(suggestions, :limit, 23)
1628 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1630 user = user.nickname
1634 |> String.replace("{{host}}", host)
1635 |> String.replace("{{user}}", user)
1637 with {:ok, %{status: 200, body: body}} <-
1642 recv_timeout: timeout,
1646 {:ok, data} <- Jason.decode(body) do
1649 |> Enum.slice(0, limit)
1654 case User.get_or_fetch(x["acct"]) do
1661 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1664 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1670 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1677 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1678 with %Activity{} = activity <- Activity.get_by_id(status_id),
1679 true <- Visibility.visible_for_user?(activity, user) do
1683 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1693 def reports(%{assigns: %{user: user}} = conn, params) do
1694 case CommonAPI.report(user, params) do
1697 |> put_view(ReportView)
1698 |> try_render("report.json", %{activity: activity})
1702 |> put_status(:bad_request)
1703 |> json(%{error: err})
1707 def try_render(conn, target, params)
1708 when is_binary(target) do
1709 res = render(conn, target, params)
1714 |> json(%{error: "Can't display this activity"})
1720 def try_render(conn, _, _) do
1723 |> json(%{error: "Can't display this activity"})
1726 defp present?(nil), do: false
1727 defp present?(false), do: false
1728 defp present?(_), do: true