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.Token
42 alias Pleroma.Web.ControllerHelper
47 @httpoison Application.get_env(:pleroma, :httpoison)
48 @local_mastodon_name "Mastodon-Local"
50 action_fallback(:errors)
52 def create_app(conn, params) do
53 scopes = ControllerHelper.oauth_scopes(params, ["read"])
57 |> Map.drop(["scope", "scopes"])
58 |> Map.put("scopes", scopes)
60 with cs <- App.register_changeset(%App{}, app_attrs),
61 false <- cs.changes[:client_name] == @local_mastodon_name,
62 {:ok, app} <- Repo.insert(cs) do
65 |> render("show.json", %{app: app})
74 value_function \\ fn x -> {:ok, x} end
76 if Map.has_key?(params, params_field) do
77 case value_function.(params[params_field]) do
78 {:ok, new_value} -> Map.put(map, map_field, new_value)
86 def update_credentials(%{assigns: %{user: user}} = conn, params) do
91 |> add_if_present(params, "display_name", :name)
92 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
93 |> add_if_present(params, "avatar", :avatar, fn value ->
94 with %Plug.Upload{} <- value,
95 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
102 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
105 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
109 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
110 |> Enum.reduce(%{}, fn key, acc ->
111 add_if_present(acc, params, to_string(key), key, fn value ->
112 {:ok, ControllerHelper.truthy_param?(value)}
115 |> add_if_present(params, "default_scope", :default_scope)
116 |> add_if_present(params, "header", :banner, fn value ->
117 with %Plug.Upload{} <- value,
118 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
124 |> Map.put(:emoji, user_info_emojis)
126 info_cng = User.Info.profile_update(user.info, info_params)
128 with changeset <- User.update_changeset(user, user_params),
129 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
130 {:ok, user} <- User.update_and_set_cache(changeset) do
131 if original_user != user do
132 CommonAPI.update(user)
135 json(conn, AccountView.render("account.json", %{user: user, for: user}))
140 |> json(%{error: "Invalid request"})
144 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
145 account = AccountView.render("account.json", %{user: user, for: user})
149 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
150 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
153 |> render("short.json", %{app: app})
157 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
158 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
159 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
160 account = AccountView.render("account.json", %{user: user, for: for_user})
166 |> json(%{error: "Can't find user"})
170 @mastodon_api_level "2.6.5"
172 def masto_instance(conn, _params) do
173 instance = Config.get(:instance)
177 title: Keyword.get(instance, :name),
178 description: Keyword.get(instance, :description),
179 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
180 email: Keyword.get(instance, :email),
182 streaming_api: Pleroma.Web.Endpoint.websocket_url()
184 stats: Stats.get_stats(),
185 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
187 registrations: Pleroma.Config.get([:instance, :registrations_open]),
188 # Extra (not present in Mastodon):
189 max_toot_chars: Keyword.get(instance, :limit)
195 def peers(conn, _params) do
196 json(conn, Stats.get_peers())
199 defp mastodonized_emoji do
200 Pleroma.Emoji.get_all()
201 |> Enum.map(fn {shortcode, relative_url, tags} ->
202 url = to_string(URI.merge(Web.base_url(), relative_url))
205 "shortcode" => shortcode,
207 "visible_in_picker" => true,
214 def custom_emojis(conn, _params) do
215 mastodon_emoji = mastodonized_emoji()
216 json(conn, mastodon_emoji)
219 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
222 |> Map.drop(["since_id", "max_id", "min_id"])
225 last = List.last(activities)
232 |> Map.get("limit", "20")
233 |> String.to_integer()
236 if length(activities) <= limit do
242 |> Enum.at(limit * -1)
246 {next_url, prev_url} =
250 Pleroma.Web.Endpoint,
253 Map.merge(params, %{max_id: max_id})
256 Pleroma.Web.Endpoint,
259 Map.merge(params, %{min_id: min_id})
265 Pleroma.Web.Endpoint,
267 Map.merge(params, %{max_id: max_id})
270 Pleroma.Web.Endpoint,
272 Map.merge(params, %{min_id: min_id})
278 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
284 def home_timeline(%{assigns: %{user: user}} = conn, params) do
287 |> Map.put("type", ["Create", "Announce"])
288 |> Map.put("blocking_user", user)
289 |> Map.put("muting_user", user)
290 |> Map.put("user", user)
293 [user.ap_id | user.following]
294 |> ActivityPub.fetch_activities(params)
295 |> ActivityPub.contain_timeline(user)
299 |> add_link_headers(:home_timeline, activities)
300 |> put_view(StatusView)
301 |> render("index.json", %{activities: activities, for: user, as: :activity})
304 def public_timeline(%{assigns: %{user: user}} = conn, params) do
305 local_only = params["local"] in [true, "True", "true", "1"]
309 |> Map.put("type", ["Create", "Announce"])
310 |> Map.put("local_only", local_only)
311 |> Map.put("blocking_user", user)
312 |> Map.put("muting_user", user)
313 |> ActivityPub.fetch_public_activities()
317 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
318 |> put_view(StatusView)
319 |> render("index.json", %{activities: activities, for: user, as: :activity})
322 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
323 with %User{} = user <- User.get_cached_by_id(params["id"]) do
324 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
327 |> add_link_headers(:user_statuses, activities, params["id"])
328 |> put_view(StatusView)
329 |> render("index.json", %{
330 activities: activities,
337 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
340 |> Map.put("type", "Create")
341 |> Map.put("blocking_user", user)
342 |> Map.put("user", user)
343 |> Map.put(:visibility, "direct")
347 |> ActivityPub.fetch_activities_query(params)
348 |> Pagination.fetch_paginated(params)
351 |> add_link_headers(:dm_timeline, activities)
352 |> put_view(StatusView)
353 |> render("index.json", %{activities: activities, for: user, as: :activity})
356 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
357 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
358 true <- Visibility.visible_for_user?(activity, user) do
360 |> put_view(StatusView)
361 |> try_render("status.json", %{activity: activity, for: user})
365 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
366 with %Activity{} = activity <- Activity.get_by_id(id),
368 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
369 "blocking_user" => user,
373 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
375 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
376 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
382 activities: grouped_activities[true] || [],
386 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
391 activities: grouped_activities[false] || [],
395 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
402 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
403 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
405 |> add_link_headers(:scheduled_statuses, scheduled_activities)
406 |> put_view(ScheduledActivityView)
407 |> render("index.json", %{scheduled_activities: scheduled_activities})
411 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
412 with %ScheduledActivity{} = scheduled_activity <-
413 ScheduledActivity.get(user, scheduled_activity_id) do
415 |> put_view(ScheduledActivityView)
416 |> render("show.json", %{scheduled_activity: scheduled_activity})
418 _ -> {:error, :not_found}
422 def update_scheduled_status(
423 %{assigns: %{user: user}} = conn,
424 %{"id" => scheduled_activity_id} = params
426 with %ScheduledActivity{} = scheduled_activity <-
427 ScheduledActivity.get(user, scheduled_activity_id),
428 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
430 |> put_view(ScheduledActivityView)
431 |> render("show.json", %{scheduled_activity: scheduled_activity})
433 nil -> {:error, :not_found}
438 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
439 with %ScheduledActivity{} = scheduled_activity <-
440 ScheduledActivity.get(user, scheduled_activity_id),
441 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
443 |> put_view(ScheduledActivityView)
444 |> render("show.json", %{scheduled_activity: scheduled_activity})
446 nil -> {:error, :not_found}
451 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
452 when length(media_ids) > 0 do
455 |> Map.put("status", ".")
457 post_status(conn, params)
460 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
463 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
466 case get_req_header(conn, "idempotency-key") do
468 _ -> Ecto.UUID.generate()
471 scheduled_at = params["scheduled_at"]
473 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
474 with {:ok, scheduled_activity} <-
475 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
477 |> put_view(ScheduledActivityView)
478 |> render("show.json", %{scheduled_activity: scheduled_activity})
481 params = Map.drop(params, ["scheduled_at"])
484 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
485 CommonAPI.post(user, params)
489 |> put_view(StatusView)
490 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
494 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
495 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
501 |> json(%{error: "Can't delete this post"})
505 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
506 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
507 %Activity{} = announce <- Activity.normalize(announce.data) do
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
518 |> put_view(StatusView)
519 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
523 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
524 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
525 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
527 |> put_view(StatusView)
528 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
532 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
533 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
534 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
536 |> put_view(StatusView)
537 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
541 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
542 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
544 |> put_view(StatusView)
545 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
549 |> put_resp_content_type("application/json")
550 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
554 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
555 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
557 |> put_view(StatusView)
558 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
562 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
563 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
564 %User{} = user <- User.get_cached_by_nickname(user.nickname),
565 true <- Visibility.visible_for_user?(activity, user),
566 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
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
579 |> put_view(StatusView)
580 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
584 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
585 activity = Activity.get_by_id(id)
587 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
589 |> put_view(StatusView)
590 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
594 |> put_resp_content_type("application/json")
595 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
599 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
600 activity = Activity.get_by_id(id)
602 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
604 |> put_view(StatusView)
605 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
609 def notifications(%{assigns: %{user: user}} = conn, params) do
610 notifications = MastodonAPI.get_notifications(user, params)
613 |> add_link_headers(:notifications, notifications)
614 |> put_view(NotificationView)
615 |> render("index.json", %{notifications: notifications, for: user})
618 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
619 with {:ok, notification} <- Notification.get(user, id) do
621 |> put_view(NotificationView)
622 |> render("show.json", %{notification: notification, for: user})
626 |> put_resp_content_type("application/json")
627 |> send_resp(403, Jason.encode!(%{"error" => reason}))
631 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
632 Notification.clear(user)
636 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
637 with {:ok, _notif} <- Notification.dismiss(user, id) do
642 |> put_resp_content_type("application/json")
643 |> send_resp(403, Jason.encode!(%{"error" => reason}))
647 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
648 Notification.destroy_multiple(user, ids)
652 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
654 q = from(u in User, where: u.id in ^id)
655 targets = Repo.all(q)
658 |> put_view(AccountView)
659 |> render("relationships.json", %{user: user, targets: targets})
662 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
663 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
665 def update_media(%{assigns: %{user: user}} = conn, data) do
666 with %Object{} = object <- Repo.get(Object, data["id"]),
667 true <- Object.authorize_mutation(object, user),
668 true <- is_binary(data["description"]),
669 description <- data["description"] do
670 new_data = %{object.data | "name" => description}
674 |> Object.change(%{data: new_data})
677 attachment_data = Map.put(new_data, "id", object.id)
680 |> put_view(StatusView)
681 |> render("attachment.json", %{attachment: attachment_data})
685 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
686 with {:ok, object} <-
689 actor: User.ap_id(user),
690 description: Map.get(data, "description")
692 attachment_data = Map.put(object.data, "id", object.id)
695 |> put_view(StatusView)
696 |> render("attachment.json", %{attachment: attachment_data})
700 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
701 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
702 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
703 q = from(u in User, where: u.ap_id in ^likes)
707 |> put_view(AccountView)
708 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
714 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
715 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
716 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
717 q = from(u in User, where: u.ap_id in ^announces)
721 |> put_view(AccountView)
722 |> render("accounts.json", %{for: user, users: users, as: :user})
728 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
729 local_only = params["local"] in [true, "True", "true", "1"]
732 [params["tag"], params["any"]]
736 |> Enum.map(&String.downcase(&1))
741 |> Enum.map(&String.downcase(&1))
746 |> Enum.map(&String.downcase(&1))
750 |> Map.put("type", "Create")
751 |> Map.put("local_only", local_only)
752 |> Map.put("blocking_user", user)
753 |> Map.put("muting_user", user)
754 |> Map.put("tag", tags)
755 |> Map.put("tag_all", tag_all)
756 |> Map.put("tag_reject", tag_reject)
757 |> ActivityPub.fetch_public_activities()
761 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
762 |> put_view(StatusView)
763 |> render("index.json", %{activities: activities, for: user, as: :activity})
766 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
767 with %User{} = user <- User.get_cached_by_id(id),
768 followers <- MastodonAPI.get_followers(user, params) do
771 for_user && user.id == for_user.id -> followers
772 user.info.hide_followers -> []
777 |> add_link_headers(:followers, followers, user)
778 |> put_view(AccountView)
779 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
783 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
784 with %User{} = user <- User.get_cached_by_id(id),
785 followers <- MastodonAPI.get_friends(user, params) do
788 for_user && user.id == for_user.id -> followers
789 user.info.hide_follows -> []
794 |> add_link_headers(:following, followers, user)
795 |> put_view(AccountView)
796 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
800 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
801 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
803 |> put_view(AccountView)
804 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
808 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
809 with %User{} = follower <- User.get_cached_by_id(id),
810 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
812 |> put_view(AccountView)
813 |> render("relationship.json", %{user: followed, target: follower})
817 |> put_resp_content_type("application/json")
818 |> send_resp(403, Jason.encode!(%{"error" => message}))
822 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
823 with %User{} = follower <- User.get_cached_by_id(id),
824 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
826 |> put_view(AccountView)
827 |> render("relationship.json", %{user: followed, target: follower})
831 |> put_resp_content_type("application/json")
832 |> send_resp(403, Jason.encode!(%{"error" => message}))
836 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
837 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
838 {_, true} <- {:followed, follower.id != followed.id},
839 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
841 |> put_view(AccountView)
842 |> render("relationship.json", %{user: follower, target: followed})
849 |> put_resp_content_type("application/json")
850 |> send_resp(403, Jason.encode!(%{"error" => message}))
854 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
855 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
856 {_, true} <- {:followed, follower.id != followed.id},
857 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
859 |> put_view(AccountView)
860 |> render("account.json", %{user: followed, for: follower})
867 |> put_resp_content_type("application/json")
868 |> send_resp(403, Jason.encode!(%{"error" => message}))
872 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
873 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
874 {_, true} <- {:followed, follower.id != followed.id},
875 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
877 |> put_view(AccountView)
878 |> render("relationship.json", %{user: follower, target: followed})
888 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
889 with %User{} = muted <- User.get_cached_by_id(id),
890 {:ok, muter} <- User.mute(muter, muted) do
892 |> put_view(AccountView)
893 |> render("relationship.json", %{user: muter, target: muted})
897 |> put_resp_content_type("application/json")
898 |> send_resp(403, Jason.encode!(%{"error" => message}))
902 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
903 with %User{} = muted <- User.get_cached_by_id(id),
904 {:ok, muter} <- User.unmute(muter, muted) do
906 |> put_view(AccountView)
907 |> render("relationship.json", %{user: muter, target: muted})
911 |> put_resp_content_type("application/json")
912 |> send_resp(403, Jason.encode!(%{"error" => message}))
916 def mutes(%{assigns: %{user: user}} = conn, _) do
917 with muted_accounts <- User.muted_users(user) do
918 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
923 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
924 with %User{} = blocked <- User.get_cached_by_id(id),
925 {:ok, blocker} <- User.block(blocker, blocked),
926 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
928 |> put_view(AccountView)
929 |> render("relationship.json", %{user: blocker, target: blocked})
933 |> put_resp_content_type("application/json")
934 |> send_resp(403, Jason.encode!(%{"error" => message}))
938 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
939 with %User{} = blocked <- User.get_cached_by_id(id),
940 {:ok, blocker} <- User.unblock(blocker, blocked),
941 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
943 |> put_view(AccountView)
944 |> render("relationship.json", %{user: blocker, target: blocked})
948 |> put_resp_content_type("application/json")
949 |> send_resp(403, Jason.encode!(%{"error" => message}))
953 def blocks(%{assigns: %{user: user}} = conn, _) do
954 with blocked_accounts <- User.blocked_users(user) do
955 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
960 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
961 json(conn, info.domain_blocks || [])
964 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
965 User.block_domain(blocker, domain)
969 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
970 User.unblock_domain(blocker, domain)
974 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
975 with %User{} = subscription_target <- User.get_cached_by_id(id),
976 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
978 |> put_view(AccountView)
979 |> render("relationship.json", %{user: user, target: subscription_target})
983 |> put_resp_content_type("application/json")
984 |> send_resp(403, Jason.encode!(%{"error" => message}))
988 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
989 with %User{} = subscription_target <- User.get_cached_by_id(id),
990 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
992 |> put_view(AccountView)
993 |> render("relationship.json", %{user: user, target: subscription_target})
997 |> put_resp_content_type("application/json")
998 |> send_resp(403, Jason.encode!(%{"error" => message}))
1002 def status_search(user, query) do
1004 if Regex.match?(~r/https?:/, query) do
1005 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1006 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1007 true <- Visibility.visible_for_user?(activity, user) do
1016 [a, o] in Activity.with_preloaded_object(Activity),
1017 where: fragment("?->>'type' = 'Create'", a.data),
1018 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1021 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1026 order_by: [desc: :id]
1029 Repo.all(q) ++ fetched
1032 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1033 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1035 statuses = status_search(user, query)
1037 tags_path = Web.base_url() <> "/tag/"
1043 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1044 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1045 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1048 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1050 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1057 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1058 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1060 statuses = status_search(user, query)
1066 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1067 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1070 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1072 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1079 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1080 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1082 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1087 def favourites(%{assigns: %{user: user}} = conn, params) do
1090 |> Map.put("type", "Create")
1091 |> Map.put("favorited_by", user.ap_id)
1092 |> Map.put("blocking_user", user)
1095 ActivityPub.fetch_activities([], params)
1099 |> add_link_headers(:favourites, activities)
1100 |> put_view(StatusView)
1101 |> render("index.json", %{activities: activities, for: user, as: :activity})
1104 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1105 with %User{} = user <- User.get_by_id(id),
1106 false <- user.info.hide_favorites do
1109 |> Map.put("type", "Create")
1110 |> Map.put("favorited_by", user.ap_id)
1111 |> Map.put("blocking_user", for_user)
1115 ["https://www.w3.org/ns/activitystreams#Public"] ++
1116 [for_user.ap_id | for_user.following]
1118 ["https://www.w3.org/ns/activitystreams#Public"]
1123 |> ActivityPub.fetch_activities(params)
1127 |> add_link_headers(:favourites, activities)
1128 |> put_view(StatusView)
1129 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1132 {:error, :not_found}
1137 |> json(%{error: "Can't get favorites"})
1141 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1142 user = User.get_cached_by_id(user.id)
1145 Bookmark.for_user_query(user.id)
1146 |> Pagination.fetch_paginated(params)
1150 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1153 |> add_link_headers(:bookmarks, bookmarks)
1154 |> put_view(StatusView)
1155 |> render("index.json", %{activities: activities, for: user, as: :activity})
1158 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1159 lists = Pleroma.List.for_user(user, opts)
1160 res = ListView.render("lists.json", lists: lists)
1164 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1165 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1166 res = ListView.render("list.json", list: list)
1172 |> json(%{error: "Record not found"})
1176 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1177 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1178 res = ListView.render("lists.json", lists: lists)
1182 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1183 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1184 {:ok, _list} <- Pleroma.List.delete(list) do
1192 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1193 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1194 res = ListView.render("list.json", list: list)
1199 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1201 |> Enum.each(fn account_id ->
1202 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1203 %User{} = followed <- User.get_cached_by_id(account_id) do
1204 Pleroma.List.follow(list, followed)
1211 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1213 |> Enum.each(fn account_id ->
1214 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1215 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1216 Pleroma.List.unfollow(list, followed)
1223 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1224 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1225 {:ok, users} = Pleroma.List.get_following(list) do
1227 |> put_view(AccountView)
1228 |> render("accounts.json", %{for: user, users: users, as: :user})
1232 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1233 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1234 {:ok, list} <- Pleroma.List.rename(list, title) do
1235 res = ListView.render("list.json", list: list)
1243 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1244 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1247 |> Map.put("type", "Create")
1248 |> Map.put("blocking_user", user)
1249 |> Map.put("muting_user", user)
1251 # we must filter the following list for the user to avoid leaking statuses the user
1252 # does not actually have permission to see (for more info, peruse security issue #270).
1255 |> Enum.filter(fn x -> x in user.following end)
1256 |> ActivityPub.fetch_activities_bounded(following, params)
1260 |> put_view(StatusView)
1261 |> render("index.json", %{activities: activities, for: user, as: :activity})
1266 |> json(%{error: "Error."})
1270 def index(%{assigns: %{user: user}} = conn, _params) do
1271 token = get_session(conn, :oauth_token)
1274 mastodon_emoji = mastodonized_emoji()
1276 limit = Config.get([:instance, :limit])
1279 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1281 flavour = get_user_flavour(user)
1286 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1287 access_token: token,
1289 domain: Pleroma.Web.Endpoint.host(),
1292 unfollow_modal: false,
1295 auto_play_gif: false,
1296 display_sensitive_media: false,
1297 reduce_motion: false,
1298 max_toot_chars: limit,
1299 mascot: "/images/pleroma-fox-tan-smol.png"
1302 delete_others_notice: present?(user.info.is_moderator),
1303 admin: present?(user.info.is_admin)
1307 default_privacy: user.info.default_scope,
1308 default_sensitive: false,
1309 allow_content_types: Config.get([:instance, :allowed_post_formats])
1311 media_attachments: %{
1312 accept_content_types: [
1328 user.info.settings ||
1358 push_subscription: nil,
1360 custom_emojis: mastodon_emoji,
1366 |> put_layout(false)
1367 |> put_view(MastodonView)
1368 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1371 |> put_session(:return_to, conn.request_path)
1372 |> redirect(to: "/web/login")
1376 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1377 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1379 with changeset <- Ecto.Changeset.change(user),
1380 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1381 {:ok, _user} <- User.update_and_set_cache(changeset) do
1386 |> put_resp_content_type("application/json")
1387 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1391 @supported_flavours ["glitch", "vanilla"]
1393 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1394 when flavour in @supported_flavours do
1395 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1397 with changeset <- Ecto.Changeset.change(user),
1398 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1399 {:ok, user} <- User.update_and_set_cache(changeset),
1400 flavour <- user.info.flavour do
1405 |> put_resp_content_type("application/json")
1406 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1410 def set_flavour(conn, _params) do
1413 |> json(%{error: "Unsupported flavour"})
1416 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1417 json(conn, get_user_flavour(user))
1420 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1424 defp get_user_flavour(_) do
1428 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1429 redirect(conn, to: local_mastodon_root_path(conn))
1432 @doc "Local Mastodon FE login init action"
1433 def login(conn, %{"code" => auth_token}) do
1434 with {:ok, app} <- get_or_make_app(),
1435 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1436 {:ok, token} <- Token.exchange_token(app, auth) do
1438 |> put_session(:oauth_token, token.token)
1439 |> redirect(to: local_mastodon_root_path(conn))
1443 @doc "Local Mastodon FE callback action"
1444 def login(conn, _) do
1445 with {:ok, app} <- get_or_make_app() do
1450 response_type: "code",
1451 client_id: app.client_id,
1453 scope: Enum.join(app.scopes, " ")
1456 redirect(conn, to: path)
1460 defp local_mastodon_root_path(conn) do
1461 case get_session(conn, :return_to) do
1463 mastodon_api_path(conn, :index, ["getting-started"])
1466 delete_session(conn, :return_to)
1471 defp get_or_make_app do
1472 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1473 scopes = ["read", "write", "follow", "push"]
1475 with %App{} = app <- Repo.get_by(App, find_attrs) do
1477 if app.scopes == scopes do
1481 |> Ecto.Changeset.change(%{scopes: scopes})
1489 App.register_changeset(
1491 Map.put(find_attrs, :scopes, scopes)
1498 def logout(conn, _) do
1501 |> redirect(to: "/")
1504 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1505 Logger.debug("Unimplemented, returning unmodified relationship")
1507 with %User{} = target <- User.get_cached_by_id(id) do
1509 |> put_view(AccountView)
1510 |> render("relationship.json", %{user: user, target: target})
1514 def empty_array(conn, _) do
1515 Logger.debug("Unimplemented, returning an empty array")
1519 def empty_object(conn, _) do
1520 Logger.debug("Unimplemented, returning an empty object")
1524 def get_filters(%{assigns: %{user: user}} = conn, _) do
1525 filters = Filter.get_filters(user)
1526 res = FilterView.render("filters.json", filters: filters)
1531 %{assigns: %{user: user}} = conn,
1532 %{"phrase" => phrase, "context" => context} = params
1538 hide: Map.get(params, "irreversible", nil),
1539 whole_word: Map.get(params, "boolean", true)
1543 {:ok, response} = Filter.create(query)
1544 res = FilterView.render("filter.json", filter: response)
1548 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1549 filter = Filter.get(filter_id, user)
1550 res = FilterView.render("filter.json", filter: filter)
1555 %{assigns: %{user: user}} = conn,
1556 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1560 filter_id: filter_id,
1563 hide: Map.get(params, "irreversible", nil),
1564 whole_word: Map.get(params, "boolean", true)
1568 {:ok, response} = Filter.update(query)
1569 res = FilterView.render("filter.json", filter: response)
1573 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1576 filter_id: filter_id
1579 {:ok, _} = Filter.delete(query)
1585 def errors(conn, {:error, %Changeset{} = changeset}) do
1588 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1589 |> Enum.map_join(", ", fn {_k, v} -> v end)
1593 |> json(%{error: error_message})
1596 def errors(conn, {:error, :not_found}) do
1599 |> json(%{error: "Record not found"})
1602 def errors(conn, _) do
1605 |> json("Something went wrong")
1608 def suggestions(%{assigns: %{user: user}} = conn, _) do
1609 suggestions = Config.get(:suggestions)
1611 if Keyword.get(suggestions, :enabled, false) do
1612 api = Keyword.get(suggestions, :third_party_engine, "")
1613 timeout = Keyword.get(suggestions, :timeout, 5000)
1614 limit = Keyword.get(suggestions, :limit, 23)
1616 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1618 user = user.nickname
1622 |> String.replace("{{host}}", host)
1623 |> String.replace("{{user}}", user)
1625 with {:ok, %{status: 200, body: body}} <-
1630 recv_timeout: timeout,
1634 {:ok, data} <- Jason.decode(body) do
1637 |> Enum.slice(0, limit)
1642 case User.get_or_fetch(x["acct"]) do
1643 {:ok, %User{id: id}} -> id
1649 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1652 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1658 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1665 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1666 with %Activity{} = activity <- Activity.get_by_id(status_id),
1667 true <- Visibility.visible_for_user?(activity, user) do
1671 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1681 def reports(%{assigns: %{user: user}} = conn, params) do
1682 case CommonAPI.report(user, params) do
1685 |> put_view(ReportView)
1686 |> try_render("report.json", %{activity: activity})
1690 |> put_status(:bad_request)
1691 |> json(%{error: err})
1695 def conversations(%{assigns: %{user: user}} = conn, params) do
1696 participations = Participation.for_user_with_last_activity_id(user, params)
1699 Enum.map(participations, fn participation ->
1700 ConversationView.render("participation.json", %{participation: participation, user: user})
1704 |> add_link_headers(:conversations, participations)
1705 |> json(conversations)
1708 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1709 with %Participation{} = participation <-
1710 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1711 {:ok, participation} <- Participation.mark_as_read(participation) do
1712 participation_view =
1713 ConversationView.render("participation.json", %{participation: participation, user: user})
1716 |> json(participation_view)
1720 def try_render(conn, target, params)
1721 when is_binary(target) do
1722 res = render(conn, target, params)
1727 |> json(%{error: "Can't display this activity"})
1733 def try_render(conn, _, _) do
1736 |> json(%{error: "Can't display this activity"})
1739 defp present?(nil), do: false
1740 defp present?(false), do: false
1741 defp present?(_), do: true