1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Conversation.Participation
13 alias Pleroma.Formatter
14 alias Pleroma.Notification
16 alias Pleroma.Object.Fetcher
17 alias Pleroma.Pagination
19 alias Pleroma.ScheduledActivity
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
43 alias Pleroma.Web.ControllerHelper
48 @httpoison Application.get_env(:pleroma, :httpoison)
49 @local_mastodon_name "Mastodon-Local"
51 action_fallback(:errors)
53 def create_app(conn, params) do
54 scopes = Scopes.fetch_scopes(params, ["read"])
58 |> Map.drop(["scope", "scopes"])
59 |> Map.put("scopes", scopes)
61 with cs <- App.register_changeset(%App{}, app_attrs),
62 false <- cs.changes[:client_name] == @local_mastodon_name,
63 {:ok, app} <- Repo.insert(cs) do
66 |> render("show.json", %{app: app})
75 value_function \\ fn x -> {:ok, x} end
77 if Map.has_key?(params, params_field) do
78 case value_function.(params[params_field]) do
79 {:ok, new_value} -> Map.put(map, map_field, new_value)
87 def update_credentials(%{assigns: %{user: user}} = conn, params) do
92 |> add_if_present(params, "display_name", :name)
93 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
94 |> add_if_present(params, "avatar", :avatar, fn value ->
95 with %Plug.Upload{} <- value,
96 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
103 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
106 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
110 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
111 |> Enum.reduce(%{}, fn key, acc ->
112 add_if_present(acc, params, to_string(key), key, fn value ->
113 {:ok, ControllerHelper.truthy_param?(value)}
116 |> add_if_present(params, "default_scope", :default_scope)
117 |> add_if_present(params, "header", :banner, fn value ->
118 with %Plug.Upload{} <- value,
119 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
125 |> Map.put(:emoji, user_info_emojis)
127 info_cng = User.Info.profile_update(user.info, info_params)
129 with changeset <- User.update_changeset(user, user_params),
130 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
131 {:ok, user} <- User.update_and_set_cache(changeset) do
132 if original_user != user do
133 CommonAPI.update(user)
136 json(conn, AccountView.render("account.json", %{user: user, for: user}))
141 |> json(%{error: "Invalid request"})
145 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
146 account = AccountView.render("account.json", %{user: user, for: user})
150 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
151 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
154 |> render("short.json", %{app: app})
158 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
159 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
160 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
161 account = AccountView.render("account.json", %{user: user, for: for_user})
167 |> json(%{error: "Can't find user"})
171 @mastodon_api_level "2.6.5"
173 def masto_instance(conn, _params) do
174 instance = Config.get(:instance)
178 title: Keyword.get(instance, :name),
179 description: Keyword.get(instance, :description),
180 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
181 email: Keyword.get(instance, :email),
183 streaming_api: Pleroma.Web.Endpoint.websocket_url()
185 stats: Stats.get_stats(),
186 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
188 registrations: Pleroma.Config.get([:instance, :registrations_open]),
189 # Extra (not present in Mastodon):
190 max_toot_chars: Keyword.get(instance, :limit)
196 def peers(conn, _params) do
197 json(conn, Stats.get_peers())
200 defp mastodonized_emoji do
201 Pleroma.Emoji.get_all()
202 |> Enum.map(fn {shortcode, relative_url, tags} ->
203 url = to_string(URI.merge(Web.base_url(), relative_url))
206 "shortcode" => shortcode,
208 "visible_in_picker" => true,
215 def custom_emojis(conn, _params) do
216 mastodon_emoji = mastodonized_emoji()
217 json(conn, mastodon_emoji)
220 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
223 |> Map.drop(["since_id", "max_id", "min_id"])
226 last = List.last(activities)
233 |> Map.get("limit", "20")
234 |> String.to_integer()
237 if length(activities) <= limit do
243 |> Enum.at(limit * -1)
247 {next_url, prev_url} =
251 Pleroma.Web.Endpoint,
254 Map.merge(params, %{max_id: max_id})
257 Pleroma.Web.Endpoint,
260 Map.merge(params, %{min_id: min_id})
266 Pleroma.Web.Endpoint,
268 Map.merge(params, %{max_id: max_id})
271 Pleroma.Web.Endpoint,
273 Map.merge(params, %{min_id: min_id})
279 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
285 def home_timeline(%{assigns: %{user: user}} = conn, params) do
288 |> Map.put("type", ["Create", "Announce"])
289 |> Map.put("blocking_user", user)
290 |> Map.put("muting_user", user)
291 |> Map.put("user", user)
294 [user.ap_id | user.following]
295 |> ActivityPub.fetch_activities(params)
296 |> ActivityPub.contain_timeline(user)
300 |> add_link_headers(:home_timeline, activities)
301 |> put_view(StatusView)
302 |> render("index.json", %{activities: activities, for: user, as: :activity})
305 def public_timeline(%{assigns: %{user: user}} = conn, params) do
306 local_only = params["local"] in [true, "True", "true", "1"]
310 |> Map.put("type", ["Create", "Announce"])
311 |> Map.put("local_only", local_only)
312 |> Map.put("blocking_user", user)
313 |> Map.put("muting_user", user)
314 |> ActivityPub.fetch_public_activities()
318 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
319 |> put_view(StatusView)
320 |> render("index.json", %{activities: activities, for: user, as: :activity})
323 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
324 with %User{} = user <- User.get_cached_by_id(params["id"]) do
325 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
328 |> add_link_headers(:user_statuses, activities, params["id"])
329 |> put_view(StatusView)
330 |> render("index.json", %{
331 activities: activities,
338 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
341 |> Map.put("type", "Create")
342 |> Map.put("blocking_user", user)
343 |> Map.put("user", user)
344 |> Map.put(:visibility, "direct")
348 |> ActivityPub.fetch_activities_query(params)
349 |> Pagination.fetch_paginated(params)
352 |> add_link_headers(:dm_timeline, activities)
353 |> put_view(StatusView)
354 |> render("index.json", %{activities: activities, for: user, as: :activity})
357 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
358 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
359 true <- Visibility.visible_for_user?(activity, user) do
361 |> put_view(StatusView)
362 |> try_render("status.json", %{activity: activity, for: user})
366 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
367 with %Activity{} = activity <- Activity.get_by_id(id),
369 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
370 "blocking_user" => user,
374 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
376 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
377 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
383 activities: grouped_activities[true] || [],
387 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
392 activities: grouped_activities[false] || [],
396 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
403 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
404 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
406 |> add_link_headers(:scheduled_statuses, scheduled_activities)
407 |> put_view(ScheduledActivityView)
408 |> render("index.json", %{scheduled_activities: scheduled_activities})
412 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
413 with %ScheduledActivity{} = scheduled_activity <-
414 ScheduledActivity.get(user, scheduled_activity_id) do
416 |> put_view(ScheduledActivityView)
417 |> render("show.json", %{scheduled_activity: scheduled_activity})
419 _ -> {:error, :not_found}
423 def update_scheduled_status(
424 %{assigns: %{user: user}} = conn,
425 %{"id" => scheduled_activity_id} = params
427 with %ScheduledActivity{} = scheduled_activity <-
428 ScheduledActivity.get(user, scheduled_activity_id),
429 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
431 |> put_view(ScheduledActivityView)
432 |> render("show.json", %{scheduled_activity: scheduled_activity})
434 nil -> {:error, :not_found}
439 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
440 with %ScheduledActivity{} = scheduled_activity <-
441 ScheduledActivity.get(user, scheduled_activity_id),
442 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
444 |> put_view(ScheduledActivityView)
445 |> render("show.json", %{scheduled_activity: scheduled_activity})
447 nil -> {:error, :not_found}
452 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
453 when length(media_ids) > 0 do
456 |> Map.put("status", ".")
458 post_status(conn, params)
461 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
464 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
467 case get_req_header(conn, "idempotency-key") do
469 _ -> Ecto.UUID.generate()
472 scheduled_at = params["scheduled_at"]
474 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
475 with {:ok, scheduled_activity} <-
476 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
478 |> put_view(ScheduledActivityView)
479 |> render("show.json", %{scheduled_activity: scheduled_activity})
482 params = Map.drop(params, ["scheduled_at"])
485 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
486 CommonAPI.post(user, params)
490 |> put_view(StatusView)
491 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
495 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
496 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
502 |> json(%{error: "Can't delete this post"})
506 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
507 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
508 %Activity{} = announce <- Activity.normalize(announce.data) do
510 |> put_view(StatusView)
511 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
515 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
524 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
526 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
528 |> put_view(StatusView)
529 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
533 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
534 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
535 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
537 |> put_view(StatusView)
538 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
542 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
543 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
545 |> put_view(StatusView)
546 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
550 |> put_resp_content_type("application/json")
551 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
555 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
556 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
558 |> put_view(StatusView)
559 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
563 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
564 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
565 %User{} = user <- User.get_cached_by_nickname(user.nickname),
566 true <- Visibility.visible_for_user?(activity, user),
567 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
569 |> put_view(StatusView)
570 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
574 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
575 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
576 %User{} = user <- User.get_cached_by_nickname(user.nickname),
577 true <- Visibility.visible_for_user?(activity, user),
578 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
580 |> put_view(StatusView)
581 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
585 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
586 activity = Activity.get_by_id(id)
588 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
590 |> put_view(StatusView)
591 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
595 |> put_resp_content_type("application/json")
596 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
600 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
601 activity = Activity.get_by_id(id)
603 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
605 |> put_view(StatusView)
606 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
610 def notifications(%{assigns: %{user: user}} = conn, params) do
611 notifications = MastodonAPI.get_notifications(user, params)
614 |> add_link_headers(:notifications, notifications)
615 |> put_view(NotificationView)
616 |> render("index.json", %{notifications: notifications, for: user})
619 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
620 with {:ok, notification} <- Notification.get(user, id) do
622 |> put_view(NotificationView)
623 |> render("show.json", %{notification: notification, for: user})
627 |> put_resp_content_type("application/json")
628 |> send_resp(403, Jason.encode!(%{"error" => reason}))
632 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
633 Notification.clear(user)
637 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
638 with {:ok, _notif} <- Notification.dismiss(user, id) do
643 |> put_resp_content_type("application/json")
644 |> send_resp(403, Jason.encode!(%{"error" => reason}))
648 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
649 Notification.destroy_multiple(user, ids)
653 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
655 q = from(u in User, where: u.id in ^id)
656 targets = Repo.all(q)
659 |> put_view(AccountView)
660 |> render("relationships.json", %{user: user, targets: targets})
663 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
664 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
666 def update_media(%{assigns: %{user: user}} = conn, data) do
667 with %Object{} = object <- Repo.get(Object, data["id"]),
668 true <- Object.authorize_mutation(object, user),
669 true <- is_binary(data["description"]),
670 description <- data["description"] do
671 new_data = %{object.data | "name" => description}
675 |> Object.change(%{data: new_data})
678 attachment_data = Map.put(new_data, "id", object.id)
681 |> put_view(StatusView)
682 |> render("attachment.json", %{attachment: attachment_data})
686 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
687 with {:ok, object} <-
690 actor: User.ap_id(user),
691 description: Map.get(data, "description")
693 attachment_data = Map.put(object.data, "id", object.id)
696 |> put_view(StatusView)
697 |> render("attachment.json", %{attachment: attachment_data})
701 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
702 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
703 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
704 q = from(u in User, where: u.ap_id in ^likes)
708 |> put_view(AccountView)
709 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
715 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
716 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
717 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
718 q = from(u in User, where: u.ap_id in ^announces)
722 |> put_view(AccountView)
723 |> render("accounts.json", %{for: user, users: users, as: :user})
729 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
730 local_only = params["local"] in [true, "True", "true", "1"]
733 [params["tag"], params["any"]]
737 |> Enum.map(&String.downcase(&1))
742 |> Enum.map(&String.downcase(&1))
747 |> Enum.map(&String.downcase(&1))
751 |> Map.put("type", "Create")
752 |> Map.put("local_only", local_only)
753 |> Map.put("blocking_user", user)
754 |> Map.put("muting_user", user)
755 |> Map.put("tag", tags)
756 |> Map.put("tag_all", tag_all)
757 |> Map.put("tag_reject", tag_reject)
758 |> ActivityPub.fetch_public_activities()
762 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
763 |> put_view(StatusView)
764 |> render("index.json", %{activities: activities, for: user, as: :activity})
767 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
768 with %User{} = user <- User.get_cached_by_id(id),
769 followers <- MastodonAPI.get_followers(user, params) do
772 for_user && user.id == for_user.id -> followers
773 user.info.hide_followers -> []
778 |> add_link_headers(:followers, followers, user)
779 |> put_view(AccountView)
780 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
784 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
785 with %User{} = user <- User.get_cached_by_id(id),
786 followers <- MastodonAPI.get_friends(user, params) do
789 for_user && user.id == for_user.id -> followers
790 user.info.hide_follows -> []
795 |> add_link_headers(:following, followers, user)
796 |> put_view(AccountView)
797 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
801 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
802 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
804 |> put_view(AccountView)
805 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
809 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
810 with %User{} = follower <- User.get_cached_by_id(id),
811 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
813 |> put_view(AccountView)
814 |> render("relationship.json", %{user: followed, target: follower})
818 |> put_resp_content_type("application/json")
819 |> send_resp(403, Jason.encode!(%{"error" => message}))
823 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
824 with %User{} = follower <- User.get_cached_by_id(id),
825 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
827 |> put_view(AccountView)
828 |> render("relationship.json", %{user: followed, target: follower})
832 |> put_resp_content_type("application/json")
833 |> send_resp(403, Jason.encode!(%{"error" => message}))
837 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
838 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
839 {_, true} <- {:followed, follower.id != followed.id},
840 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
842 |> put_view(AccountView)
843 |> render("relationship.json", %{user: follower, target: followed})
850 |> put_resp_content_type("application/json")
851 |> send_resp(403, Jason.encode!(%{"error" => message}))
855 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
856 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
857 {_, true} <- {:followed, follower.id != followed.id},
858 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
860 |> put_view(AccountView)
861 |> render("account.json", %{user: followed, for: follower})
868 |> put_resp_content_type("application/json")
869 |> send_resp(403, Jason.encode!(%{"error" => message}))
873 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
874 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
875 {_, true} <- {:followed, follower.id != followed.id},
876 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
878 |> put_view(AccountView)
879 |> render("relationship.json", %{user: follower, target: followed})
889 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
890 with %User{} = muted <- User.get_cached_by_id(id),
891 {:ok, muter} <- User.mute(muter, muted) do
893 |> put_view(AccountView)
894 |> render("relationship.json", %{user: muter, target: muted})
898 |> put_resp_content_type("application/json")
899 |> send_resp(403, Jason.encode!(%{"error" => message}))
903 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
904 with %User{} = muted <- User.get_cached_by_id(id),
905 {:ok, muter} <- User.unmute(muter, muted) do
907 |> put_view(AccountView)
908 |> render("relationship.json", %{user: muter, target: muted})
912 |> put_resp_content_type("application/json")
913 |> send_resp(403, Jason.encode!(%{"error" => message}))
917 def mutes(%{assigns: %{user: user}} = conn, _) do
918 with muted_accounts <- User.muted_users(user) do
919 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
924 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
925 with %User{} = blocked <- User.get_cached_by_id(id),
926 {:ok, blocker} <- User.block(blocker, blocked),
927 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
929 |> put_view(AccountView)
930 |> render("relationship.json", %{user: blocker, target: blocked})
934 |> put_resp_content_type("application/json")
935 |> send_resp(403, Jason.encode!(%{"error" => message}))
939 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
940 with %User{} = blocked <- User.get_cached_by_id(id),
941 {:ok, blocker} <- User.unblock(blocker, blocked),
942 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
944 |> put_view(AccountView)
945 |> render("relationship.json", %{user: blocker, target: blocked})
949 |> put_resp_content_type("application/json")
950 |> send_resp(403, Jason.encode!(%{"error" => message}))
954 def blocks(%{assigns: %{user: user}} = conn, _) do
955 with blocked_accounts <- User.blocked_users(user) do
956 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
961 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
962 json(conn, info.domain_blocks || [])
965 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
966 User.block_domain(blocker, domain)
970 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
971 User.unblock_domain(blocker, domain)
975 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
976 with %User{} = subscription_target <- User.get_cached_by_id(id),
977 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
979 |> put_view(AccountView)
980 |> render("relationship.json", %{user: user, target: subscription_target})
984 |> put_resp_content_type("application/json")
985 |> send_resp(403, Jason.encode!(%{"error" => message}))
989 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
990 with %User{} = subscription_target <- User.get_cached_by_id(id),
991 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
993 |> put_view(AccountView)
994 |> render("relationship.json", %{user: user, target: subscription_target})
998 |> put_resp_content_type("application/json")
999 |> send_resp(403, Jason.encode!(%{"error" => message}))
1003 def status_search(user, query) do
1005 if Regex.match?(~r/https?:/, query) do
1006 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1007 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1008 true <- Visibility.visible_for_user?(activity, user) do
1017 [a, o] in Activity.with_preloaded_object(Activity),
1018 where: fragment("?->>'type' = 'Create'", a.data),
1019 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1022 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1027 order_by: [desc: :id]
1030 Repo.all(q) ++ fetched
1033 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1034 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1036 statuses = status_search(user, query)
1038 tags_path = Web.base_url() <> "/tag/"
1044 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1045 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1046 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1049 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1051 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1058 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1059 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1061 statuses = status_search(user, query)
1067 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1068 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1071 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1073 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1080 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1081 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1083 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1088 def favourites(%{assigns: %{user: user}} = conn, params) do
1091 |> Map.put("type", "Create")
1092 |> Map.put("favorited_by", user.ap_id)
1093 |> Map.put("blocking_user", user)
1096 ActivityPub.fetch_activities([], params)
1100 |> add_link_headers(:favourites, activities)
1101 |> put_view(StatusView)
1102 |> render("index.json", %{activities: activities, for: user, as: :activity})
1105 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1106 with %User{} = user <- User.get_by_id(id),
1107 false <- user.info.hide_favorites do
1110 |> Map.put("type", "Create")
1111 |> Map.put("favorited_by", user.ap_id)
1112 |> Map.put("blocking_user", for_user)
1116 ["https://www.w3.org/ns/activitystreams#Public"] ++
1117 [for_user.ap_id | for_user.following]
1119 ["https://www.w3.org/ns/activitystreams#Public"]
1124 |> ActivityPub.fetch_activities(params)
1128 |> add_link_headers(:favourites, activities)
1129 |> put_view(StatusView)
1130 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1133 {:error, :not_found}
1138 |> json(%{error: "Can't get favorites"})
1142 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1143 user = User.get_cached_by_id(user.id)
1146 Bookmark.for_user_query(user.id)
1147 |> Pagination.fetch_paginated(params)
1151 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1154 |> add_link_headers(:bookmarks, bookmarks)
1155 |> put_view(StatusView)
1156 |> render("index.json", %{activities: activities, for: user, as: :activity})
1159 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1160 lists = Pleroma.List.for_user(user, opts)
1161 res = ListView.render("lists.json", lists: lists)
1165 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1166 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1167 res = ListView.render("list.json", list: list)
1173 |> json(%{error: "Record not found"})
1177 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1178 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1179 res = ListView.render("lists.json", lists: lists)
1183 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1184 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1185 {:ok, _list} <- Pleroma.List.delete(list) do
1193 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1194 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1195 res = ListView.render("list.json", list: list)
1200 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1202 |> Enum.each(fn account_id ->
1203 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1204 %User{} = followed <- User.get_cached_by_id(account_id) do
1205 Pleroma.List.follow(list, followed)
1212 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1214 |> Enum.each(fn account_id ->
1215 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1216 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1217 Pleroma.List.unfollow(list, followed)
1224 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1225 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1226 {:ok, users} = Pleroma.List.get_following(list) do
1228 |> put_view(AccountView)
1229 |> render("accounts.json", %{for: user, users: users, as: :user})
1233 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1234 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1235 {:ok, list} <- Pleroma.List.rename(list, title) do
1236 res = ListView.render("list.json", list: list)
1244 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1245 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1248 |> Map.put("type", "Create")
1249 |> Map.put("blocking_user", user)
1250 |> Map.put("muting_user", user)
1252 # we must filter the following list for the user to avoid leaking statuses the user
1253 # does not actually have permission to see (for more info, peruse security issue #270).
1256 |> Enum.filter(fn x -> x in user.following end)
1257 |> ActivityPub.fetch_activities_bounded(following, params)
1261 |> put_view(StatusView)
1262 |> render("index.json", %{activities: activities, for: user, as: :activity})
1267 |> json(%{error: "Error."})
1271 def index(%{assigns: %{user: user}} = conn, _params) do
1272 token = get_session(conn, :oauth_token)
1275 mastodon_emoji = mastodonized_emoji()
1277 limit = Config.get([:instance, :limit])
1280 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1282 flavour = get_user_flavour(user)
1287 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1288 access_token: token,
1290 domain: Pleroma.Web.Endpoint.host(),
1293 unfollow_modal: false,
1296 auto_play_gif: false,
1297 display_sensitive_media: false,
1298 reduce_motion: false,
1299 max_toot_chars: limit,
1300 mascot: "/images/pleroma-fox-tan-smol.png"
1303 delete_others_notice: present?(user.info.is_moderator),
1304 admin: present?(user.info.is_admin)
1308 default_privacy: user.info.default_scope,
1309 default_sensitive: false,
1310 allow_content_types: Config.get([:instance, :allowed_post_formats])
1312 media_attachments: %{
1313 accept_content_types: [
1329 user.info.settings ||
1359 push_subscription: nil,
1361 custom_emojis: mastodon_emoji,
1367 |> put_layout(false)
1368 |> put_view(MastodonView)
1369 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1372 |> put_session(:return_to, conn.request_path)
1373 |> redirect(to: "/web/login")
1377 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1378 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1380 with changeset <- Ecto.Changeset.change(user),
1381 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1382 {:ok, _user} <- User.update_and_set_cache(changeset) do
1387 |> put_resp_content_type("application/json")
1388 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1392 @supported_flavours ["glitch", "vanilla"]
1394 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1395 when flavour in @supported_flavours do
1396 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1398 with changeset <- Ecto.Changeset.change(user),
1399 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1400 {:ok, user} <- User.update_and_set_cache(changeset),
1401 flavour <- user.info.flavour do
1406 |> put_resp_content_type("application/json")
1407 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1411 def set_flavour(conn, _params) do
1414 |> json(%{error: "Unsupported flavour"})
1417 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1418 json(conn, get_user_flavour(user))
1421 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1425 defp get_user_flavour(_) do
1429 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1430 redirect(conn, to: local_mastodon_root_path(conn))
1433 @doc "Local Mastodon FE login init action"
1434 def login(conn, %{"code" => auth_token}) do
1435 with {:ok, app} <- get_or_make_app(),
1436 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1437 {:ok, token} <- Token.exchange_token(app, auth) do
1439 |> put_session(:oauth_token, token.token)
1440 |> redirect(to: local_mastodon_root_path(conn))
1444 @doc "Local Mastodon FE callback action"
1445 def login(conn, _) do
1446 with {:ok, app} <- get_or_make_app() do
1451 response_type: "code",
1452 client_id: app.client_id,
1454 scope: Enum.join(app.scopes, " ")
1457 redirect(conn, to: path)
1461 defp local_mastodon_root_path(conn) do
1462 case get_session(conn, :return_to) do
1464 mastodon_api_path(conn, :index, ["getting-started"])
1467 delete_session(conn, :return_to)
1472 defp get_or_make_app do
1473 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1474 scopes = ["read", "write", "follow", "push"]
1476 with %App{} = app <- Repo.get_by(App, find_attrs) do
1478 if app.scopes == scopes do
1482 |> Ecto.Changeset.change(%{scopes: scopes})
1490 App.register_changeset(
1492 Map.put(find_attrs, :scopes, scopes)
1499 def logout(conn, _) do
1502 |> redirect(to: "/")
1505 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1506 Logger.debug("Unimplemented, returning unmodified relationship")
1508 with %User{} = target <- User.get_cached_by_id(id) do
1510 |> put_view(AccountView)
1511 |> render("relationship.json", %{user: user, target: target})
1515 def empty_array(conn, _) do
1516 Logger.debug("Unimplemented, returning an empty array")
1520 def empty_object(conn, _) do
1521 Logger.debug("Unimplemented, returning an empty object")
1525 def get_filters(%{assigns: %{user: user}} = conn, _) do
1526 filters = Filter.get_filters(user)
1527 res = FilterView.render("filters.json", filters: filters)
1532 %{assigns: %{user: user}} = conn,
1533 %{"phrase" => phrase, "context" => context} = params
1539 hide: Map.get(params, "irreversible", false),
1540 whole_word: Map.get(params, "boolean", true)
1544 {:ok, response} = Filter.create(query)
1545 res = FilterView.render("filter.json", filter: response)
1549 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1550 filter = Filter.get(filter_id, user)
1551 res = FilterView.render("filter.json", filter: filter)
1556 %{assigns: %{user: user}} = conn,
1557 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1561 filter_id: filter_id,
1564 hide: Map.get(params, "irreversible", nil),
1565 whole_word: Map.get(params, "boolean", true)
1569 {:ok, response} = Filter.update(query)
1570 res = FilterView.render("filter.json", filter: response)
1574 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1577 filter_id: filter_id
1580 {:ok, _} = Filter.delete(query)
1586 def errors(conn, {:error, %Changeset{} = changeset}) do
1589 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1590 |> Enum.map_join(", ", fn {_k, v} -> v end)
1594 |> json(%{error: error_message})
1597 def errors(conn, {:error, :not_found}) do
1600 |> json(%{error: "Record not found"})
1603 def errors(conn, _) do
1606 |> json("Something went wrong")
1609 def suggestions(%{assigns: %{user: user}} = conn, _) do
1610 suggestions = Config.get(:suggestions)
1612 if Keyword.get(suggestions, :enabled, false) do
1613 api = Keyword.get(suggestions, :third_party_engine, "")
1614 timeout = Keyword.get(suggestions, :timeout, 5000)
1615 limit = Keyword.get(suggestions, :limit, 23)
1617 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1619 user = user.nickname
1623 |> String.replace("{{host}}", host)
1624 |> String.replace("{{user}}", user)
1626 with {:ok, %{status: 200, body: body}} <-
1631 recv_timeout: timeout,
1635 {:ok, data} <- Jason.decode(body) do
1638 |> Enum.slice(0, limit)
1643 case User.get_or_fetch(x["acct"]) do
1644 {:ok, %User{id: id}} -> id
1650 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1653 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1659 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1666 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1667 with %Activity{} = activity <- Activity.get_by_id(status_id),
1668 true <- Visibility.visible_for_user?(activity, user) do
1672 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1682 def reports(%{assigns: %{user: user}} = conn, params) do
1683 case CommonAPI.report(user, params) do
1686 |> put_view(ReportView)
1687 |> try_render("report.json", %{activity: activity})
1691 |> put_status(:bad_request)
1692 |> json(%{error: err})
1696 def conversations(%{assigns: %{user: user}} = conn, params) do
1697 participations = Participation.for_user_with_last_activity_id(user, params)
1700 Enum.map(participations, fn participation ->
1701 ConversationView.render("participation.json", %{participation: participation, user: user})
1705 |> add_link_headers(:conversations, participations)
1706 |> json(conversations)
1709 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1710 with %Participation{} = participation <-
1711 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1712 {:ok, participation} <- Participation.mark_as_read(participation) do
1713 participation_view =
1714 ConversationView.render("participation.json", %{participation: participation, user: user})
1717 |> json(participation_view)
1721 def try_render(conn, target, params)
1722 when is_binary(target) do
1723 res = render(conn, target, params)
1728 |> json(%{error: "Can't display this activity"})
1734 def try_render(conn, _, _) do
1737 |> json(%{error: "Can't display this activity"})
1740 defp present?(nil), do: false
1741 defp present?(false), do: false
1742 defp present?(_), do: true