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
7 alias Pleroma.Object.Fetcher
12 alias Pleroma.Notification
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(id),
547 %User{} = user <- User.get_by_nickname(user.nickname),
548 true <- Visibility.visible_for_user?(activity, user),
549 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
551 |> put_view(StatusView)
552 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
556 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
557 with %Activity{} = activity <- Activity.get_by_id(id),
558 %User{} = user <- User.get_by_nickname(user.nickname),
559 true <- Visibility.visible_for_user?(activity, user),
560 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
562 |> put_view(StatusView)
563 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
567 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
568 activity = Activity.get_by_id(id)
570 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
577 |> put_resp_content_type("application/json")
578 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
582 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
583 activity = Activity.get_by_id(id)
585 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
587 |> put_view(StatusView)
588 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
592 def notifications(%{assigns: %{user: user}} = conn, params) do
593 notifications = MastodonAPI.get_notifications(user, params)
596 |> add_link_headers(:notifications, notifications)
597 |> put_view(NotificationView)
598 |> render("index.json", %{notifications: notifications, for: user})
601 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
602 with {:ok, notification} <- Notification.get(user, id) do
604 |> put_view(NotificationView)
605 |> render("show.json", %{notification: notification, for: user})
609 |> put_resp_content_type("application/json")
610 |> send_resp(403, Jason.encode!(%{"error" => reason}))
614 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
615 Notification.clear(user)
619 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
620 with {:ok, _notif} <- Notification.dismiss(user, id) do
625 |> put_resp_content_type("application/json")
626 |> send_resp(403, Jason.encode!(%{"error" => reason}))
630 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
631 Notification.destroy_multiple(user, ids)
635 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
637 q = from(u in User, where: u.id in ^id)
638 targets = Repo.all(q)
641 |> put_view(AccountView)
642 |> render("relationships.json", %{user: user, targets: targets})
645 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
646 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
648 def update_media(%{assigns: %{user: user}} = conn, data) do
649 with %Object{} = object <- Repo.get(Object, data["id"]),
650 true <- Object.authorize_mutation(object, user),
651 true <- is_binary(data["description"]),
652 description <- data["description"] do
653 new_data = %{object.data | "name" => description}
657 |> Object.change(%{data: new_data})
660 attachment_data = Map.put(new_data, "id", object.id)
663 |> put_view(StatusView)
664 |> render("attachment.json", %{attachment: attachment_data})
668 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
669 with {:ok, object} <-
672 actor: User.ap_id(user),
673 description: Map.get(data, "description")
675 attachment_data = Map.put(object.data, "id", object.id)
678 |> put_view(StatusView)
679 |> render("attachment.json", %{attachment: attachment_data})
683 def favourited_by(conn, %{"id" => id}) do
684 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
685 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
686 q = from(u in User, where: u.ap_id in ^likes)
690 |> put_view(AccountView)
691 |> render(AccountView, "accounts.json", %{users: users, as: :user})
697 def reblogged_by(conn, %{"id" => id}) do
698 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
699 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
700 q = from(u in User, where: u.ap_id in ^announces)
704 |> put_view(AccountView)
705 |> render("accounts.json", %{users: users, as: :user})
711 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
712 local_only = params["local"] in [true, "True", "true", "1"]
715 [params["tag"], params["any"]]
719 |> Enum.map(&String.downcase(&1))
724 |> Enum.map(&String.downcase(&1))
729 |> Enum.map(&String.downcase(&1))
733 |> Map.put("type", "Create")
734 |> Map.put("local_only", local_only)
735 |> Map.put("blocking_user", user)
736 |> Map.put("muting_user", user)
737 |> Map.put("tag", tags)
738 |> Map.put("tag_all", tag_all)
739 |> Map.put("tag_reject", tag_reject)
740 |> ActivityPub.fetch_public_activities()
744 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
745 |> put_view(StatusView)
746 |> render("index.json", %{activities: activities, for: user, as: :activity})
749 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
750 with %User{} = user <- User.get_by_id(id),
751 followers <- MastodonAPI.get_followers(user, params) do
754 for_user && user.id == for_user.id -> followers
755 user.info.hide_followers -> []
760 |> add_link_headers(:followers, followers, user)
761 |> put_view(AccountView)
762 |> render("accounts.json", %{users: followers, as: :user})
766 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
767 with %User{} = user <- User.get_by_id(id),
768 followers <- MastodonAPI.get_friends(user, params) do
771 for_user && user.id == for_user.id -> followers
772 user.info.hide_follows -> []
777 |> add_link_headers(:following, followers, user)
778 |> put_view(AccountView)
779 |> render("accounts.json", %{users: followers, as: :user})
783 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
784 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
786 |> put_view(AccountView)
787 |> render("accounts.json", %{users: follow_requests, as: :user})
791 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
792 with %User{} = follower <- User.get_by_id(id),
793 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
795 |> put_view(AccountView)
796 |> render("relationship.json", %{user: followed, target: follower})
800 |> put_resp_content_type("application/json")
801 |> send_resp(403, Jason.encode!(%{"error" => message}))
805 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
806 with %User{} = follower <- User.get_by_id(id),
807 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
809 |> put_view(AccountView)
810 |> render("relationship.json", %{user: followed, target: follower})
814 |> put_resp_content_type("application/json")
815 |> send_resp(403, Jason.encode!(%{"error" => message}))
819 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
820 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
821 {_, true} <- {:followed, follower.id != followed.id},
822 false <- User.following?(follower, followed),
823 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
825 |> put_view(AccountView)
826 |> render("relationship.json", %{user: follower, target: followed})
832 followed = User.get_cached_by_id(id)
835 case conn.params["reblogs"] do
836 true -> CommonAPI.show_reblogs(follower, followed)
837 false -> CommonAPI.hide_reblogs(follower, followed)
841 |> put_view(AccountView)
842 |> render("relationship.json", %{user: follower, target: followed})
846 |> put_resp_content_type("application/json")
847 |> send_resp(403, Jason.encode!(%{"error" => message}))
851 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
852 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
853 {_, true} <- {:followed, follower.id != followed.id},
854 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
856 |> put_view(AccountView)
857 |> render("account.json", %{user: followed, for: follower})
864 |> put_resp_content_type("application/json")
865 |> send_resp(403, Jason.encode!(%{"error" => message}))
869 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
870 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
871 {_, true} <- {:followed, follower.id != followed.id},
872 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
874 |> put_view(AccountView)
875 |> render("relationship.json", %{user: follower, target: followed})
885 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
886 with %User{} = muted <- User.get_by_id(id),
887 {:ok, muter} <- User.mute(muter, muted) do
889 |> put_view(AccountView)
890 |> render("relationship.json", %{user: muter, target: muted})
894 |> put_resp_content_type("application/json")
895 |> send_resp(403, Jason.encode!(%{"error" => message}))
899 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
900 with %User{} = muted <- User.get_by_id(id),
901 {:ok, muter} <- User.unmute(muter, muted) do
903 |> put_view(AccountView)
904 |> render("relationship.json", %{user: muter, target: muted})
908 |> put_resp_content_type("application/json")
909 |> send_resp(403, Jason.encode!(%{"error" => message}))
913 def mutes(%{assigns: %{user: user}} = conn, _) do
914 with muted_accounts <- User.muted_users(user) do
915 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
920 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
921 with %User{} = blocked <- User.get_by_id(id),
922 {:ok, blocker} <- User.block(blocker, blocked),
923 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
925 |> put_view(AccountView)
926 |> render("relationship.json", %{user: blocker, target: blocked})
930 |> put_resp_content_type("application/json")
931 |> send_resp(403, Jason.encode!(%{"error" => message}))
935 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
936 with %User{} = blocked <- User.get_by_id(id),
937 {:ok, blocker} <- User.unblock(blocker, blocked),
938 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
940 |> put_view(AccountView)
941 |> render("relationship.json", %{user: blocker, target: blocked})
945 |> put_resp_content_type("application/json")
946 |> send_resp(403, Jason.encode!(%{"error" => message}))
950 def blocks(%{assigns: %{user: user}} = conn, _) do
951 with blocked_accounts <- User.blocked_users(user) do
952 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
957 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
958 json(conn, info.domain_blocks || [])
961 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
962 User.block_domain(blocker, domain)
966 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
967 User.unblock_domain(blocker, domain)
971 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
972 with %User{} = subscription_target <- User.get_cached_by_id(id),
973 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
975 |> put_view(AccountView)
976 |> render("relationship.json", %{user: user, target: subscription_target})
980 |> put_resp_content_type("application/json")
981 |> send_resp(403, Jason.encode!(%{"error" => message}))
985 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
986 with %User{} = subscription_target <- User.get_cached_by_id(id),
987 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
989 |> put_view(AccountView)
990 |> render("relationship.json", %{user: user, target: subscription_target})
994 |> put_resp_content_type("application/json")
995 |> send_resp(403, Jason.encode!(%{"error" => message}))
999 def status_search(user, query) do
1001 if Regex.match?(~r/https?:/, query) do
1002 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1003 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1004 true <- Visibility.visible_for_user?(activity, user) do
1014 where: fragment("?->>'type' = 'Create'", a.data),
1015 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1018 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
1023 order_by: [desc: :id]
1026 Repo.all(q) ++ fetched
1029 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1030 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1032 statuses = status_search(user, query)
1034 tags_path = Web.base_url() <> "/tag/"
1040 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1041 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1042 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1045 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1047 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1054 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1055 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1057 statuses = status_search(user, query)
1063 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1064 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1067 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1069 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1076 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1077 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1079 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1084 def favourites(%{assigns: %{user: user}} = conn, params) do
1087 |> Map.put("type", "Create")
1088 |> Map.put("favorited_by", user.ap_id)
1089 |> Map.put("blocking_user", user)
1092 ActivityPub.fetch_activities([], params)
1096 |> add_link_headers(:favourites, activities)
1097 |> put_view(StatusView)
1098 |> render("index.json", %{activities: activities, for: user, as: :activity})
1101 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1102 user = User.get_by_id(user.id)
1106 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1110 |> put_view(StatusView)
1111 |> render("index.json", %{activities: activities, for: user, as: :activity})
1114 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1115 lists = Pleroma.List.for_user(user, opts)
1116 res = ListView.render("lists.json", lists: lists)
1120 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1121 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1122 res = ListView.render("list.json", list: list)
1128 |> json(%{error: "Record not found"})
1132 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1133 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1134 res = ListView.render("lists.json", lists: lists)
1138 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1139 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1140 {:ok, _list} <- Pleroma.List.delete(list) do
1148 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1149 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1150 res = ListView.render("list.json", list: list)
1155 def add_to_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 <- User.get_by_id(account_id) do
1160 Pleroma.List.follow(list, followed)
1167 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1169 |> Enum.each(fn account_id ->
1170 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1171 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1172 Pleroma.List.unfollow(list, followed)
1179 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1180 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1181 {:ok, users} = Pleroma.List.get_following(list) do
1183 |> put_view(AccountView)
1184 |> render("accounts.json", %{users: users, as: :user})
1188 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1189 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1190 {:ok, list} <- Pleroma.List.rename(list, title) do
1191 res = ListView.render("list.json", list: list)
1199 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1200 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1203 |> Map.put("type", "Create")
1204 |> Map.put("blocking_user", user)
1205 |> Map.put("muting_user", user)
1207 # we must filter the following list for the user to avoid leaking statuses the user
1208 # does not actually have permission to see (for more info, peruse security issue #270).
1211 |> Enum.filter(fn x -> x in user.following end)
1212 |> ActivityPub.fetch_activities_bounded(following, params)
1216 |> put_view(StatusView)
1217 |> render("index.json", %{activities: activities, for: user, as: :activity})
1222 |> json(%{error: "Error."})
1226 def index(%{assigns: %{user: user}} = conn, _params) do
1227 token = get_session(conn, :oauth_token)
1230 mastodon_emoji = mastodonized_emoji()
1232 limit = Config.get([:instance, :limit])
1235 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1237 flavour = get_user_flavour(user)
1242 streaming_api_base_url:
1243 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1244 access_token: token,
1246 domain: Pleroma.Web.Endpoint.host(),
1249 unfollow_modal: false,
1252 auto_play_gif: false,
1253 display_sensitive_media: false,
1254 reduce_motion: false,
1255 max_toot_chars: limit,
1256 mascot: "/images/pleroma-fox-tan-smol.png"
1259 delete_others_notice: present?(user.info.is_moderator),
1260 admin: present?(user.info.is_admin)
1264 default_privacy: user.info.default_scope,
1265 default_sensitive: false,
1266 allow_content_types: Config.get([:instance, :allowed_post_formats])
1268 media_attachments: %{
1269 accept_content_types: [
1285 user.info.settings ||
1315 push_subscription: nil,
1317 custom_emojis: mastodon_emoji,
1323 |> put_layout(false)
1324 |> put_view(MastodonView)
1325 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1328 |> put_session(:return_to, conn.request_path)
1329 |> redirect(to: "/web/login")
1333 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1334 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1336 with changeset <- Ecto.Changeset.change(user),
1337 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1338 {:ok, _user} <- User.update_and_set_cache(changeset) do
1343 |> put_resp_content_type("application/json")
1344 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1348 @supported_flavours ["glitch", "vanilla"]
1350 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1351 when flavour in @supported_flavours do
1352 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1354 with changeset <- Ecto.Changeset.change(user),
1355 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1356 {:ok, user} <- User.update_and_set_cache(changeset),
1357 flavour <- user.info.flavour do
1362 |> put_resp_content_type("application/json")
1363 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1367 def set_flavour(conn, _params) do
1370 |> json(%{error: "Unsupported flavour"})
1373 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1374 json(conn, get_user_flavour(user))
1377 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1381 defp get_user_flavour(_) do
1385 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1386 redirect(conn, to: local_mastodon_root_path(conn))
1389 @doc "Local Mastodon FE login init action"
1390 def login(conn, %{"code" => auth_token}) do
1391 with {:ok, app} <- get_or_make_app(),
1392 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1393 {:ok, token} <- Token.exchange_token(app, auth) do
1395 |> put_session(:oauth_token, token.token)
1396 |> redirect(to: local_mastodon_root_path(conn))
1400 @doc "Local Mastodon FE callback action"
1401 def login(conn, _) do
1402 with {:ok, app} <- get_or_make_app() do
1407 response_type: "code",
1408 client_id: app.client_id,
1410 scope: Enum.join(app.scopes, " ")
1413 redirect(conn, to: path)
1417 defp local_mastodon_root_path(conn) do
1418 case get_session(conn, :return_to) do
1420 mastodon_api_path(conn, :index, ["getting-started"])
1423 delete_session(conn, :return_to)
1428 defp get_or_make_app do
1429 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1430 scopes = ["read", "write", "follow", "push"]
1432 with %App{} = app <- Repo.get_by(App, find_attrs) do
1434 if app.scopes == scopes do
1438 |> Ecto.Changeset.change(%{scopes: scopes})
1446 App.register_changeset(
1448 Map.put(find_attrs, :scopes, scopes)
1455 def logout(conn, _) do
1458 |> redirect(to: "/")
1461 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1462 Logger.debug("Unimplemented, returning unmodified relationship")
1464 with %User{} = target <- User.get_by_id(id) do
1466 |> put_view(AccountView)
1467 |> render("relationship.json", %{user: user, target: target})
1471 def empty_array(conn, _) do
1472 Logger.debug("Unimplemented, returning an empty array")
1476 def empty_object(conn, _) do
1477 Logger.debug("Unimplemented, returning an empty object")
1481 def get_filters(%{assigns: %{user: user}} = conn, _) do
1482 filters = Filter.get_filters(user)
1483 res = FilterView.render("filters.json", filters: filters)
1488 %{assigns: %{user: user}} = conn,
1489 %{"phrase" => phrase, "context" => context} = params
1495 hide: Map.get(params, "irreversible", nil),
1496 whole_word: Map.get(params, "boolean", true)
1500 {:ok, response} = Filter.create(query)
1501 res = FilterView.render("filter.json", filter: response)
1505 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1506 filter = Filter.get(filter_id, user)
1507 res = FilterView.render("filter.json", filter: filter)
1512 %{assigns: %{user: user}} = conn,
1513 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1517 filter_id: filter_id,
1520 hide: Map.get(params, "irreversible", nil),
1521 whole_word: Map.get(params, "boolean", true)
1525 {:ok, response} = Filter.update(query)
1526 res = FilterView.render("filter.json", filter: response)
1530 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1533 filter_id: filter_id
1536 {:ok, _} = Filter.delete(query)
1542 def errors(conn, {:error, %Changeset{} = changeset}) do
1545 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1546 |> Enum.map_join(", ", fn {_k, v} -> v end)
1550 |> json(%{error: error_message})
1553 def errors(conn, {:error, :not_found}) do
1556 |> json(%{error: "Record not found"})
1559 def errors(conn, _) do
1562 |> json("Something went wrong")
1565 def suggestions(%{assigns: %{user: user}} = conn, _) do
1566 suggestions = Config.get(:suggestions)
1568 if Keyword.get(suggestions, :enabled, false) do
1569 api = Keyword.get(suggestions, :third_party_engine, "")
1570 timeout = Keyword.get(suggestions, :timeout, 5000)
1571 limit = Keyword.get(suggestions, :limit, 23)
1573 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1575 user = user.nickname
1579 |> String.replace("{{host}}", host)
1580 |> String.replace("{{user}}", user)
1582 with {:ok, %{status: 200, body: body}} <-
1587 recv_timeout: timeout,
1591 {:ok, data} <- Jason.decode(body) do
1594 |> Enum.slice(0, limit)
1599 case User.get_or_fetch(x["acct"]) do
1606 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1609 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1615 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1622 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1623 with %Activity{} = activity <- Activity.get_by_id(status_id),
1624 true <- Visibility.visible_for_user?(activity, user) do
1628 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1638 def reports(%{assigns: %{user: user}} = conn, params) do
1639 case CommonAPI.report(user, params) do
1642 |> put_view(ReportView)
1643 |> try_render("report.json", %{activity: activity})
1647 |> put_status(:bad_request)
1648 |> json(%{error: err})
1652 def try_render(conn, target, params)
1653 when is_binary(target) do
1654 res = render(conn, target, params)
1659 |> json(%{error: "Can't display this activity"})
1665 def try_render(conn, _, _) do
1668 |> json(%{error: "Can't display this activity"})
1671 defp present?(nil), do: false
1672 defp present?(false), do: false
1673 defp present?(_), do: true