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 false <- User.following?(follower, followed),
825 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
827 |> put_view(AccountView)
828 |> render("relationship.json", %{user: follower, target: followed})
834 followed = User.get_cached_by_id(id)
837 case conn.params["reblogs"] do
838 true -> CommonAPI.show_reblogs(follower, followed)
839 false -> CommonAPI.hide_reblogs(follower, followed)
843 |> put_view(AccountView)
844 |> render("relationship.json", %{user: follower, target: followed})
848 |> put_resp_content_type("application/json")
849 |> send_resp(403, Jason.encode!(%{"error" => message}))
853 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
854 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
855 {_, true} <- {:followed, follower.id != followed.id},
856 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
858 |> put_view(AccountView)
859 |> render("account.json", %{user: followed, for: follower})
866 |> put_resp_content_type("application/json")
867 |> send_resp(403, Jason.encode!(%{"error" => message}))
871 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
872 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
873 {_, true} <- {:followed, follower.id != followed.id},
874 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
876 |> put_view(AccountView)
877 |> render("relationship.json", %{user: follower, target: followed})
887 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
888 with %User{} = muted <- User.get_by_id(id),
889 {:ok, muter} <- User.mute(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 unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
902 with %User{} = muted <- User.get_by_id(id),
903 {:ok, muter} <- User.unmute(muter, muted) do
905 |> put_view(AccountView)
906 |> render("relationship.json", %{user: muter, target: muted})
910 |> put_resp_content_type("application/json")
911 |> send_resp(403, Jason.encode!(%{"error" => message}))
915 def mutes(%{assigns: %{user: user}} = conn, _) do
916 with muted_accounts <- User.muted_users(user) do
917 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
922 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
923 with %User{} = blocked <- User.get_by_id(id),
924 {:ok, blocker} <- User.block(blocker, blocked),
925 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
927 |> put_view(AccountView)
928 |> render("relationship.json", %{user: blocker, target: blocked})
932 |> put_resp_content_type("application/json")
933 |> send_resp(403, Jason.encode!(%{"error" => message}))
937 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
938 with %User{} = blocked <- User.get_by_id(id),
939 {:ok, blocker} <- User.unblock(blocker, blocked),
940 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
942 |> put_view(AccountView)
943 |> render("relationship.json", %{user: blocker, target: blocked})
947 |> put_resp_content_type("application/json")
948 |> send_resp(403, Jason.encode!(%{"error" => message}))
952 def blocks(%{assigns: %{user: user}} = conn, _) do
953 with blocked_accounts <- User.blocked_users(user) do
954 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
959 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
960 json(conn, info.domain_blocks || [])
963 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
964 User.block_domain(blocker, domain)
968 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
969 User.unblock_domain(blocker, domain)
973 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
974 with %User{} = subscription_target <- User.get_cached_by_id(id),
975 {:ok, subscription_target} = User.subscribe(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 unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
988 with %User{} = subscription_target <- User.get_cached_by_id(id),
989 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
991 |> put_view(AccountView)
992 |> render("relationship.json", %{user: user, target: subscription_target})
996 |> put_resp_content_type("application/json")
997 |> send_resp(403, Jason.encode!(%{"error" => message}))
1001 def status_search(user, query) do
1003 if Regex.match?(~r/https?:/, query) do
1004 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1005 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1006 true <- Visibility.visible_for_user?(activity, user) do
1015 [a, o] in Activity.with_preloaded_object(Activity),
1016 where: fragment("?->>'type' = 'Create'", a.data),
1017 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1020 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1025 order_by: [desc: :id]
1028 Repo.all(q) ++ fetched
1031 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1032 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1034 statuses = status_search(user, query)
1036 tags_path = Web.base_url() <> "/tag/"
1042 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1043 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1044 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1047 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1049 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1056 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1057 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1059 statuses = status_search(user, query)
1065 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1066 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1069 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1071 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1078 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1079 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1081 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1086 def favourites(%{assigns: %{user: user}} = conn, params) do
1089 |> Map.put("type", "Create")
1090 |> Map.put("favorited_by", user.ap_id)
1091 |> Map.put("blocking_user", user)
1094 ActivityPub.fetch_activities([], params)
1098 |> add_link_headers(:favourites, activities)
1099 |> put_view(StatusView)
1100 |> render("index.json", %{activities: activities, for: user, as: :activity})
1103 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1104 user = User.get_by_id(user.id)
1108 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1112 |> put_view(StatusView)
1113 |> render("index.json", %{activities: activities, for: user, as: :activity})
1116 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1117 lists = Pleroma.List.for_user(user, opts)
1118 res = ListView.render("lists.json", lists: lists)
1122 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1123 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1124 res = ListView.render("list.json", list: list)
1130 |> json(%{error: "Record not found"})
1134 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1135 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1136 res = ListView.render("lists.json", lists: lists)
1140 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1141 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1142 {:ok, _list} <- Pleroma.List.delete(list) do
1150 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1151 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1152 res = ListView.render("list.json", list: list)
1157 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1159 |> Enum.each(fn account_id ->
1160 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1161 %User{} = followed <- User.get_by_id(account_id) do
1162 Pleroma.List.follow(list, followed)
1169 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1171 |> Enum.each(fn account_id ->
1172 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1173 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1174 Pleroma.List.unfollow(list, followed)
1181 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1182 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1183 {:ok, users} = Pleroma.List.get_following(list) do
1185 |> put_view(AccountView)
1186 |> render("accounts.json", %{users: users, as: :user})
1190 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1191 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1192 {:ok, list} <- Pleroma.List.rename(list, title) do
1193 res = ListView.render("list.json", list: list)
1201 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1202 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1205 |> Map.put("type", "Create")
1206 |> Map.put("blocking_user", user)
1207 |> Map.put("muting_user", user)
1209 # we must filter the following list for the user to avoid leaking statuses the user
1210 # does not actually have permission to see (for more info, peruse security issue #270).
1213 |> Enum.filter(fn x -> x in user.following end)
1214 |> ActivityPub.fetch_activities_bounded(following, params)
1218 |> put_view(StatusView)
1219 |> render("index.json", %{activities: activities, for: user, as: :activity})
1224 |> json(%{error: "Error."})
1228 def index(%{assigns: %{user: user}} = conn, _params) do
1229 token = get_session(conn, :oauth_token)
1232 mastodon_emoji = mastodonized_emoji()
1234 limit = Config.get([:instance, :limit])
1237 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1239 flavour = get_user_flavour(user)
1244 streaming_api_base_url:
1245 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1246 access_token: token,
1248 domain: Pleroma.Web.Endpoint.host(),
1251 unfollow_modal: false,
1254 auto_play_gif: false,
1255 display_sensitive_media: false,
1256 reduce_motion: false,
1257 max_toot_chars: limit,
1258 mascot: "/images/pleroma-fox-tan-smol.png"
1261 delete_others_notice: present?(user.info.is_moderator),
1262 admin: present?(user.info.is_admin)
1266 default_privacy: user.info.default_scope,
1267 default_sensitive: false,
1268 allow_content_types: Config.get([:instance, :allowed_post_formats])
1270 media_attachments: %{
1271 accept_content_types: [
1287 user.info.settings ||
1317 push_subscription: nil,
1319 custom_emojis: mastodon_emoji,
1325 |> put_layout(false)
1326 |> put_view(MastodonView)
1327 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1330 |> put_session(:return_to, conn.request_path)
1331 |> redirect(to: "/web/login")
1335 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1336 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1338 with changeset <- Ecto.Changeset.change(user),
1339 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1340 {:ok, _user} <- User.update_and_set_cache(changeset) do
1345 |> put_resp_content_type("application/json")
1346 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1350 @supported_flavours ["glitch", "vanilla"]
1352 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1353 when flavour in @supported_flavours do
1354 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1356 with changeset <- Ecto.Changeset.change(user),
1357 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1358 {:ok, user} <- User.update_and_set_cache(changeset),
1359 flavour <- user.info.flavour do
1364 |> put_resp_content_type("application/json")
1365 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1369 def set_flavour(conn, _params) do
1372 |> json(%{error: "Unsupported flavour"})
1375 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1376 json(conn, get_user_flavour(user))
1379 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1383 defp get_user_flavour(_) do
1387 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1388 redirect(conn, to: local_mastodon_root_path(conn))
1391 @doc "Local Mastodon FE login init action"
1392 def login(conn, %{"code" => auth_token}) do
1393 with {:ok, app} <- get_or_make_app(),
1394 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1395 {:ok, token} <- Token.exchange_token(app, auth) do
1397 |> put_session(:oauth_token, token.token)
1398 |> redirect(to: local_mastodon_root_path(conn))
1402 @doc "Local Mastodon FE callback action"
1403 def login(conn, _) do
1404 with {:ok, app} <- get_or_make_app() do
1409 response_type: "code",
1410 client_id: app.client_id,
1412 scope: Enum.join(app.scopes, " ")
1415 redirect(conn, to: path)
1419 defp local_mastodon_root_path(conn) do
1420 case get_session(conn, :return_to) do
1422 mastodon_api_path(conn, :index, ["getting-started"])
1425 delete_session(conn, :return_to)
1430 defp get_or_make_app do
1431 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1432 scopes = ["read", "write", "follow", "push"]
1434 with %App{} = app <- Repo.get_by(App, find_attrs) do
1436 if app.scopes == scopes do
1440 |> Ecto.Changeset.change(%{scopes: scopes})
1448 App.register_changeset(
1450 Map.put(find_attrs, :scopes, scopes)
1457 def logout(conn, _) do
1460 |> redirect(to: "/")
1463 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1464 Logger.debug("Unimplemented, returning unmodified relationship")
1466 with %User{} = target <- User.get_by_id(id) do
1468 |> put_view(AccountView)
1469 |> render("relationship.json", %{user: user, target: target})
1473 def empty_array(conn, _) do
1474 Logger.debug("Unimplemented, returning an empty array")
1478 def empty_object(conn, _) do
1479 Logger.debug("Unimplemented, returning an empty object")
1483 def get_filters(%{assigns: %{user: user}} = conn, _) do
1484 filters = Filter.get_filters(user)
1485 res = FilterView.render("filters.json", filters: filters)
1490 %{assigns: %{user: user}} = conn,
1491 %{"phrase" => phrase, "context" => context} = params
1497 hide: Map.get(params, "irreversible", nil),
1498 whole_word: Map.get(params, "boolean", true)
1502 {:ok, response} = Filter.create(query)
1503 res = FilterView.render("filter.json", filter: response)
1507 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1508 filter = Filter.get(filter_id, user)
1509 res = FilterView.render("filter.json", filter: filter)
1514 %{assigns: %{user: user}} = conn,
1515 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1519 filter_id: filter_id,
1522 hide: Map.get(params, "irreversible", nil),
1523 whole_word: Map.get(params, "boolean", true)
1527 {:ok, response} = Filter.update(query)
1528 res = FilterView.render("filter.json", filter: response)
1532 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1535 filter_id: filter_id
1538 {:ok, _} = Filter.delete(query)
1544 def errors(conn, {:error, %Changeset{} = changeset}) do
1547 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1548 |> Enum.map_join(", ", fn {_k, v} -> v end)
1552 |> json(%{error: error_message})
1555 def errors(conn, {:error, :not_found}) do
1558 |> json(%{error: "Record not found"})
1561 def errors(conn, _) do
1564 |> json("Something went wrong")
1567 def suggestions(%{assigns: %{user: user}} = conn, _) do
1568 suggestions = Config.get(:suggestions)
1570 if Keyword.get(suggestions, :enabled, false) do
1571 api = Keyword.get(suggestions, :third_party_engine, "")
1572 timeout = Keyword.get(suggestions, :timeout, 5000)
1573 limit = Keyword.get(suggestions, :limit, 23)
1575 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1577 user = user.nickname
1581 |> String.replace("{{host}}", host)
1582 |> String.replace("{{user}}", user)
1584 with {:ok, %{status: 200, body: body}} <-
1589 recv_timeout: timeout,
1593 {:ok, data} <- Jason.decode(body) do
1596 |> Enum.slice(0, limit)
1601 case User.get_or_fetch(x["acct"]) do
1608 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1611 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1617 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1624 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1625 with %Activity{} = activity <- Activity.get_by_id(status_id),
1626 true <- Visibility.visible_for_user?(activity, user) do
1630 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1640 def reports(%{assigns: %{user: user}} = conn, params) do
1641 case CommonAPI.report(user, params) do
1644 |> put_view(ReportView)
1645 |> try_render("report.json", %{activity: activity})
1649 |> put_status(:bad_request)
1650 |> json(%{error: err})
1654 def try_render(conn, target, params)
1655 when is_binary(target) do
1656 res = render(conn, target, params)
1661 |> json(%{error: "Can't display this activity"})
1667 def try_render(conn, _, _) do
1670 |> json(%{error: "Can't display this activity"})
1673 defp present?(nil), do: false
1674 defp present?(false), do: false
1675 defp present?(_), do: true