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.Notification
13 alias Pleroma.Object.Fetcher
14 alias Pleroma.Pagination
16 alias Pleroma.ScheduledActivity
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.AppView
25 alias Pleroma.Web.MastodonAPI.FilterView
26 alias Pleroma.Web.MastodonAPI.ListView
27 alias Pleroma.Web.MastodonAPI.MastodonAPI
28 alias Pleroma.Web.MastodonAPI.MastodonView
29 alias Pleroma.Web.MastodonAPI.NotificationView
30 alias Pleroma.Web.MastodonAPI.ReportView
31 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
32 alias Pleroma.Web.MastodonAPI.StatusView
33 alias Pleroma.Web.MediaProxy
34 alias Pleroma.Web.OAuth.App
35 alias Pleroma.Web.OAuth.Authorization
36 alias Pleroma.Web.OAuth.Token
38 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
43 @httpoison Application.get_env(:pleroma, :httpoison)
44 @local_mastodon_name "Mastodon-Local"
46 action_fallback(:errors)
48 def create_app(conn, params) do
49 scopes = oauth_scopes(params, ["read"])
53 |> Map.drop(["scope", "scopes"])
54 |> Map.put("scopes", scopes)
56 with cs <- App.register_changeset(%App{}, app_attrs),
57 false <- cs.changes[:client_name] == @local_mastodon_name,
58 {:ok, app} <- Repo.insert(cs) do
61 |> render("show.json", %{app: app})
70 value_function \\ fn x -> {:ok, x} end
72 if Map.has_key?(params, params_field) do
73 case value_function.(params[params_field]) do
74 {:ok, new_value} -> Map.put(map, map_field, new_value)
82 def update_credentials(%{assigns: %{user: user}} = conn, params) do
87 |> add_if_present(params, "display_name", :name)
88 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
89 |> add_if_present(params, "avatar", :avatar, fn value ->
90 with %Plug.Upload{} <- value,
91 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
100 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
101 |> add_if_present(params, "header", :banner, fn value ->
102 with %Plug.Upload{} <- value,
103 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
110 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
112 with changeset <- User.update_changeset(user, user_params),
113 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
114 {:ok, user} <- User.update_and_set_cache(changeset) do
115 if original_user != user do
116 CommonAPI.update(user)
119 json(conn, AccountView.render("account.json", %{user: user, for: user}))
124 |> json(%{error: "Invalid request"})
128 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
129 account = AccountView.render("account.json", %{user: user, for: user})
133 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
134 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
137 |> render("short.json", %{app: app})
141 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
142 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
143 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
144 account = AccountView.render("account.json", %{user: user, for: for_user})
150 |> json(%{error: "Can't find user"})
154 @mastodon_api_level "2.5.0"
156 def masto_instance(conn, _params) do
157 instance = Config.get(:instance)
161 title: Keyword.get(instance, :name),
162 description: Keyword.get(instance, :description),
163 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
164 email: Keyword.get(instance, :email),
166 streaming_api: Pleroma.Web.Endpoint.websocket_url()
168 stats: Stats.get_stats(),
169 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
171 registrations: Pleroma.Config.get([:instance, :registrations_open]),
172 # Extra (not present in Mastodon):
173 max_toot_chars: Keyword.get(instance, :limit)
179 def peers(conn, _params) do
180 json(conn, Stats.get_peers())
183 defp mastodonized_emoji do
184 Pleroma.Emoji.get_all()
185 |> Enum.map(fn {shortcode, relative_url, tags} ->
186 url = to_string(URI.merge(Web.base_url(), relative_url))
189 "shortcode" => shortcode,
191 "visible_in_picker" => true,
193 "tags" => String.split(tags, ",")
198 def custom_emojis(conn, _params) do
199 mastodon_emoji = mastodonized_emoji()
200 json(conn, mastodon_emoji)
203 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
206 |> Map.drop(["since_id", "max_id", "min_id"])
209 last = List.last(activities)
216 |> Map.get("limit", "20")
217 |> String.to_integer()
220 if length(activities) <= limit do
226 |> Enum.at(limit * -1)
230 {next_url, prev_url} =
234 Pleroma.Web.Endpoint,
237 Map.merge(params, %{max_id: max_id})
240 Pleroma.Web.Endpoint,
243 Map.merge(params, %{min_id: min_id})
249 Pleroma.Web.Endpoint,
251 Map.merge(params, %{max_id: max_id})
254 Pleroma.Web.Endpoint,
256 Map.merge(params, %{min_id: min_id})
262 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
268 def home_timeline(%{assigns: %{user: user}} = conn, params) do
271 |> Map.put("type", ["Create", "Announce"])
272 |> Map.put("blocking_user", user)
273 |> Map.put("muting_user", user)
274 |> Map.put("user", user)
277 [user.ap_id | user.following]
278 |> ActivityPub.fetch_activities(params)
279 |> ActivityPub.contain_timeline(user)
283 |> add_link_headers(:home_timeline, activities)
284 |> put_view(StatusView)
285 |> render("index.json", %{activities: activities, for: user, as: :activity})
288 def public_timeline(%{assigns: %{user: user}} = conn, params) do
289 local_only = params["local"] in [true, "True", "true", "1"]
293 |> Map.put("type", ["Create", "Announce"])
294 |> Map.put("local_only", local_only)
295 |> Map.put("blocking_user", user)
296 |> Map.put("muting_user", user)
297 |> ActivityPub.fetch_public_activities()
301 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
302 |> put_view(StatusView)
303 |> render("index.json", %{activities: activities, for: user, as: :activity})
306 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
307 with %User{} = user <- User.get_by_id(params["id"]) do
308 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
311 |> add_link_headers(:user_statuses, activities, params["id"])
312 |> put_view(StatusView)
313 |> render("index.json", %{
314 activities: activities,
321 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
324 |> Map.put("type", "Create")
325 |> Map.put("blocking_user", user)
326 |> Map.put("user", user)
327 |> Map.put(:visibility, "direct")
331 |> ActivityPub.fetch_activities_query(params)
332 |> Pagination.fetch_paginated(params)
335 |> add_link_headers(:dm_timeline, activities)
336 |> put_view(StatusView)
337 |> render("index.json", %{activities: activities, for: user, as: :activity})
340 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
341 with %Activity{} = activity <- Activity.get_by_id(id),
342 true <- Visibility.visible_for_user?(activity, user) do
344 |> put_view(StatusView)
345 |> try_render("status.json", %{activity: activity, for: user})
349 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
350 with %Activity{} = activity <- Activity.get_by_id(id),
352 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
353 "blocking_user" => user,
357 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
359 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
360 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
366 activities: grouped_activities[true] || [],
370 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
375 activities: grouped_activities[false] || [],
379 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
386 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
387 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
389 |> add_link_headers(:scheduled_statuses, scheduled_activities)
390 |> put_view(ScheduledActivityView)
391 |> render("index.json", %{scheduled_activities: scheduled_activities})
395 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
396 with %ScheduledActivity{} = scheduled_activity <-
397 ScheduledActivity.get(user, scheduled_activity_id) do
399 |> put_view(ScheduledActivityView)
400 |> render("show.json", %{scheduled_activity: scheduled_activity})
402 _ -> {:error, :not_found}
406 def update_scheduled_status(
407 %{assigns: %{user: user}} = conn,
408 %{"id" => scheduled_activity_id} = params
410 with %ScheduledActivity{} = scheduled_activity <-
411 ScheduledActivity.get(user, scheduled_activity_id),
412 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
414 |> put_view(ScheduledActivityView)
415 |> render("show.json", %{scheduled_activity: scheduled_activity})
417 nil -> {:error, :not_found}
422 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id),
425 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
427 |> put_view(ScheduledActivityView)
428 |> render("show.json", %{scheduled_activity: scheduled_activity})
430 nil -> {:error, :not_found}
435 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
436 when length(media_ids) > 0 do
439 |> Map.put("status", ".")
441 post_status(conn, params)
444 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
447 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
450 case get_req_header(conn, "idempotency-key") do
452 _ -> Ecto.UUID.generate()
455 scheduled_at = params["scheduled_at"]
457 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
458 with {:ok, scheduled_activity} <-
459 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
461 |> put_view(ScheduledActivityView)
462 |> render("show.json", %{scheduled_activity: scheduled_activity})
465 params = Map.drop(params, ["scheduled_at"])
468 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
469 CommonAPI.post(user, params)
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
479 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
485 |> json(%{error: "Can't delete this post"})
489 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
490 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
492 |> put_view(StatusView)
493 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
497 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
498 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
499 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
501 |> put_view(StatusView)
502 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
506 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
507 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
508 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
510 |> put_view(StatusView)
511 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
515 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
524 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
527 |> put_view(StatusView)
528 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
532 |> put_resp_content_type("application/json")
533 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
537 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
538 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
540 |> put_view(StatusView)
541 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
545 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
546 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
547 %Object{} = object <- Object.normalize(activity),
548 %User{} = user <- User.get_by_nickname(user.nickname),
549 true <- Visibility.visible_for_user?(activity, user),
550 {:ok, user} <- User.bookmark(user, object.data["id"]) do
552 |> put_view(StatusView)
553 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
557 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
558 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
559 %Object{} = object <- Object.normalize(activity),
560 %User{} = user <- User.get_by_nickname(user.nickname),
561 true <- Visibility.visible_for_user?(activity, user),
562 {:ok, user} <- User.unbookmark(user, object.data["id"]) do
564 |> put_view(StatusView)
565 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
569 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
570 activity = Activity.get_by_id(id)
572 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
574 |> put_view(StatusView)
575 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
579 |> put_resp_content_type("application/json")
580 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
584 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
585 activity = Activity.get_by_id(id)
587 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
589 |> put_view(StatusView)
590 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
594 def notifications(%{assigns: %{user: user}} = conn, params) do
595 notifications = MastodonAPI.get_notifications(user, params)
598 |> add_link_headers(:notifications, notifications)
599 |> put_view(NotificationView)
600 |> render("index.json", %{notifications: notifications, for: user})
603 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
604 with {:ok, notification} <- Notification.get(user, id) do
606 |> put_view(NotificationView)
607 |> render("show.json", %{notification: notification, for: user})
611 |> put_resp_content_type("application/json")
612 |> send_resp(403, Jason.encode!(%{"error" => reason}))
616 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
617 Notification.clear(user)
621 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
622 with {:ok, _notif} <- Notification.dismiss(user, id) do
627 |> put_resp_content_type("application/json")
628 |> send_resp(403, Jason.encode!(%{"error" => reason}))
632 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
633 Notification.destroy_multiple(user, ids)
637 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
639 q = from(u in User, where: u.id in ^id)
640 targets = Repo.all(q)
643 |> put_view(AccountView)
644 |> render("relationships.json", %{user: user, targets: targets})
647 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
648 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
650 def update_media(%{assigns: %{user: user}} = conn, data) do
651 with %Object{} = object <- Repo.get(Object, data["id"]),
652 true <- Object.authorize_mutation(object, user),
653 true <- is_binary(data["description"]),
654 description <- data["description"] do
655 new_data = %{object.data | "name" => description}
659 |> Object.change(%{data: new_data})
662 attachment_data = Map.put(new_data, "id", object.id)
665 |> put_view(StatusView)
666 |> render("attachment.json", %{attachment: attachment_data})
670 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
671 with {:ok, object} <-
674 actor: User.ap_id(user),
675 description: Map.get(data, "description")
677 attachment_data = Map.put(object.data, "id", object.id)
680 |> put_view(StatusView)
681 |> render("attachment.json", %{attachment: attachment_data})
685 def favourited_by(conn, %{"id" => id}) do
686 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
687 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
688 q = from(u in User, where: u.ap_id in ^likes)
692 |> put_view(AccountView)
693 |> render(AccountView, "accounts.json", %{users: users, as: :user})
699 def reblogged_by(conn, %{"id" => id}) do
700 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
701 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
702 q = from(u in User, where: u.ap_id in ^announces)
706 |> put_view(AccountView)
707 |> render("accounts.json", %{users: users, as: :user})
713 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
714 local_only = params["local"] in [true, "True", "true", "1"]
717 [params["tag"], params["any"]]
721 |> Enum.map(&String.downcase(&1))
726 |> Enum.map(&String.downcase(&1))
731 |> Enum.map(&String.downcase(&1))
735 |> Map.put("type", "Create")
736 |> Map.put("local_only", local_only)
737 |> Map.put("blocking_user", user)
738 |> Map.put("muting_user", user)
739 |> Map.put("tag", tags)
740 |> Map.put("tag_all", tag_all)
741 |> Map.put("tag_reject", tag_reject)
742 |> ActivityPub.fetch_public_activities()
746 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
747 |> put_view(StatusView)
748 |> render("index.json", %{activities: activities, for: user, as: :activity})
751 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
752 with %User{} = user <- User.get_by_id(id),
753 followers <- MastodonAPI.get_followers(user, params) do
756 for_user && user.id == for_user.id -> followers
757 user.info.hide_followers -> []
762 |> add_link_headers(:followers, followers, user)
763 |> put_view(AccountView)
764 |> render("accounts.json", %{users: followers, as: :user})
768 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
769 with %User{} = user <- User.get_by_id(id),
770 followers <- MastodonAPI.get_friends(user, params) do
773 for_user && user.id == for_user.id -> followers
774 user.info.hide_follows -> []
779 |> add_link_headers(:following, followers, user)
780 |> put_view(AccountView)
781 |> render("accounts.json", %{users: followers, as: :user})
785 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
786 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
788 |> put_view(AccountView)
789 |> render("accounts.json", %{users: follow_requests, as: :user})
793 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
794 with %User{} = follower <- User.get_by_id(id),
795 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
797 |> put_view(AccountView)
798 |> render("relationship.json", %{user: followed, target: follower})
802 |> put_resp_content_type("application/json")
803 |> send_resp(403, Jason.encode!(%{"error" => message}))
807 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
808 with %User{} = follower <- User.get_by_id(id),
809 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
811 |> put_view(AccountView)
812 |> render("relationship.json", %{user: followed, target: follower})
816 |> put_resp_content_type("application/json")
817 |> send_resp(403, Jason.encode!(%{"error" => message}))
821 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
822 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
823 {_, true} <- {:followed, follower.id != followed.id},
824 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
826 |> put_view(AccountView)
827 |> render("relationship.json", %{user: follower, target: followed})
834 |> put_resp_content_type("application/json")
835 |> send_resp(403, Jason.encode!(%{"error" => message}))
839 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
840 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
841 {_, true} <- {:followed, follower.id != followed.id},
842 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
844 |> put_view(AccountView)
845 |> render("account.json", %{user: followed, for: follower})
852 |> put_resp_content_type("application/json")
853 |> send_resp(403, Jason.encode!(%{"error" => message}))
857 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
858 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
859 {_, true} <- {:followed, follower.id != followed.id},
860 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
862 |> put_view(AccountView)
863 |> render("relationship.json", %{user: follower, target: followed})
873 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
874 with %User{} = muted <- User.get_by_id(id),
875 {:ok, muter} <- User.mute(muter, muted) do
877 |> put_view(AccountView)
878 |> render("relationship.json", %{user: muter, target: muted})
882 |> put_resp_content_type("application/json")
883 |> send_resp(403, Jason.encode!(%{"error" => message}))
887 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
888 with %User{} = muted <- User.get_by_id(id),
889 {:ok, muter} <- User.unmute(muter, muted) do
891 |> put_view(AccountView)
892 |> render("relationship.json", %{user: muter, target: muted})
896 |> put_resp_content_type("application/json")
897 |> send_resp(403, Jason.encode!(%{"error" => message}))
901 def mutes(%{assigns: %{user: user}} = conn, _) do
902 with muted_accounts <- User.muted_users(user) do
903 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
908 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
909 with %User{} = blocked <- User.get_by_id(id),
910 {:ok, blocker} <- User.block(blocker, blocked),
911 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
913 |> put_view(AccountView)
914 |> render("relationship.json", %{user: blocker, target: blocked})
918 |> put_resp_content_type("application/json")
919 |> send_resp(403, Jason.encode!(%{"error" => message}))
923 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
924 with %User{} = blocked <- User.get_by_id(id),
925 {:ok, blocker} <- User.unblock(blocker, blocked),
926 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
928 |> put_view(AccountView)
929 |> render("relationship.json", %{user: blocker, target: blocked})
933 |> put_resp_content_type("application/json")
934 |> send_resp(403, Jason.encode!(%{"error" => message}))
938 def blocks(%{assigns: %{user: user}} = conn, _) do
939 with blocked_accounts <- User.blocked_users(user) do
940 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
945 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
946 json(conn, info.domain_blocks || [])
949 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
950 User.block_domain(blocker, domain)
954 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
955 User.unblock_domain(blocker, domain)
959 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
960 with %User{} = subscription_target <- User.get_cached_by_id(id),
961 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
963 |> put_view(AccountView)
964 |> render("relationship.json", %{user: user, target: subscription_target})
968 |> put_resp_content_type("application/json")
969 |> send_resp(403, Jason.encode!(%{"error" => message}))
973 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
974 with %User{} = subscription_target <- User.get_cached_by_id(id),
975 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
977 |> put_view(AccountView)
978 |> render("relationship.json", %{user: user, target: subscription_target})
982 |> put_resp_content_type("application/json")
983 |> send_resp(403, Jason.encode!(%{"error" => message}))
987 def status_search(user, query) do
989 if Regex.match?(~r/https?:/, query) do
990 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
991 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
992 true <- Visibility.visible_for_user?(activity, user) do
1001 [a, o] in Activity.with_preloaded_object(Activity),
1002 where: fragment("?->>'type' = 'Create'", a.data),
1003 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1006 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1011 order_by: [desc: :id]
1014 Repo.all(q) ++ fetched
1017 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1018 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1020 statuses = status_search(user, query)
1022 tags_path = Web.base_url() <> "/tag/"
1028 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1029 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1030 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1033 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1035 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1042 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1043 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1045 statuses = status_search(user, query)
1051 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1052 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1055 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1057 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1064 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1065 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1067 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1072 def favourites(%{assigns: %{user: user}} = conn, params) do
1075 |> Map.put("type", "Create")
1076 |> Map.put("favorited_by", user.ap_id)
1077 |> Map.put("blocking_user", user)
1080 ActivityPub.fetch_activities([], params)
1084 |> add_link_headers(:favourites, activities)
1085 |> put_view(StatusView)
1086 |> render("index.json", %{activities: activities, for: user, as: :activity})
1089 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1090 user = User.get_by_id(user.id)
1094 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1098 |> put_view(StatusView)
1099 |> render("index.json", %{activities: activities, for: user, as: :activity})
1102 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1103 lists = Pleroma.List.for_user(user, opts)
1104 res = ListView.render("lists.json", lists: lists)
1108 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1109 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1110 res = ListView.render("list.json", list: list)
1116 |> json(%{error: "Record not found"})
1120 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1121 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1122 res = ListView.render("lists.json", lists: lists)
1126 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1127 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1128 {:ok, _list} <- Pleroma.List.delete(list) do
1136 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1137 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1138 res = ListView.render("list.json", list: list)
1143 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1145 |> Enum.each(fn account_id ->
1146 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1147 %User{} = followed <- User.get_by_id(account_id) do
1148 Pleroma.List.follow(list, followed)
1155 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1157 |> Enum.each(fn account_id ->
1158 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1159 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1160 Pleroma.List.unfollow(list, followed)
1167 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1168 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1169 {:ok, users} = Pleroma.List.get_following(list) do
1171 |> put_view(AccountView)
1172 |> render("accounts.json", %{users: users, as: :user})
1176 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1177 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1178 {:ok, list} <- Pleroma.List.rename(list, title) do
1179 res = ListView.render("list.json", list: list)
1187 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1188 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1191 |> Map.put("type", "Create")
1192 |> Map.put("blocking_user", user)
1193 |> Map.put("muting_user", user)
1195 # we must filter the following list for the user to avoid leaking statuses the user
1196 # does not actually have permission to see (for more info, peruse security issue #270).
1199 |> Enum.filter(fn x -> x in user.following end)
1200 |> ActivityPub.fetch_activities_bounded(following, params)
1204 |> put_view(StatusView)
1205 |> render("index.json", %{activities: activities, for: user, as: :activity})
1210 |> json(%{error: "Error."})
1214 def index(%{assigns: %{user: user}} = conn, _params) do
1215 token = get_session(conn, :oauth_token)
1218 mastodon_emoji = mastodonized_emoji()
1220 limit = Config.get([:instance, :limit])
1223 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1225 flavour = get_user_flavour(user)
1230 streaming_api_base_url:
1231 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1232 access_token: token,
1234 domain: Pleroma.Web.Endpoint.host(),
1237 unfollow_modal: false,
1240 auto_play_gif: false,
1241 display_sensitive_media: false,
1242 reduce_motion: false,
1243 max_toot_chars: limit,
1244 mascot: "/images/pleroma-fox-tan-smol.png"
1247 delete_others_notice: present?(user.info.is_moderator),
1248 admin: present?(user.info.is_admin)
1252 default_privacy: user.info.default_scope,
1253 default_sensitive: false,
1254 allow_content_types: Config.get([:instance, :allowed_post_formats])
1256 media_attachments: %{
1257 accept_content_types: [
1273 user.info.settings ||
1303 push_subscription: nil,
1305 custom_emojis: mastodon_emoji,
1311 |> put_layout(false)
1312 |> put_view(MastodonView)
1313 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1316 |> put_session(:return_to, conn.request_path)
1317 |> redirect(to: "/web/login")
1321 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1322 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1324 with changeset <- Ecto.Changeset.change(user),
1325 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1326 {:ok, _user} <- User.update_and_set_cache(changeset) do
1331 |> put_resp_content_type("application/json")
1332 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1336 @supported_flavours ["glitch", "vanilla"]
1338 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1339 when flavour in @supported_flavours do
1340 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1342 with changeset <- Ecto.Changeset.change(user),
1343 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1344 {:ok, user} <- User.update_and_set_cache(changeset),
1345 flavour <- user.info.flavour do
1350 |> put_resp_content_type("application/json")
1351 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1355 def set_flavour(conn, _params) do
1358 |> json(%{error: "Unsupported flavour"})
1361 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1362 json(conn, get_user_flavour(user))
1365 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1369 defp get_user_flavour(_) do
1373 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1374 redirect(conn, to: local_mastodon_root_path(conn))
1377 @doc "Local Mastodon FE login init action"
1378 def login(conn, %{"code" => auth_token}) do
1379 with {:ok, app} <- get_or_make_app(),
1380 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1381 {:ok, token} <- Token.exchange_token(app, auth) do
1383 |> put_session(:oauth_token, token.token)
1384 |> redirect(to: local_mastodon_root_path(conn))
1388 @doc "Local Mastodon FE callback action"
1389 def login(conn, _) do
1390 with {:ok, app} <- get_or_make_app() do
1395 response_type: "code",
1396 client_id: app.client_id,
1398 scope: Enum.join(app.scopes, " ")
1401 redirect(conn, to: path)
1405 defp local_mastodon_root_path(conn) do
1406 case get_session(conn, :return_to) do
1408 mastodon_api_path(conn, :index, ["getting-started"])
1411 delete_session(conn, :return_to)
1416 defp get_or_make_app do
1417 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1418 scopes = ["read", "write", "follow", "push"]
1420 with %App{} = app <- Repo.get_by(App, find_attrs) do
1422 if app.scopes == scopes do
1426 |> Ecto.Changeset.change(%{scopes: scopes})
1434 App.register_changeset(
1436 Map.put(find_attrs, :scopes, scopes)
1443 def logout(conn, _) do
1446 |> redirect(to: "/")
1449 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1450 Logger.debug("Unimplemented, returning unmodified relationship")
1452 with %User{} = target <- User.get_by_id(id) do
1454 |> put_view(AccountView)
1455 |> render("relationship.json", %{user: user, target: target})
1459 def empty_array(conn, _) do
1460 Logger.debug("Unimplemented, returning an empty array")
1464 def empty_object(conn, _) do
1465 Logger.debug("Unimplemented, returning an empty object")
1469 def get_filters(%{assigns: %{user: user}} = conn, _) do
1470 filters = Filter.get_filters(user)
1471 res = FilterView.render("filters.json", filters: filters)
1476 %{assigns: %{user: user}} = conn,
1477 %{"phrase" => phrase, "context" => context} = params
1483 hide: Map.get(params, "irreversible", nil),
1484 whole_word: Map.get(params, "boolean", true)
1488 {:ok, response} = Filter.create(query)
1489 res = FilterView.render("filter.json", filter: response)
1493 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1494 filter = Filter.get(filter_id, user)
1495 res = FilterView.render("filter.json", filter: filter)
1500 %{assigns: %{user: user}} = conn,
1501 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1505 filter_id: filter_id,
1508 hide: Map.get(params, "irreversible", nil),
1509 whole_word: Map.get(params, "boolean", true)
1513 {:ok, response} = Filter.update(query)
1514 res = FilterView.render("filter.json", filter: response)
1518 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1521 filter_id: filter_id
1524 {:ok, _} = Filter.delete(query)
1530 def errors(conn, {:error, %Changeset{} = changeset}) do
1533 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1534 |> Enum.map_join(", ", fn {_k, v} -> v end)
1538 |> json(%{error: error_message})
1541 def errors(conn, {:error, :not_found}) do
1544 |> json(%{error: "Record not found"})
1547 def errors(conn, _) do
1550 |> json("Something went wrong")
1553 def suggestions(%{assigns: %{user: user}} = conn, _) do
1554 suggestions = Config.get(:suggestions)
1556 if Keyword.get(suggestions, :enabled, false) do
1557 api = Keyword.get(suggestions, :third_party_engine, "")
1558 timeout = Keyword.get(suggestions, :timeout, 5000)
1559 limit = Keyword.get(suggestions, :limit, 23)
1561 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1563 user = user.nickname
1567 |> String.replace("{{host}}", host)
1568 |> String.replace("{{user}}", user)
1570 with {:ok, %{status: 200, body: body}} <-
1575 recv_timeout: timeout,
1579 {:ok, data} <- Jason.decode(body) do
1582 |> Enum.slice(0, limit)
1587 case User.get_or_fetch(x["acct"]) do
1594 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1597 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1603 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1610 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1611 with %Activity{} = activity <- Activity.get_by_id(status_id),
1612 true <- Visibility.visible_for_user?(activity, user) do
1616 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1626 def reports(%{assigns: %{user: user}} = conn, params) do
1627 case CommonAPI.report(user, params) do
1630 |> put_view(ReportView)
1631 |> try_render("report.json", %{activity: activity})
1635 |> put_status(:bad_request)
1636 |> json(%{error: err})
1640 def try_render(conn, target, params)
1641 when is_binary(target) do
1642 res = render(conn, target, params)
1647 |> json(%{error: "Can't display this activity"})
1653 def try_render(conn, _, _) do
1656 |> json(%{error: "Can't display this activity"})
1659 defp present?(nil), do: false
1660 defp present?(false), do: false
1661 defp present?(_), do: true