1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
12 alias Pleroma.Formatter
13 alias Pleroma.Notification
15 alias Pleroma.Object.Fetcher
16 alias Pleroma.Pagination
18 alias Pleroma.ScheduledActivity
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Visibility
24 alias Pleroma.Web.CommonAPI
25 alias Pleroma.Web.MastodonAPI.AccountView
26 alias Pleroma.Web.MastodonAPI.AppView
27 alias Pleroma.Web.MastodonAPI.FilterView
28 alias Pleroma.Web.MastodonAPI.ListView
29 alias Pleroma.Web.MastodonAPI.MastodonAPI
30 alias Pleroma.Web.MastodonAPI.MastodonView
31 alias Pleroma.Web.MastodonAPI.NotificationView
32 alias Pleroma.Web.MastodonAPI.ReportView
33 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
34 alias Pleroma.Web.MastodonAPI.StatusView
35 alias Pleroma.Web.MediaProxy
36 alias Pleroma.Web.OAuth.App
37 alias Pleroma.Web.OAuth.Authorization
38 alias Pleroma.Web.OAuth.Token
40 alias Pleroma.Web.ControllerHelper
45 @httpoison Application.get_env(:pleroma, :httpoison)
46 @local_mastodon_name "Mastodon-Local"
48 action_fallback(:errors)
50 def create_app(conn, params) do
51 scopes = ControllerHelper.oauth_scopes(params, ["read"])
55 |> Map.drop(["scope", "scopes"])
56 |> Map.put("scopes", scopes)
58 with cs <- App.register_changeset(%App{}, app_attrs),
59 false <- cs.changes[:client_name] == @local_mastodon_name,
60 {:ok, app} <- Repo.insert(cs) do
63 |> render("show.json", %{app: app})
72 value_function \\ fn x -> {:ok, x} end
74 if Map.has_key?(params, params_field) do
75 case value_function.(params[params_field]) do
76 {:ok, new_value} -> Map.put(map, map_field, new_value)
84 def update_credentials(%{assigns: %{user: user}} = conn, params) do
89 |> add_if_present(params, "display_name", :name)
90 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
91 |> add_if_present(params, "avatar", :avatar, fn value ->
92 with %Plug.Upload{} <- value,
93 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
100 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
103 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
107 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
108 |> Enum.reduce(%{}, fn key, acc ->
109 add_if_present(acc, params, to_string(key), key, fn value ->
110 {:ok, ControllerHelper.truthy_param?(value)}
113 |> add_if_present(params, "default_scope", :default_scope)
114 |> add_if_present(params, "header", :banner, fn value ->
115 with %Plug.Upload{} <- value,
116 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
122 |> Map.put(:emoji, user_info_emojis)
124 info_cng = User.Info.profile_update(user.info, info_params)
126 with changeset <- User.update_changeset(user, user_params),
127 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
128 {:ok, user} <- User.update_and_set_cache(changeset) do
129 if original_user != user do
130 CommonAPI.update(user)
133 json(conn, AccountView.render("account.json", %{user: user, for: user}))
138 |> json(%{error: "Invalid request"})
142 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
143 account = AccountView.render("account.json", %{user: user, for: user})
147 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
148 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
151 |> render("short.json", %{app: app})
155 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
156 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
157 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
158 account = AccountView.render("account.json", %{user: user, for: for_user})
164 |> json(%{error: "Can't find user"})
168 @mastodon_api_level "2.5.0"
170 def masto_instance(conn, _params) do
171 instance = Config.get(:instance)
175 title: Keyword.get(instance, :name),
176 description: Keyword.get(instance, :description),
177 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
178 email: Keyword.get(instance, :email),
180 streaming_api: Pleroma.Web.Endpoint.websocket_url()
182 stats: Stats.get_stats(),
183 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
185 registrations: Pleroma.Config.get([:instance, :registrations_open]),
186 # Extra (not present in Mastodon):
187 max_toot_chars: Keyword.get(instance, :limit)
193 def peers(conn, _params) do
194 json(conn, Stats.get_peers())
197 defp mastodonized_emoji do
198 Pleroma.Emoji.get_all()
199 |> Enum.map(fn {shortcode, relative_url, tags} ->
200 url = to_string(URI.merge(Web.base_url(), relative_url))
203 "shortcode" => shortcode,
205 "visible_in_picker" => true,
212 def custom_emojis(conn, _params) do
213 mastodon_emoji = mastodonized_emoji()
214 json(conn, mastodon_emoji)
217 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
220 |> Map.drop(["since_id", "max_id", "min_id"])
223 last = List.last(activities)
230 |> Map.get("limit", "20")
231 |> String.to_integer()
234 if length(activities) <= limit do
240 |> Enum.at(limit * -1)
244 {next_url, prev_url} =
248 Pleroma.Web.Endpoint,
251 Map.merge(params, %{max_id: max_id})
254 Pleroma.Web.Endpoint,
257 Map.merge(params, %{min_id: min_id})
263 Pleroma.Web.Endpoint,
265 Map.merge(params, %{max_id: max_id})
268 Pleroma.Web.Endpoint,
270 Map.merge(params, %{min_id: min_id})
276 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
282 def home_timeline(%{assigns: %{user: user}} = conn, params) do
285 |> Map.put("type", ["Create", "Announce"])
286 |> Map.put("blocking_user", user)
287 |> Map.put("muting_user", user)
288 |> Map.put("user", user)
291 [user.ap_id | user.following]
292 |> ActivityPub.fetch_activities(params)
293 |> ActivityPub.contain_timeline(user)
296 user = Repo.preload(user, bookmarks: :activity)
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()
316 user = Repo.preload(user, bookmarks: :activity)
319 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
320 |> put_view(StatusView)
321 |> render("index.json", %{activities: activities, for: user, as: :activity})
324 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
325 with %User{} = user <- User.get_cached_by_id(params["id"]),
326 reading_user <- Repo.preload(reading_user, :bookmarks) do
327 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
330 |> add_link_headers(:user_statuses, activities, params["id"])
331 |> put_view(StatusView)
332 |> render("index.json", %{
333 activities: activities,
340 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
343 |> Map.put("type", "Create")
344 |> Map.put("blocking_user", user)
345 |> Map.put("user", user)
346 |> Map.put(:visibility, "direct")
350 |> ActivityPub.fetch_activities_query(params)
351 |> Pagination.fetch_paginated(params)
353 user = Repo.preload(user, bookmarks: :activity)
356 |> add_link_headers(:dm_timeline, activities)
357 |> put_view(StatusView)
358 |> render("index.json", %{activities: activities, for: user, as: :activity})
361 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
362 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
363 true <- Visibility.visible_for_user?(activity, user) do
364 user = Repo.preload(user, bookmarks: :activity)
367 |> put_view(StatusView)
368 |> try_render("status.json", %{activity: activity, for: user})
372 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
373 with %Activity{} = activity <- Activity.get_by_id(id),
375 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
376 "blocking_user" => user,
380 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
382 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
383 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
389 activities: grouped_activities[true] || [],
393 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
398 activities: grouped_activities[false] || [],
402 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
409 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
410 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
412 |> add_link_headers(:scheduled_statuses, scheduled_activities)
413 |> put_view(ScheduledActivityView)
414 |> render("index.json", %{scheduled_activities: scheduled_activities})
418 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
419 with %ScheduledActivity{} = scheduled_activity <-
420 ScheduledActivity.get(user, scheduled_activity_id) do
422 |> put_view(ScheduledActivityView)
423 |> render("show.json", %{scheduled_activity: scheduled_activity})
425 _ -> {:error, :not_found}
429 def update_scheduled_status(
430 %{assigns: %{user: user}} = conn,
431 %{"id" => scheduled_activity_id} = params
433 with %ScheduledActivity{} = scheduled_activity <-
434 ScheduledActivity.get(user, scheduled_activity_id),
435 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
437 |> put_view(ScheduledActivityView)
438 |> render("show.json", %{scheduled_activity: scheduled_activity})
440 nil -> {:error, :not_found}
445 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
446 with %ScheduledActivity{} = scheduled_activity <-
447 ScheduledActivity.get(user, scheduled_activity_id),
448 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
450 |> put_view(ScheduledActivityView)
451 |> render("show.json", %{scheduled_activity: scheduled_activity})
453 nil -> {:error, :not_found}
458 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
459 when length(media_ids) > 0 do
462 |> Map.put("status", ".")
464 post_status(conn, params)
467 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
470 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
473 case get_req_header(conn, "idempotency-key") do
475 _ -> Ecto.UUID.generate()
478 scheduled_at = params["scheduled_at"]
480 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
481 with {:ok, scheduled_activity} <-
482 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
484 |> put_view(ScheduledActivityView)
485 |> render("show.json", %{scheduled_activity: scheduled_activity})
488 params = Map.drop(params, ["scheduled_at"])
491 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
492 CommonAPI.post(user, params)
496 |> put_view(StatusView)
497 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
501 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
502 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
508 |> json(%{error: "Can't delete this post"})
512 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
513 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
514 %Activity{} = announce <- Activity.normalize(announce.data) do
515 user = Repo.preload(user, bookmarks: :activity)
518 |> put_view(StatusView)
519 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
523 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
524 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
525 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
526 user = Repo.preload(user, bookmarks: :activity)
529 |> put_view(StatusView)
530 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
534 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
535 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
536 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
538 |> put_view(StatusView)
539 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
543 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
544 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
545 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
547 |> put_view(StatusView)
548 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
552 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
553 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
555 |> put_view(StatusView)
556 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
560 |> put_resp_content_type("application/json")
561 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
565 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
566 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
568 |> put_view(StatusView)
569 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
573 def bookmark_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.create(user.id, activity.id) do
578 user = Repo.preload(user, bookmarks: :activity)
581 |> put_view(StatusView)
582 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
586 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
587 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
588 %User{} = user <- User.get_cached_by_nickname(user.nickname),
589 true <- Visibility.visible_for_user?(activity, user),
590 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
591 user = Repo.preload(user, bookmarks: :activity)
594 |> put_view(StatusView)
595 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
599 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
600 activity = Activity.get_by_id(id)
602 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
604 |> put_view(StatusView)
605 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
609 |> put_resp_content_type("application/json")
610 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
614 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
615 activity = Activity.get_by_id(id)
617 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
619 |> put_view(StatusView)
620 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
624 def notifications(%{assigns: %{user: user}} = conn, params) do
625 notifications = MastodonAPI.get_notifications(user, params)
628 |> add_link_headers(:notifications, notifications)
629 |> put_view(NotificationView)
630 |> render("index.json", %{notifications: notifications, for: user})
633 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
634 with {:ok, notification} <- Notification.get(user, id) do
636 |> put_view(NotificationView)
637 |> render("show.json", %{notification: notification, for: user})
641 |> put_resp_content_type("application/json")
642 |> send_resp(403, Jason.encode!(%{"error" => reason}))
646 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
647 Notification.clear(user)
651 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
652 with {:ok, _notif} <- Notification.dismiss(user, id) do
657 |> put_resp_content_type("application/json")
658 |> send_resp(403, Jason.encode!(%{"error" => reason}))
662 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
663 Notification.destroy_multiple(user, ids)
667 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
669 q = from(u in User, where: u.id in ^id)
670 targets = Repo.all(q)
673 |> put_view(AccountView)
674 |> render("relationships.json", %{user: user, targets: targets})
677 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
678 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
680 def update_media(%{assigns: %{user: user}} = conn, data) do
681 with %Object{} = object <- Repo.get(Object, data["id"]),
682 true <- Object.authorize_mutation(object, user),
683 true <- is_binary(data["description"]),
684 description <- data["description"] do
685 new_data = %{object.data | "name" => description}
689 |> Object.change(%{data: new_data})
692 attachment_data = Map.put(new_data, "id", object.id)
695 |> put_view(StatusView)
696 |> render("attachment.json", %{attachment: attachment_data})
700 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
701 with {:ok, object} <-
704 actor: User.ap_id(user),
705 description: Map.get(data, "description")
707 attachment_data = Map.put(object.data, "id", object.id)
710 |> put_view(StatusView)
711 |> render("attachment.json", %{attachment: attachment_data})
715 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
716 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
717 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
718 q = from(u in User, where: u.ap_id in ^likes)
722 |> put_view(AccountView)
723 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
729 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
730 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
731 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
732 q = from(u in User, where: u.ap_id in ^announces)
736 |> put_view(AccountView)
737 |> render("accounts.json", %{for: user, users: users, as: :user})
743 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
744 local_only = params["local"] in [true, "True", "true", "1"]
747 [params["tag"], params["any"]]
751 |> Enum.map(&String.downcase(&1))
756 |> Enum.map(&String.downcase(&1))
761 |> Enum.map(&String.downcase(&1))
765 |> Map.put("type", "Create")
766 |> Map.put("local_only", local_only)
767 |> Map.put("blocking_user", user)
768 |> Map.put("muting_user", user)
769 |> Map.put("tag", tags)
770 |> Map.put("tag_all", tag_all)
771 |> Map.put("tag_reject", tag_reject)
772 |> ActivityPub.fetch_public_activities()
776 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
777 |> put_view(StatusView)
778 |> render("index.json", %{activities: activities, for: user, as: :activity})
781 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
782 with %User{} = user <- User.get_cached_by_id(id),
783 followers <- MastodonAPI.get_followers(user, params) do
786 for_user && user.id == for_user.id -> followers
787 user.info.hide_followers -> []
792 |> add_link_headers(:followers, followers, user)
793 |> put_view(AccountView)
794 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
798 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
799 with %User{} = user <- User.get_cached_by_id(id),
800 followers <- MastodonAPI.get_friends(user, params) do
803 for_user && user.id == for_user.id -> followers
804 user.info.hide_follows -> []
809 |> add_link_headers(:following, followers, user)
810 |> put_view(AccountView)
811 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
815 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
816 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
818 |> put_view(AccountView)
819 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
823 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
824 with %User{} = follower <- User.get_cached_by_id(id),
825 {:ok, follower} <- CommonAPI.accept_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 reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
838 with %User{} = follower <- User.get_cached_by_id(id),
839 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
841 |> put_view(AccountView)
842 |> render("relationship.json", %{user: followed, target: follower})
846 |> put_resp_content_type("application/json")
847 |> send_resp(403, Jason.encode!(%{"error" => message}))
851 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
852 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
853 {_, true} <- {:followed, follower.id != followed.id},
854 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
856 |> put_view(AccountView)
857 |> render("relationship.json", %{user: follower, target: followed})
864 |> put_resp_content_type("application/json")
865 |> send_resp(403, Jason.encode!(%{"error" => message}))
869 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
870 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
871 {_, true} <- {:followed, follower.id != followed.id},
872 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
874 |> put_view(AccountView)
875 |> render("account.json", %{user: followed, for: follower})
882 |> put_resp_content_type("application/json")
883 |> send_resp(403, Jason.encode!(%{"error" => message}))
887 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
888 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
889 {_, true} <- {:followed, follower.id != followed.id},
890 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
892 |> put_view(AccountView)
893 |> render("relationship.json", %{user: follower, target: followed})
903 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
904 with %User{} = muted <- User.get_cached_by_id(id),
905 {:ok, muter} <- User.mute(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 unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
918 with %User{} = muted <- User.get_cached_by_id(id),
919 {:ok, muter} <- User.unmute(muter, muted) do
921 |> put_view(AccountView)
922 |> render("relationship.json", %{user: muter, target: muted})
926 |> put_resp_content_type("application/json")
927 |> send_resp(403, Jason.encode!(%{"error" => message}))
931 def mutes(%{assigns: %{user: user}} = conn, _) do
932 with muted_accounts <- User.muted_users(user) do
933 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
938 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
939 with %User{} = blocked <- User.get_cached_by_id(id),
940 {:ok, blocker} <- User.block(blocker, blocked),
941 {:ok, _activity} <- ActivityPub.block(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 unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
954 with %User{} = blocked <- User.get_cached_by_id(id),
955 {:ok, blocker} <- User.unblock(blocker, blocked),
956 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
958 |> put_view(AccountView)
959 |> render("relationship.json", %{user: blocker, target: blocked})
963 |> put_resp_content_type("application/json")
964 |> send_resp(403, Jason.encode!(%{"error" => message}))
968 def blocks(%{assigns: %{user: user}} = conn, _) do
969 with blocked_accounts <- User.blocked_users(user) do
970 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
975 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
976 json(conn, info.domain_blocks || [])
979 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
980 User.block_domain(blocker, domain)
984 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
985 User.unblock_domain(blocker, domain)
989 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
990 with %User{} = subscription_target <- User.get_cached_by_id(id),
991 {:ok, subscription_target} = User.subscribe(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 unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1004 with %User{} = subscription_target <- User.get_cached_by_id(id),
1005 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1007 |> put_view(AccountView)
1008 |> render("relationship.json", %{user: user, target: subscription_target})
1010 {:error, message} ->
1012 |> put_resp_content_type("application/json")
1013 |> send_resp(403, Jason.encode!(%{"error" => message}))
1017 def status_search(user, query) do
1019 if Regex.match?(~r/https?:/, query) do
1020 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1021 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1022 true <- Visibility.visible_for_user?(activity, user) do
1031 [a, o] in Activity.with_preloaded_object(Activity),
1032 where: fragment("?->>'type' = 'Create'", a.data),
1033 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1036 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1041 order_by: [desc: :id]
1044 Repo.all(q) ++ fetched
1047 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1048 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1050 statuses = status_search(user, query)
1052 tags_path = Web.base_url() <> "/tag/"
1058 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1059 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1060 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1063 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1065 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1072 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1073 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1075 statuses = status_search(user, query)
1081 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1082 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1085 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1087 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1094 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1095 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1097 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1102 def favourites(%{assigns: %{user: user}} = conn, params) do
1105 |> Map.put("type", "Create")
1106 |> Map.put("favorited_by", user.ap_id)
1107 |> Map.put("blocking_user", user)
1110 ActivityPub.fetch_activities([], params)
1113 user = Repo.preload(user, bookmarks: :activity)
1116 |> add_link_headers(:favourites, activities)
1117 |> put_view(StatusView)
1118 |> render("index.json", %{activities: activities, for: user, as: :activity})
1121 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1122 with %User{} = user <- User.get_by_id(id),
1123 false <- user.info.hide_favorites do
1126 |> Map.put("type", "Create")
1127 |> Map.put("favorited_by", user.ap_id)
1128 |> Map.put("blocking_user", for_user)
1132 ["https://www.w3.org/ns/activitystreams#Public"] ++
1133 [for_user.ap_id | for_user.following]
1135 ["https://www.w3.org/ns/activitystreams#Public"]
1140 |> ActivityPub.fetch_activities(params)
1144 |> add_link_headers(:favourites, activities)
1145 |> put_view(StatusView)
1146 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1149 {:error, :not_found}
1154 |> json(%{error: "Can't get favorites"})
1158 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1159 user = User.get_cached_by_id(user.id)
1160 user = Repo.preload(user, bookmarks: :activity)
1163 Bookmark.for_user_query(user.id)
1164 |> Pagination.fetch_paginated(params)
1168 |> Enum.map(fn b -> b.activity end)
1171 |> add_link_headers(:bookmarks, bookmarks)
1172 |> put_view(StatusView)
1173 |> render("index.json", %{activities: activities, for: user, as: :activity})
1176 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1177 lists = Pleroma.List.for_user(user, opts)
1178 res = ListView.render("lists.json", lists: lists)
1182 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1183 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1184 res = ListView.render("list.json", list: list)
1190 |> json(%{error: "Record not found"})
1194 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1195 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1196 res = ListView.render("lists.json", lists: lists)
1200 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1201 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1202 {:ok, _list} <- Pleroma.List.delete(list) do
1210 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1211 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1212 res = ListView.render("list.json", list: list)
1217 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1219 |> Enum.each(fn account_id ->
1220 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1221 %User{} = followed <- User.get_cached_by_id(account_id) do
1222 Pleroma.List.follow(list, followed)
1229 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1231 |> Enum.each(fn account_id ->
1232 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1233 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1234 Pleroma.List.unfollow(list, followed)
1241 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1242 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1243 {:ok, users} = Pleroma.List.get_following(list) do
1245 |> put_view(AccountView)
1246 |> render("accounts.json", %{for: user, users: users, as: :user})
1250 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1251 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1252 {:ok, list} <- Pleroma.List.rename(list, title) do
1253 res = ListView.render("list.json", list: list)
1261 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1262 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1265 |> Map.put("type", "Create")
1266 |> Map.put("blocking_user", user)
1267 |> Map.put("muting_user", user)
1269 # we must filter the following list for the user to avoid leaking statuses the user
1270 # does not actually have permission to see (for more info, peruse security issue #270).
1273 |> Enum.filter(fn x -> x in user.following end)
1274 |> ActivityPub.fetch_activities_bounded(following, params)
1277 user = Repo.preload(user, bookmarks: :activity)
1280 |> put_view(StatusView)
1281 |> render("index.json", %{activities: activities, for: user, as: :activity})
1286 |> json(%{error: "Error."})
1290 def index(%{assigns: %{user: user}} = conn, _params) do
1291 token = get_session(conn, :oauth_token)
1294 mastodon_emoji = mastodonized_emoji()
1296 limit = Config.get([:instance, :limit])
1299 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1301 flavour = get_user_flavour(user)
1306 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1307 access_token: token,
1309 domain: Pleroma.Web.Endpoint.host(),
1312 unfollow_modal: false,
1315 auto_play_gif: false,
1316 display_sensitive_media: false,
1317 reduce_motion: false,
1318 max_toot_chars: limit,
1319 mascot: "/images/pleroma-fox-tan-smol.png"
1322 delete_others_notice: present?(user.info.is_moderator),
1323 admin: present?(user.info.is_admin)
1327 default_privacy: user.info.default_scope,
1328 default_sensitive: false,
1329 allow_content_types: Config.get([:instance, :allowed_post_formats])
1331 media_attachments: %{
1332 accept_content_types: [
1348 user.info.settings ||
1378 push_subscription: nil,
1380 custom_emojis: mastodon_emoji,
1386 |> put_layout(false)
1387 |> put_view(MastodonView)
1388 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1391 |> put_session(:return_to, conn.request_path)
1392 |> redirect(to: "/web/login")
1396 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1397 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1399 with changeset <- Ecto.Changeset.change(user),
1400 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1401 {:ok, _user} <- User.update_and_set_cache(changeset) do
1406 |> put_resp_content_type("application/json")
1407 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1411 @supported_flavours ["glitch", "vanilla"]
1413 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1414 when flavour in @supported_flavours do
1415 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1417 with changeset <- Ecto.Changeset.change(user),
1418 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1419 {:ok, user} <- User.update_and_set_cache(changeset),
1420 flavour <- user.info.flavour do
1425 |> put_resp_content_type("application/json")
1426 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1430 def set_flavour(conn, _params) do
1433 |> json(%{error: "Unsupported flavour"})
1436 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1437 json(conn, get_user_flavour(user))
1440 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1444 defp get_user_flavour(_) do
1448 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1449 redirect(conn, to: local_mastodon_root_path(conn))
1452 @doc "Local Mastodon FE login init action"
1453 def login(conn, %{"code" => auth_token}) do
1454 with {:ok, app} <- get_or_make_app(),
1455 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1456 {:ok, token} <- Token.exchange_token(app, auth) do
1458 |> put_session(:oauth_token, token.token)
1459 |> redirect(to: local_mastodon_root_path(conn))
1463 @doc "Local Mastodon FE callback action"
1464 def login(conn, _) do
1465 with {:ok, app} <- get_or_make_app() do
1470 response_type: "code",
1471 client_id: app.client_id,
1473 scope: Enum.join(app.scopes, " ")
1476 redirect(conn, to: path)
1480 defp local_mastodon_root_path(conn) do
1481 case get_session(conn, :return_to) do
1483 mastodon_api_path(conn, :index, ["getting-started"])
1486 delete_session(conn, :return_to)
1491 defp get_or_make_app do
1492 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1493 scopes = ["read", "write", "follow", "push"]
1495 with %App{} = app <- Repo.get_by(App, find_attrs) do
1497 if app.scopes == scopes do
1501 |> Ecto.Changeset.change(%{scopes: scopes})
1509 App.register_changeset(
1511 Map.put(find_attrs, :scopes, scopes)
1518 def logout(conn, _) do
1521 |> redirect(to: "/")
1524 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1525 Logger.debug("Unimplemented, returning unmodified relationship")
1527 with %User{} = target <- User.get_cached_by_id(id) do
1529 |> put_view(AccountView)
1530 |> render("relationship.json", %{user: user, target: target})
1534 def empty_array(conn, _) do
1535 Logger.debug("Unimplemented, returning an empty array")
1539 def empty_object(conn, _) do
1540 Logger.debug("Unimplemented, returning an empty object")
1544 def get_filters(%{assigns: %{user: user}} = conn, _) do
1545 filters = Filter.get_filters(user)
1546 res = FilterView.render("filters.json", filters: filters)
1551 %{assigns: %{user: user}} = conn,
1552 %{"phrase" => phrase, "context" => context} = params
1558 hide: Map.get(params, "irreversible", nil),
1559 whole_word: Map.get(params, "boolean", true)
1563 {:ok, response} = Filter.create(query)
1564 res = FilterView.render("filter.json", filter: response)
1568 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1569 filter = Filter.get(filter_id, user)
1570 res = FilterView.render("filter.json", filter: filter)
1575 %{assigns: %{user: user}} = conn,
1576 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1580 filter_id: filter_id,
1583 hide: Map.get(params, "irreversible", nil),
1584 whole_word: Map.get(params, "boolean", true)
1588 {:ok, response} = Filter.update(query)
1589 res = FilterView.render("filter.json", filter: response)
1593 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1596 filter_id: filter_id
1599 {:ok, _} = Filter.delete(query)
1605 def errors(conn, {:error, %Changeset{} = changeset}) do
1608 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1609 |> Enum.map_join(", ", fn {_k, v} -> v end)
1613 |> json(%{error: error_message})
1616 def errors(conn, {:error, :not_found}) do
1619 |> json(%{error: "Record not found"})
1622 def errors(conn, _) do
1625 |> json("Something went wrong")
1628 def suggestions(%{assigns: %{user: user}} = conn, _) do
1629 suggestions = Config.get(:suggestions)
1631 if Keyword.get(suggestions, :enabled, false) do
1632 api = Keyword.get(suggestions, :third_party_engine, "")
1633 timeout = Keyword.get(suggestions, :timeout, 5000)
1634 limit = Keyword.get(suggestions, :limit, 23)
1636 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1638 user = user.nickname
1642 |> String.replace("{{host}}", host)
1643 |> String.replace("{{user}}", user)
1645 with {:ok, %{status: 200, body: body}} <-
1650 recv_timeout: timeout,
1654 {:ok, data} <- Jason.decode(body) do
1657 |> Enum.slice(0, limit)
1662 case User.get_or_fetch(x["acct"]) do
1663 {:ok, %User{id: id}} -> id
1669 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1672 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1678 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1685 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1686 with %Activity{} = activity <- Activity.get_by_id(status_id),
1687 true <- Visibility.visible_for_user?(activity, user) do
1691 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1701 def reports(%{assigns: %{user: user}} = conn, params) do
1702 case CommonAPI.report(user, params) do
1705 |> put_view(ReportView)
1706 |> try_render("report.json", %{activity: activity})
1710 |> put_status(:bad_request)
1711 |> json(%{error: err})
1715 def try_render(conn, target, params)
1716 when is_binary(target) do
1717 res = render(conn, target, params)
1722 |> json(%{error: "Can't display this activity"})
1728 def try_render(conn, _, _) do
1731 |> json(%{error: "Can't display this activity"})
1734 defp present?(nil), do: false
1735 defp present?(false), do: false
1736 defp present?(_), do: true