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)
298 user = Repo.preload(user, bookmarks: :activity)
301 |> add_link_headers(:home_timeline, activities)
302 |> put_view(StatusView)
303 |> render("index.json", %{activities: activities, for: user, as: :activity})
306 def public_timeline(%{assigns: %{user: user}} = conn, params) do
307 local_only = params["local"] in [true, "True", "true", "1"]
311 |> Map.put("type", ["Create", "Announce"])
312 |> Map.put("local_only", local_only)
313 |> Map.put("blocking_user", user)
314 |> Map.put("muting_user", user)
315 |> ActivityPub.fetch_public_activities()
318 user = Repo.preload(user, bookmarks: :activity)
321 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
322 |> put_view(StatusView)
323 |> render("index.json", %{activities: activities, for: user, as: :activity})
326 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
327 with %User{} = user <- User.get_cached_by_id(params["id"]),
328 reading_user <- Repo.preload(reading_user, :bookmarks) do
329 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
332 |> add_link_headers(:user_statuses, activities, params["id"])
333 |> put_view(StatusView)
334 |> render("index.json", %{
335 activities: activities,
342 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
345 |> Map.put("type", "Create")
346 |> Map.put("blocking_user", user)
347 |> Map.put("user", user)
348 |> Map.put(:visibility, "direct")
352 |> ActivityPub.fetch_activities_query(params)
353 |> Pagination.fetch_paginated(params)
355 user = Repo.preload(user, bookmarks: :activity)
358 |> add_link_headers(:dm_timeline, activities)
359 |> put_view(StatusView)
360 |> render("index.json", %{activities: activities, for: user, as: :activity})
363 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
364 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
365 true <- Visibility.visible_for_user?(activity, user) do
366 user = Repo.preload(user, bookmarks: :activity)
369 |> put_view(StatusView)
370 |> try_render("status.json", %{activity: activity, for: user})
374 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
375 with %Activity{} = activity <- Activity.get_by_id(id),
377 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
378 "blocking_user" => user,
382 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
384 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
385 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
391 activities: grouped_activities[true] || [],
395 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
400 activities: grouped_activities[false] || [],
404 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
411 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
412 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
414 |> add_link_headers(:scheduled_statuses, scheduled_activities)
415 |> put_view(ScheduledActivityView)
416 |> render("index.json", %{scheduled_activities: scheduled_activities})
420 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
421 with %ScheduledActivity{} = scheduled_activity <-
422 ScheduledActivity.get(user, scheduled_activity_id) do
424 |> put_view(ScheduledActivityView)
425 |> render("show.json", %{scheduled_activity: scheduled_activity})
427 _ -> {:error, :not_found}
431 def update_scheduled_status(
432 %{assigns: %{user: user}} = conn,
433 %{"id" => scheduled_activity_id} = params
435 with %ScheduledActivity{} = scheduled_activity <-
436 ScheduledActivity.get(user, scheduled_activity_id),
437 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
439 |> put_view(ScheduledActivityView)
440 |> render("show.json", %{scheduled_activity: scheduled_activity})
442 nil -> {:error, :not_found}
447 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
448 with %ScheduledActivity{} = scheduled_activity <-
449 ScheduledActivity.get(user, scheduled_activity_id),
450 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
452 |> put_view(ScheduledActivityView)
453 |> render("show.json", %{scheduled_activity: scheduled_activity})
455 nil -> {:error, :not_found}
460 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
461 when length(media_ids) > 0 do
464 |> Map.put("status", ".")
466 post_status(conn, params)
469 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
472 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
475 case get_req_header(conn, "idempotency-key") do
477 _ -> Ecto.UUID.generate()
480 scheduled_at = params["scheduled_at"]
482 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
483 with {:ok, scheduled_activity} <-
484 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
486 |> put_view(ScheduledActivityView)
487 |> render("show.json", %{scheduled_activity: scheduled_activity})
490 params = Map.drop(params, ["scheduled_at"])
493 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
494 CommonAPI.post(user, params)
498 |> put_view(StatusView)
499 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
503 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
504 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
510 |> json(%{error: "Can't delete this post"})
514 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
515 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
516 %Activity{} = announce <- Activity.normalize(announce.data) do
517 user = Repo.preload(user, bookmarks: :activity)
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
525 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
526 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
527 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
528 user = Repo.preload(user, bookmarks: :activity)
531 |> put_view(StatusView)
532 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
536 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
537 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
538 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
540 |> put_view(StatusView)
541 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
545 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
546 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
547 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
549 |> put_view(StatusView)
550 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
554 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
555 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
557 |> put_view(StatusView)
558 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
562 |> put_resp_content_type("application/json")
563 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
567 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
568 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
570 |> put_view(StatusView)
571 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
575 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
576 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
577 %User{} = user <- User.get_cached_by_nickname(user.nickname),
578 true <- Visibility.visible_for_user?(activity, user),
579 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
580 user = Repo.preload(user, bookmarks: :activity)
583 |> put_view(StatusView)
584 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
588 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
589 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
590 %User{} = user <- User.get_cached_by_nickname(user.nickname),
591 true <- Visibility.visible_for_user?(activity, user),
592 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
593 user = Repo.preload(user, bookmarks: :activity)
596 |> put_view(StatusView)
597 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
601 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
602 activity = Activity.get_by_id(id)
604 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
606 |> put_view(StatusView)
607 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
611 |> put_resp_content_type("application/json")
612 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
616 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
617 activity = Activity.get_by_id(id)
619 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
621 |> put_view(StatusView)
622 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
626 def notifications(%{assigns: %{user: user}} = conn, params) do
627 notifications = MastodonAPI.get_notifications(user, params)
630 |> add_link_headers(:notifications, notifications)
631 |> put_view(NotificationView)
632 |> render("index.json", %{notifications: notifications, for: user})
635 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
636 with {:ok, notification} <- Notification.get(user, id) do
638 |> put_view(NotificationView)
639 |> render("show.json", %{notification: notification, for: user})
643 |> put_resp_content_type("application/json")
644 |> send_resp(403, Jason.encode!(%{"error" => reason}))
648 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
649 Notification.clear(user)
653 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
654 with {:ok, _notif} <- Notification.dismiss(user, id) do
659 |> put_resp_content_type("application/json")
660 |> send_resp(403, Jason.encode!(%{"error" => reason}))
664 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
665 Notification.destroy_multiple(user, ids)
669 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
671 q = from(u in User, where: u.id in ^id)
672 targets = Repo.all(q)
675 |> put_view(AccountView)
676 |> render("relationships.json", %{user: user, targets: targets})
679 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
680 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
682 def update_media(%{assigns: %{user: user}} = conn, data) do
683 with %Object{} = object <- Repo.get(Object, data["id"]),
684 true <- Object.authorize_mutation(object, user),
685 true <- is_binary(data["description"]),
686 description <- data["description"] do
687 new_data = %{object.data | "name" => description}
691 |> Object.change(%{data: new_data})
694 attachment_data = Map.put(new_data, "id", object.id)
697 |> put_view(StatusView)
698 |> render("attachment.json", %{attachment: attachment_data})
702 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
703 with {:ok, object} <-
706 actor: User.ap_id(user),
707 description: Map.get(data, "description")
709 attachment_data = Map.put(object.data, "id", object.id)
712 |> put_view(StatusView)
713 |> render("attachment.json", %{attachment: attachment_data})
717 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
718 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
719 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
720 q = from(u in User, where: u.ap_id in ^likes)
724 |> put_view(AccountView)
725 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
731 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
732 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
733 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
734 q = from(u in User, where: u.ap_id in ^announces)
738 |> put_view(AccountView)
739 |> render("accounts.json", %{for: user, users: users, as: :user})
745 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
746 local_only = params["local"] in [true, "True", "true", "1"]
749 [params["tag"], params["any"]]
753 |> Enum.map(&String.downcase(&1))
758 |> Enum.map(&String.downcase(&1))
763 |> Enum.map(&String.downcase(&1))
767 |> Map.put("type", "Create")
768 |> Map.put("local_only", local_only)
769 |> Map.put("blocking_user", user)
770 |> Map.put("muting_user", user)
771 |> Map.put("tag", tags)
772 |> Map.put("tag_all", tag_all)
773 |> Map.put("tag_reject", tag_reject)
774 |> ActivityPub.fetch_public_activities()
778 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
779 |> put_view(StatusView)
780 |> render("index.json", %{activities: activities, for: user, as: :activity})
783 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
784 with %User{} = user <- User.get_cached_by_id(id),
785 followers <- MastodonAPI.get_followers(user, params) do
788 for_user && user.id == for_user.id -> followers
789 user.info.hide_followers -> []
794 |> add_link_headers(:followers, followers, user)
795 |> put_view(AccountView)
796 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
800 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
801 with %User{} = user <- User.get_cached_by_id(id),
802 followers <- MastodonAPI.get_friends(user, params) do
805 for_user && user.id == for_user.id -> followers
806 user.info.hide_follows -> []
811 |> add_link_headers(:following, followers, user)
812 |> put_view(AccountView)
813 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
817 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
818 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
820 |> put_view(AccountView)
821 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
825 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
826 with %User{} = follower <- User.get_cached_by_id(id),
827 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
829 |> put_view(AccountView)
830 |> render("relationship.json", %{user: followed, target: follower})
834 |> put_resp_content_type("application/json")
835 |> send_resp(403, Jason.encode!(%{"error" => message}))
839 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
840 with %User{} = follower <- User.get_cached_by_id(id),
841 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
843 |> put_view(AccountView)
844 |> render("relationship.json", %{user: followed, target: follower})
848 |> put_resp_content_type("application/json")
849 |> send_resp(403, Jason.encode!(%{"error" => message}))
853 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
854 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
855 {_, true} <- {:followed, follower.id != followed.id},
856 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
858 |> put_view(AccountView)
859 |> render("relationship.json", %{user: follower, target: followed})
866 |> put_resp_content_type("application/json")
867 |> send_resp(403, Jason.encode!(%{"error" => message}))
871 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
872 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
873 {_, true} <- {:followed, follower.id != followed.id},
874 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
876 |> put_view(AccountView)
877 |> render("account.json", %{user: followed, for: follower})
884 |> put_resp_content_type("application/json")
885 |> send_resp(403, Jason.encode!(%{"error" => message}))
889 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
890 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
891 {_, true} <- {:followed, follower.id != followed.id},
892 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
894 |> put_view(AccountView)
895 |> render("relationship.json", %{user: follower, target: followed})
905 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
906 with %User{} = muted <- User.get_cached_by_id(id),
907 {:ok, muter} <- User.mute(muter, muted) do
909 |> put_view(AccountView)
910 |> render("relationship.json", %{user: muter, target: muted})
914 |> put_resp_content_type("application/json")
915 |> send_resp(403, Jason.encode!(%{"error" => message}))
919 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
920 with %User{} = muted <- User.get_cached_by_id(id),
921 {:ok, muter} <- User.unmute(muter, muted) do
923 |> put_view(AccountView)
924 |> render("relationship.json", %{user: muter, target: muted})
928 |> put_resp_content_type("application/json")
929 |> send_resp(403, Jason.encode!(%{"error" => message}))
933 def mutes(%{assigns: %{user: user}} = conn, _) do
934 with muted_accounts <- User.muted_users(user) do
935 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
940 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
941 with %User{} = blocked <- User.get_cached_by_id(id),
942 {:ok, blocker} <- User.block(blocker, blocked),
943 {:ok, _activity} <- ActivityPub.block(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 unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
956 with %User{} = blocked <- User.get_cached_by_id(id),
957 {:ok, blocker} <- User.unblock(blocker, blocked),
958 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
960 |> put_view(AccountView)
961 |> render("relationship.json", %{user: blocker, target: blocked})
965 |> put_resp_content_type("application/json")
966 |> send_resp(403, Jason.encode!(%{"error" => message}))
970 def blocks(%{assigns: %{user: user}} = conn, _) do
971 with blocked_accounts <- User.blocked_users(user) do
972 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
977 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
978 json(conn, info.domain_blocks || [])
981 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
982 User.block_domain(blocker, domain)
986 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
987 User.unblock_domain(blocker, domain)
991 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
992 with %User{} = subscription_target <- User.get_cached_by_id(id),
993 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
995 |> put_view(AccountView)
996 |> render("relationship.json", %{user: user, target: subscription_target})
1000 |> put_resp_content_type("application/json")
1001 |> send_resp(403, Jason.encode!(%{"error" => message}))
1005 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1006 with %User{} = subscription_target <- User.get_cached_by_id(id),
1007 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1009 |> put_view(AccountView)
1010 |> render("relationship.json", %{user: user, target: subscription_target})
1012 {:error, message} ->
1014 |> put_resp_content_type("application/json")
1015 |> send_resp(403, Jason.encode!(%{"error" => message}))
1019 def status_search(user, query) do
1021 if Regex.match?(~r/https?:/, query) do
1022 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1023 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1024 true <- Visibility.visible_for_user?(activity, user) do
1033 [a, o] in Activity.with_preloaded_object(Activity),
1034 where: fragment("?->>'type' = 'Create'", a.data),
1035 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1038 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1043 order_by: [desc: :id]
1046 Repo.all(q) ++ fetched
1049 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1050 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1052 statuses = status_search(user, query)
1054 tags_path = Web.base_url() <> "/tag/"
1060 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1061 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1062 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1065 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1067 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1074 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1075 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1077 statuses = status_search(user, query)
1083 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1084 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1087 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1089 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1096 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1097 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1099 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1104 def favourites(%{assigns: %{user: user}} = conn, params) do
1107 |> Map.put("type", "Create")
1108 |> Map.put("favorited_by", user.ap_id)
1109 |> Map.put("blocking_user", user)
1112 ActivityPub.fetch_activities([], params)
1115 user = Repo.preload(user, bookmarks: :activity)
1118 |> add_link_headers(:favourites, activities)
1119 |> put_view(StatusView)
1120 |> render("index.json", %{activities: activities, for: user, as: :activity})
1123 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1124 with %User{} = user <- User.get_by_id(id),
1125 false <- user.info.hide_favorites do
1128 |> Map.put("type", "Create")
1129 |> Map.put("favorited_by", user.ap_id)
1130 |> Map.put("blocking_user", for_user)
1134 ["https://www.w3.org/ns/activitystreams#Public"] ++
1135 [for_user.ap_id | for_user.following]
1137 ["https://www.w3.org/ns/activitystreams#Public"]
1142 |> ActivityPub.fetch_activities(params)
1146 |> add_link_headers(:favourites, activities)
1147 |> put_view(StatusView)
1148 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1151 {:error, :not_found}
1156 |> json(%{error: "Can't get favorites"})
1160 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1161 user = User.get_cached_by_id(user.id)
1162 user = Repo.preload(user, bookmarks: :activity)
1165 Bookmark.for_user_query(user.id)
1166 |> Pagination.fetch_paginated(params)
1170 |> Enum.map(fn b -> b.activity end)
1173 |> add_link_headers(:bookmarks, bookmarks)
1174 |> put_view(StatusView)
1175 |> render("index.json", %{activities: activities, for: user, as: :activity})
1178 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1179 lists = Pleroma.List.for_user(user, opts)
1180 res = ListView.render("lists.json", lists: lists)
1184 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1185 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1186 res = ListView.render("list.json", list: list)
1192 |> json(%{error: "Record not found"})
1196 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1197 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1198 res = ListView.render("lists.json", lists: lists)
1202 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1203 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1204 {:ok, _list} <- Pleroma.List.delete(list) do
1212 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1213 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1214 res = ListView.render("list.json", list: list)
1219 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1221 |> Enum.each(fn account_id ->
1222 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1223 %User{} = followed <- User.get_cached_by_id(account_id) do
1224 Pleroma.List.follow(list, followed)
1231 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1233 |> Enum.each(fn account_id ->
1234 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1235 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1236 Pleroma.List.unfollow(list, followed)
1243 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1244 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1245 {:ok, users} = Pleroma.List.get_following(list) do
1247 |> put_view(AccountView)
1248 |> render("accounts.json", %{for: user, users: users, as: :user})
1252 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1253 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1254 {:ok, list} <- Pleroma.List.rename(list, title) do
1255 res = ListView.render("list.json", list: list)
1263 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1264 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1267 |> Map.put("type", "Create")
1268 |> Map.put("blocking_user", user)
1269 |> Map.put("muting_user", user)
1271 # we must filter the following list for the user to avoid leaking statuses the user
1272 # does not actually have permission to see (for more info, peruse security issue #270).
1275 |> Enum.filter(fn x -> x in user.following end)
1276 |> ActivityPub.fetch_activities_bounded(following, params)
1279 user = Repo.preload(user, bookmarks: :activity)
1282 |> put_view(StatusView)
1283 |> render("index.json", %{activities: activities, for: user, as: :activity})
1288 |> json(%{error: "Error."})
1292 def index(%{assigns: %{user: user}} = conn, _params) do
1293 token = get_session(conn, :oauth_token)
1296 mastodon_emoji = mastodonized_emoji()
1298 limit = Config.get([:instance, :limit])
1301 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1303 flavour = get_user_flavour(user)
1308 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1309 access_token: token,
1311 domain: Pleroma.Web.Endpoint.host(),
1314 unfollow_modal: false,
1317 auto_play_gif: false,
1318 display_sensitive_media: false,
1319 reduce_motion: false,
1320 max_toot_chars: limit,
1321 mascot: "/images/pleroma-fox-tan-smol.png"
1324 delete_others_notice: present?(user.info.is_moderator),
1325 admin: present?(user.info.is_admin)
1329 default_privacy: user.info.default_scope,
1330 default_sensitive: false,
1331 allow_content_types: Config.get([:instance, :allowed_post_formats])
1333 media_attachments: %{
1334 accept_content_types: [
1350 user.info.settings ||
1380 push_subscription: nil,
1382 custom_emojis: mastodon_emoji,
1388 |> put_layout(false)
1389 |> put_view(MastodonView)
1390 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1393 |> put_session(:return_to, conn.request_path)
1394 |> redirect(to: "/web/login")
1398 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1399 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1401 with changeset <- Ecto.Changeset.change(user),
1402 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1403 {:ok, _user} <- User.update_and_set_cache(changeset) do
1408 |> put_resp_content_type("application/json")
1409 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1413 @supported_flavours ["glitch", "vanilla"]
1415 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1416 when flavour in @supported_flavours do
1417 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1419 with changeset <- Ecto.Changeset.change(user),
1420 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1421 {:ok, user} <- User.update_and_set_cache(changeset),
1422 flavour <- user.info.flavour do
1427 |> put_resp_content_type("application/json")
1428 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1432 def set_flavour(conn, _params) do
1435 |> json(%{error: "Unsupported flavour"})
1438 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1439 json(conn, get_user_flavour(user))
1442 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1446 defp get_user_flavour(_) do
1450 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1451 redirect(conn, to: local_mastodon_root_path(conn))
1454 @doc "Local Mastodon FE login init action"
1455 def login(conn, %{"code" => auth_token}) do
1456 with {:ok, app} <- get_or_make_app(),
1457 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1458 {:ok, token} <- Token.exchange_token(app, auth) do
1460 |> put_session(:oauth_token, token.token)
1461 |> redirect(to: local_mastodon_root_path(conn))
1465 @doc "Local Mastodon FE callback action"
1466 def login(conn, _) do
1467 with {:ok, app} <- get_or_make_app() do
1472 response_type: "code",
1473 client_id: app.client_id,
1475 scope: Enum.join(app.scopes, " ")
1478 redirect(conn, to: path)
1482 defp local_mastodon_root_path(conn) do
1483 case get_session(conn, :return_to) do
1485 mastodon_api_path(conn, :index, ["getting-started"])
1488 delete_session(conn, :return_to)
1493 defp get_or_make_app do
1494 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1495 scopes = ["read", "write", "follow", "push"]
1497 with %App{} = app <- Repo.get_by(App, find_attrs) do
1499 if app.scopes == scopes do
1503 |> Ecto.Changeset.change(%{scopes: scopes})
1511 App.register_changeset(
1513 Map.put(find_attrs, :scopes, scopes)
1520 def logout(conn, _) do
1523 |> redirect(to: "/")
1526 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1527 Logger.debug("Unimplemented, returning unmodified relationship")
1529 with %User{} = target <- User.get_cached_by_id(id) do
1531 |> put_view(AccountView)
1532 |> render("relationship.json", %{user: user, target: target})
1536 def empty_array(conn, _) do
1537 Logger.debug("Unimplemented, returning an empty array")
1541 def empty_object(conn, _) do
1542 Logger.debug("Unimplemented, returning an empty object")
1546 def get_filters(%{assigns: %{user: user}} = conn, _) do
1547 filters = Filter.get_filters(user)
1548 res = FilterView.render("filters.json", filters: filters)
1553 %{assigns: %{user: user}} = conn,
1554 %{"phrase" => phrase, "context" => context} = params
1560 hide: Map.get(params, "irreversible", nil),
1561 whole_word: Map.get(params, "boolean", true)
1565 {:ok, response} = Filter.create(query)
1566 res = FilterView.render("filter.json", filter: response)
1570 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1571 filter = Filter.get(filter_id, user)
1572 res = FilterView.render("filter.json", filter: filter)
1577 %{assigns: %{user: user}} = conn,
1578 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1582 filter_id: filter_id,
1585 hide: Map.get(params, "irreversible", nil),
1586 whole_word: Map.get(params, "boolean", true)
1590 {:ok, response} = Filter.update(query)
1591 res = FilterView.render("filter.json", filter: response)
1595 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1598 filter_id: filter_id
1601 {:ok, _} = Filter.delete(query)
1607 def errors(conn, {:error, %Changeset{} = changeset}) do
1610 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1611 |> Enum.map_join(", ", fn {_k, v} -> v end)
1615 |> json(%{error: error_message})
1618 def errors(conn, {:error, :not_found}) do
1621 |> json(%{error: "Record not found"})
1624 def errors(conn, _) do
1627 |> json("Something went wrong")
1630 def suggestions(%{assigns: %{user: user}} = conn, _) do
1631 suggestions = Config.get(:suggestions)
1633 if Keyword.get(suggestions, :enabled, false) do
1634 api = Keyword.get(suggestions, :third_party_engine, "")
1635 timeout = Keyword.get(suggestions, :timeout, 5000)
1636 limit = Keyword.get(suggestions, :limit, 23)
1638 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1640 user = user.nickname
1644 |> String.replace("{{host}}", host)
1645 |> String.replace("{{user}}", user)
1647 with {:ok, %{status: 200, body: body}} <-
1652 recv_timeout: timeout,
1656 {:ok, data} <- Jason.decode(body) do
1659 |> Enum.slice(0, limit)
1664 case User.get_or_fetch(x["acct"]) do
1665 {:ok, %User{id: id}} -> id
1671 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1674 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1680 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1687 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1688 with %Activity{} = activity <- Activity.get_by_id(status_id),
1689 true <- Visibility.visible_for_user?(activity, user) do
1693 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1703 def reports(%{assigns: %{user: user}} = conn, params) do
1704 case CommonAPI.report(user, params) do
1707 |> put_view(ReportView)
1708 |> try_render("report.json", %{activity: activity})
1712 |> put_status(:bad_request)
1713 |> json(%{error: err})
1717 def conversations(%{assigns: %{user: user}} = conn, params) do
1718 participations = Participation.for_user_with_last_activity_id(user, params)
1721 Enum.map(participations, fn participation ->
1722 ConversationView.render("participation.json", %{participation: participation, user: user})
1726 |> add_link_headers(:conversations, participations)
1727 |> json(conversations)
1730 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1731 with %Participation{} = participation <-
1732 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1733 {:ok, participation} <- Participation.mark_as_read(participation) do
1734 participation_view =
1735 ConversationView.render("participation.json", %{participation: participation, user: user})
1738 |> json(participation_view)
1742 def try_render(conn, target, params)
1743 when is_binary(target) do
1744 res = render(conn, target, params)
1749 |> json(%{error: "Can't display this activity"})
1755 def try_render(conn, _, _) do
1758 |> json(%{error: "Can't display this activity"})
1761 defp present?(nil), do: false
1762 defp present?(false), do: false
1763 defp present?(_), do: true