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
14 alias Pleroma.ScheduledActivity
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Visibility
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.AppView
23 alias Pleroma.Web.MastodonAPI.FilterView
24 alias Pleroma.Web.MastodonAPI.ListView
25 alias Pleroma.Web.MastodonAPI.MastodonAPI
26 alias Pleroma.Web.MastodonAPI.MastodonView
27 alias Pleroma.Web.MastodonAPI.NotificationView
28 alias Pleroma.Web.MastodonAPI.ReportView
29 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
30 alias Pleroma.Web.MastodonAPI.StatusView
31 alias Pleroma.Web.MediaProxy
32 alias Pleroma.Web.OAuth.App
33 alias Pleroma.Web.OAuth.Authorization
34 alias Pleroma.Web.OAuth.Token
36 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
41 @httpoison Application.get_env(:pleroma, :httpoison)
42 @local_mastodon_name "Mastodon-Local"
44 action_fallback(:errors)
46 def create_app(conn, params) do
47 scopes = oauth_scopes(params, ["read"])
51 |> Map.drop(["scope", "scopes"])
52 |> Map.put("scopes", scopes)
54 with cs <- App.register_changeset(%App{}, app_attrs),
55 false <- cs.changes[:client_name] == @local_mastodon_name,
56 {:ok, app} <- Repo.insert(cs) do
59 |> render("show.json", %{app: app})
68 value_function \\ fn x -> {:ok, x} end
70 if Map.has_key?(params, params_field) do
71 case value_function.(params[params_field]) do
72 {:ok, new_value} -> Map.put(map, map_field, new_value)
80 def update_credentials(%{assigns: %{user: user}} = conn, params) do
85 |> add_if_present(params, "display_name", :name)
86 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
87 |> add_if_present(params, "avatar", :avatar, fn value ->
88 with %Plug.Upload{} <- value,
89 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
98 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
99 |> add_if_present(params, "header", :banner, fn value ->
100 with %Plug.Upload{} <- value,
101 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
108 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
110 with changeset <- User.update_changeset(user, user_params),
111 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
112 {:ok, user} <- User.update_and_set_cache(changeset) do
113 if original_user != user do
114 CommonAPI.update(user)
117 json(conn, AccountView.render("account.json", %{user: user, for: user}))
122 |> json(%{error: "Invalid request"})
126 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
127 account = AccountView.render("account.json", %{user: user, for: user})
131 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
132 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
135 |> render("short.json", %{app: app})
139 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
140 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
141 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
142 account = AccountView.render("account.json", %{user: user, for: for_user})
148 |> json(%{error: "Can't find user"})
152 @mastodon_api_level "2.5.0"
154 def masto_instance(conn, _params) do
155 instance = Config.get(:instance)
159 title: Keyword.get(instance, :name),
160 description: Keyword.get(instance, :description),
161 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
162 email: Keyword.get(instance, :email),
164 streaming_api: Pleroma.Web.Endpoint.websocket_url()
166 stats: Stats.get_stats(),
167 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
169 registrations: Pleroma.Config.get([:instance, :registrations_open]),
170 # Extra (not present in Mastodon):
171 max_toot_chars: Keyword.get(instance, :limit)
177 def peers(conn, _params) do
178 json(conn, Stats.get_peers())
181 defp mastodonized_emoji do
182 Pleroma.Emoji.get_all()
183 |> Enum.map(fn {shortcode, relative_url} ->
184 url = to_string(URI.merge(Web.base_url(), relative_url))
187 "shortcode" => shortcode,
189 "visible_in_picker" => true,
195 def custom_emojis(conn, _params) do
196 mastodon_emoji = mastodonized_emoji()
197 json(conn, mastodon_emoji)
200 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
203 |> Map.drop(["since_id", "max_id"])
206 last = List.last(activities)
207 first = List.first(activities)
213 {next_url, prev_url} =
217 Pleroma.Web.Endpoint,
220 Map.merge(params, %{max_id: min})
223 Pleroma.Web.Endpoint,
226 Map.merge(params, %{since_id: max})
232 Pleroma.Web.Endpoint,
234 Map.merge(params, %{max_id: min})
237 Pleroma.Web.Endpoint,
239 Map.merge(params, %{since_id: max})
245 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
251 def home_timeline(%{assigns: %{user: user}} = conn, params) do
254 |> Map.put("type", ["Create", "Announce"])
255 |> Map.put("blocking_user", user)
256 |> Map.put("muting_user", user)
257 |> Map.put("user", user)
260 [user.ap_id | user.following]
261 |> ActivityPub.fetch_activities(params)
262 |> ActivityPub.contain_timeline(user)
266 |> add_link_headers(:home_timeline, activities)
267 |> put_view(StatusView)
268 |> render("index.json", %{activities: activities, for: user, as: :activity})
271 def public_timeline(%{assigns: %{user: user}} = conn, params) do
272 local_only = params["local"] in [true, "True", "true", "1"]
276 |> Map.put("type", ["Create", "Announce"])
277 |> Map.put("local_only", local_only)
278 |> Map.put("blocking_user", user)
279 |> Map.put("muting_user", user)
280 |> ActivityPub.fetch_public_activities()
284 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
285 |> put_view(StatusView)
286 |> render("index.json", %{activities: activities, for: user, as: :activity})
289 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
290 with %User{} = user <- User.get_by_id(params["id"]) do
291 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
294 |> add_link_headers(:user_statuses, activities, params["id"])
295 |> put_view(StatusView)
296 |> render("index.json", %{
297 activities: activities,
304 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
307 |> Map.put("type", "Create")
308 |> Map.put("blocking_user", user)
309 |> Map.put("user", user)
310 |> Map.put(:visibility, "direct")
314 |> ActivityPub.fetch_activities_query(params)
318 |> add_link_headers(:dm_timeline, activities)
319 |> put_view(StatusView)
320 |> render("index.json", %{activities: activities, for: user, as: :activity})
323 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
324 with %Activity{} = activity <- Activity.get_by_id(id),
325 true <- Visibility.visible_for_user?(activity, user) do
327 |> put_view(StatusView)
328 |> try_render("status.json", %{activity: activity, for: user})
332 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
333 with %Activity{} = activity <- Activity.get_by_id(id),
335 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
336 "blocking_user" => user,
340 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
342 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
343 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
349 activities: grouped_activities[true] || [],
353 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
358 activities: grouped_activities[false] || [],
362 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
369 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
370 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
372 |> add_link_headers(:scheduled_statuses, scheduled_activities)
373 |> put_view(ScheduledActivityView)
374 |> render("index.json", %{scheduled_activities: scheduled_activities})
378 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
379 with %ScheduledActivity{} = scheduled_activity <-
380 ScheduledActivity.get(user, scheduled_activity_id) do
382 |> put_view(ScheduledActivityView)
383 |> render("show.json", %{scheduled_activity: scheduled_activity})
385 _ -> {:error, :not_found}
389 def update_scheduled_status(
390 %{assigns: %{user: user}} = conn,
391 %{"id" => scheduled_activity_id} = params
393 with {:ok, scheduled_activity} <-
394 ScheduledActivity.update(user, scheduled_activity_id, params) do
396 |> put_view(ScheduledActivityView)
397 |> render("show.json", %{scheduled_activity: scheduled_activity})
401 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
402 with {:ok, %ScheduledActivity{}} <- ScheduledActivity.delete(user, scheduled_activity_id) do
408 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
409 when length(media_ids) > 0 do
412 |> Map.put("status", ".")
414 post_status(conn, params)
417 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
420 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
423 case get_req_header(conn, "idempotency-key") do
425 _ -> Ecto.UUID.generate()
428 scheduled_at = params["scheduled_at"]
430 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
431 {:ok, scheduled_activity} =
432 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
433 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at})
437 |> put_view(ScheduledActivityView)
438 |> render("show.json", %{scheduled_activity: scheduled_activity})
440 params = Map.drop(params, ["scheduled_at"])
443 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
444 CommonAPI.post(user, params)
448 |> put_view(StatusView)
449 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
453 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
454 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
460 |> json(%{error: "Can't delete this post"})
464 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
465 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
467 |> put_view(StatusView)
468 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
472 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
473 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
474 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
476 |> put_view(StatusView)
477 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
481 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
482 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
483 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
485 |> put_view(StatusView)
486 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
490 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
491 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
492 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
494 |> put_view(StatusView)
495 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
499 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
500 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
502 |> put_view(StatusView)
503 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
507 |> put_resp_content_type("application/json")
508 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
512 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
513 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
515 |> put_view(StatusView)
516 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
520 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
521 with %Activity{} = activity <- Activity.get_by_id(id),
522 %User{} = user <- User.get_by_nickname(user.nickname),
523 true <- Visibility.visible_for_user?(activity, user),
524 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
526 |> put_view(StatusView)
527 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
531 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
532 with %Activity{} = activity <- Activity.get_by_id(id),
533 %User{} = user <- User.get_by_nickname(user.nickname),
534 true <- Visibility.visible_for_user?(activity, user),
535 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
537 |> put_view(StatusView)
538 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
542 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
543 activity = Activity.get_by_id(id)
545 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
547 |> put_view(StatusView)
548 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
552 |> put_resp_content_type("application/json")
553 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
557 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
558 activity = Activity.get_by_id(id)
560 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
562 |> put_view(StatusView)
563 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
567 def notifications(%{assigns: %{user: user}} = conn, params) do
568 notifications = MastodonAPI.get_notifications(user, params)
571 |> add_link_headers(:notifications, notifications)
572 |> put_view(NotificationView)
573 |> render("index.json", %{notifications: notifications, for: user})
576 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
577 with {:ok, notification} <- Notification.get(user, id) do
579 |> put_view(NotificationView)
580 |> render("show.json", %{notification: notification, for: user})
584 |> put_resp_content_type("application/json")
585 |> send_resp(403, Jason.encode!(%{"error" => reason}))
589 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
590 Notification.clear(user)
594 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
595 with {:ok, _notif} <- Notification.dismiss(user, id) do
600 |> put_resp_content_type("application/json")
601 |> send_resp(403, Jason.encode!(%{"error" => reason}))
605 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
607 q = from(u in User, where: u.id in ^id)
608 targets = Repo.all(q)
611 |> put_view(AccountView)
612 |> render("relationships.json", %{user: user, targets: targets})
615 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
616 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
618 def update_media(%{assigns: %{user: user}} = conn, data) do
619 with %Object{} = object <- Repo.get(Object, data["id"]),
620 true <- Object.authorize_mutation(object, user),
621 true <- is_binary(data["description"]),
622 description <- data["description"] do
623 new_data = %{object.data | "name" => description}
627 |> Object.change(%{data: new_data})
630 attachment_data = Map.put(new_data, "id", object.id)
633 |> put_view(StatusView)
634 |> render("attachment.json", %{attachment: attachment_data})
638 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
639 with {:ok, object} <-
642 actor: User.ap_id(user),
643 description: Map.get(data, "description")
645 attachment_data = Map.put(object.data, "id", object.id)
648 |> put_view(StatusView)
649 |> render("attachment.json", %{attachment: attachment_data})
653 def favourited_by(conn, %{"id" => id}) do
654 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
655 q = from(u in User, where: u.ap_id in ^likes)
659 |> put_view(AccountView)
660 |> render(AccountView, "accounts.json", %{users: users, as: :user})
666 def reblogged_by(conn, %{"id" => id}) do
667 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
668 q = from(u in User, where: u.ap_id in ^announces)
672 |> put_view(AccountView)
673 |> render("accounts.json", %{users: users, as: :user})
679 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
680 local_only = params["local"] in [true, "True", "true", "1"]
683 [params["tag"], params["any"]]
687 |> Enum.map(&String.downcase(&1))
692 |> Enum.map(&String.downcase(&1))
697 |> Enum.map(&String.downcase(&1))
701 |> Map.put("type", "Create")
702 |> Map.put("local_only", local_only)
703 |> Map.put("blocking_user", user)
704 |> Map.put("muting_user", user)
705 |> Map.put("tag", tags)
706 |> Map.put("tag_all", tag_all)
707 |> Map.put("tag_reject", tag_reject)
708 |> ActivityPub.fetch_public_activities()
712 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
713 |> put_view(StatusView)
714 |> render("index.json", %{activities: activities, for: user, as: :activity})
717 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
718 with %User{} = user <- User.get_by_id(id),
719 followers <- MastodonAPI.get_followers(user, params) do
722 for_user && user.id == for_user.id -> followers
723 user.info.hide_followers -> []
728 |> add_link_headers(:followers, followers, user)
729 |> put_view(AccountView)
730 |> render("accounts.json", %{users: followers, as: :user})
734 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
735 with %User{} = user <- User.get_by_id(id),
736 followers <- MastodonAPI.get_friends(user, params) do
739 for_user && user.id == for_user.id -> followers
740 user.info.hide_follows -> []
745 |> add_link_headers(:following, followers, user)
746 |> put_view(AccountView)
747 |> render("accounts.json", %{users: followers, as: :user})
751 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
752 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
754 |> put_view(AccountView)
755 |> render("accounts.json", %{users: follow_requests, as: :user})
759 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
760 with %User{} = follower <- User.get_by_id(id),
761 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
763 |> put_view(AccountView)
764 |> render("relationship.json", %{user: followed, target: follower})
768 |> put_resp_content_type("application/json")
769 |> send_resp(403, Jason.encode!(%{"error" => message}))
773 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
774 with %User{} = follower <- User.get_by_id(id),
775 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
777 |> put_view(AccountView)
778 |> render("relationship.json", %{user: followed, target: follower})
782 |> put_resp_content_type("application/json")
783 |> send_resp(403, Jason.encode!(%{"error" => message}))
787 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
788 with %User{} = followed <- User.get_by_id(id),
789 false <- User.following?(follower, followed),
790 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
792 |> put_view(AccountView)
793 |> render("relationship.json", %{user: follower, target: followed})
796 followed = User.get_cached_by_id(id)
799 case conn.params["reblogs"] do
800 true -> CommonAPI.show_reblogs(follower, followed)
801 false -> CommonAPI.hide_reblogs(follower, followed)
805 |> put_view(AccountView)
806 |> render("relationship.json", %{user: follower, target: followed})
810 |> put_resp_content_type("application/json")
811 |> send_resp(403, Jason.encode!(%{"error" => message}))
815 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
816 with %User{} = followed <- User.get_by_nickname(uri),
817 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
819 |> put_view(AccountView)
820 |> render("account.json", %{user: followed, for: follower})
824 |> put_resp_content_type("application/json")
825 |> send_resp(403, Jason.encode!(%{"error" => message}))
829 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
830 with %User{} = followed <- User.get_by_id(id),
831 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
833 |> put_view(AccountView)
834 |> render("relationship.json", %{user: follower, target: followed})
838 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
839 with %User{} = muted <- User.get_by_id(id),
840 {:ok, muter} <- User.mute(muter, muted) do
842 |> put_view(AccountView)
843 |> render("relationship.json", %{user: muter, target: muted})
847 |> put_resp_content_type("application/json")
848 |> send_resp(403, Jason.encode!(%{"error" => message}))
852 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
853 with %User{} = muted <- User.get_by_id(id),
854 {:ok, muter} <- User.unmute(muter, muted) do
856 |> put_view(AccountView)
857 |> render("relationship.json", %{user: muter, target: muted})
861 |> put_resp_content_type("application/json")
862 |> send_resp(403, Jason.encode!(%{"error" => message}))
866 def mutes(%{assigns: %{user: user}} = conn, _) do
867 with muted_accounts <- User.muted_users(user) do
868 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
873 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
874 with %User{} = blocked <- User.get_by_id(id),
875 {:ok, blocker} <- User.block(blocker, blocked),
876 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
878 |> put_view(AccountView)
879 |> render("relationship.json", %{user: blocker, target: blocked})
883 |> put_resp_content_type("application/json")
884 |> send_resp(403, Jason.encode!(%{"error" => message}))
888 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
889 with %User{} = blocked <- User.get_by_id(id),
890 {:ok, blocker} <- User.unblock(blocker, blocked),
891 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
893 |> put_view(AccountView)
894 |> render("relationship.json", %{user: blocker, target: blocked})
898 |> put_resp_content_type("application/json")
899 |> send_resp(403, Jason.encode!(%{"error" => message}))
903 def blocks(%{assigns: %{user: user}} = conn, _) do
904 with blocked_accounts <- User.blocked_users(user) do
905 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
910 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
911 json(conn, info.domain_blocks || [])
914 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
915 User.block_domain(blocker, domain)
919 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
920 User.unblock_domain(blocker, domain)
924 def status_search(user, query) do
926 if Regex.match?(~r/https?:/, query) do
927 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
928 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
929 true <- Visibility.visible_for_user?(activity, user) do
939 where: fragment("?->>'type' = 'Create'", a.data),
940 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
943 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
948 order_by: [desc: :id]
951 Repo.all(q) ++ fetched
954 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
955 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
957 statuses = status_search(user, query)
959 tags_path = Web.base_url() <> "/tag/"
965 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
966 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
967 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
970 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
972 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
979 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
980 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
982 statuses = status_search(user, query)
988 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
989 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
992 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
994 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1001 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1002 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1004 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1009 def favourites(%{assigns: %{user: user}} = conn, params) do
1012 |> Map.put("type", "Create")
1013 |> Map.put("favorited_by", user.ap_id)
1014 |> Map.put("blocking_user", user)
1017 ActivityPub.fetch_activities([], params)
1021 |> add_link_headers(:favourites, activities)
1022 |> put_view(StatusView)
1023 |> render("index.json", %{activities: activities, for: user, as: :activity})
1026 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1027 user = User.get_by_id(user.id)
1031 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1035 |> put_view(StatusView)
1036 |> render("index.json", %{activities: activities, for: user, as: :activity})
1039 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1040 lists = Pleroma.List.for_user(user, opts)
1041 res = ListView.render("lists.json", lists: lists)
1045 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1046 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1047 res = ListView.render("list.json", list: list)
1053 |> json(%{error: "Record not found"})
1057 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1058 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1059 res = ListView.render("lists.json", lists: lists)
1063 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1064 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1065 {:ok, _list} <- Pleroma.List.delete(list) do
1073 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1074 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1075 res = ListView.render("list.json", list: list)
1080 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1082 |> Enum.each(fn account_id ->
1083 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1084 %User{} = followed <- User.get_by_id(account_id) do
1085 Pleroma.List.follow(list, followed)
1092 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1094 |> Enum.each(fn account_id ->
1095 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1096 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1097 Pleroma.List.unfollow(list, followed)
1104 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1105 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1106 {:ok, users} = Pleroma.List.get_following(list) do
1108 |> put_view(AccountView)
1109 |> render("accounts.json", %{users: users, as: :user})
1113 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1114 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1115 {:ok, list} <- Pleroma.List.rename(list, title) do
1116 res = ListView.render("list.json", list: list)
1124 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1125 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1128 |> Map.put("type", "Create")
1129 |> Map.put("blocking_user", user)
1130 |> Map.put("muting_user", user)
1132 # we must filter the following list for the user to avoid leaking statuses the user
1133 # does not actually have permission to see (for more info, peruse security issue #270).
1136 |> Enum.filter(fn x -> x in user.following end)
1137 |> ActivityPub.fetch_activities_bounded(following, params)
1141 |> put_view(StatusView)
1142 |> render("index.json", %{activities: activities, for: user, as: :activity})
1147 |> json(%{error: "Error."})
1151 def index(%{assigns: %{user: user}} = conn, _params) do
1152 token = get_session(conn, :oauth_token)
1155 mastodon_emoji = mastodonized_emoji()
1157 limit = Config.get([:instance, :limit])
1160 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1162 flavour = get_user_flavour(user)
1167 streaming_api_base_url:
1168 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1169 access_token: token,
1171 domain: Pleroma.Web.Endpoint.host(),
1174 unfollow_modal: false,
1177 auto_play_gif: false,
1178 display_sensitive_media: false,
1179 reduce_motion: false,
1180 max_toot_chars: limit,
1181 mascot: "/images/pleroma-fox-tan-smol.png"
1184 delete_others_notice: present?(user.info.is_moderator),
1185 admin: present?(user.info.is_admin)
1189 default_privacy: user.info.default_scope,
1190 default_sensitive: false,
1191 allow_content_types: Config.get([:instance, :allowed_post_formats])
1193 media_attachments: %{
1194 accept_content_types: [
1210 user.info.settings ||
1240 push_subscription: nil,
1242 custom_emojis: mastodon_emoji,
1248 |> put_layout(false)
1249 |> put_view(MastodonView)
1250 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1253 |> put_session(:return_to, conn.request_path)
1254 |> redirect(to: "/web/login")
1258 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1259 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1261 with changeset <- Ecto.Changeset.change(user),
1262 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1263 {:ok, _user} <- User.update_and_set_cache(changeset) do
1268 |> put_resp_content_type("application/json")
1269 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1273 @supported_flavours ["glitch", "vanilla"]
1275 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1276 when flavour in @supported_flavours do
1277 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1279 with changeset <- Ecto.Changeset.change(user),
1280 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1281 {:ok, user} <- User.update_and_set_cache(changeset),
1282 flavour <- user.info.flavour do
1287 |> put_resp_content_type("application/json")
1288 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1292 def set_flavour(conn, _params) do
1295 |> json(%{error: "Unsupported flavour"})
1298 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1299 json(conn, get_user_flavour(user))
1302 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1306 defp get_user_flavour(_) do
1310 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1311 redirect(conn, to: local_mastodon_root_path(conn))
1314 @doc "Local Mastodon FE login init action"
1315 def login(conn, %{"code" => auth_token}) do
1316 with {:ok, app} <- get_or_make_app(),
1317 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1318 {:ok, token} <- Token.exchange_token(app, auth) do
1320 |> put_session(:oauth_token, token.token)
1321 |> redirect(to: local_mastodon_root_path(conn))
1325 @doc "Local Mastodon FE callback action"
1326 def login(conn, _) do
1327 with {:ok, app} <- get_or_make_app() do
1332 response_type: "code",
1333 client_id: app.client_id,
1335 scope: Enum.join(app.scopes, " ")
1338 redirect(conn, to: path)
1342 defp local_mastodon_root_path(conn) do
1343 case get_session(conn, :return_to) do
1345 mastodon_api_path(conn, :index, ["getting-started"])
1348 delete_session(conn, :return_to)
1353 defp get_or_make_app do
1354 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1355 scopes = ["read", "write", "follow", "push"]
1357 with %App{} = app <- Repo.get_by(App, find_attrs) do
1359 if app.scopes == scopes do
1363 |> Ecto.Changeset.change(%{scopes: scopes})
1371 App.register_changeset(
1373 Map.put(find_attrs, :scopes, scopes)
1380 def logout(conn, _) do
1383 |> redirect(to: "/")
1386 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1387 Logger.debug("Unimplemented, returning unmodified relationship")
1389 with %User{} = target <- User.get_by_id(id) do
1391 |> put_view(AccountView)
1392 |> render("relationship.json", %{user: user, target: target})
1396 def empty_array(conn, _) do
1397 Logger.debug("Unimplemented, returning an empty array")
1401 def empty_object(conn, _) do
1402 Logger.debug("Unimplemented, returning an empty object")
1406 def get_filters(%{assigns: %{user: user}} = conn, _) do
1407 filters = Filter.get_filters(user)
1408 res = FilterView.render("filters.json", filters: filters)
1413 %{assigns: %{user: user}} = conn,
1414 %{"phrase" => phrase, "context" => context} = params
1420 hide: Map.get(params, "irreversible", nil),
1421 whole_word: Map.get(params, "boolean", true)
1425 {:ok, response} = Filter.create(query)
1426 res = FilterView.render("filter.json", filter: response)
1430 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1431 filter = Filter.get(filter_id, user)
1432 res = FilterView.render("filter.json", filter: filter)
1437 %{assigns: %{user: user}} = conn,
1438 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1442 filter_id: filter_id,
1445 hide: Map.get(params, "irreversible", nil),
1446 whole_word: Map.get(params, "boolean", true)
1450 {:ok, response} = Filter.update(query)
1451 res = FilterView.render("filter.json", filter: response)
1455 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1458 filter_id: filter_id
1461 {:ok, _} = Filter.delete(query)
1467 def errors(conn, {:error, :not_found}) do
1470 |> json(%{error: "Record not found"})
1473 def errors(conn, _) do
1476 |> json("Something went wrong")
1479 def suggestions(%{assigns: %{user: user}} = conn, _) do
1480 suggestions = Config.get(:suggestions)
1482 if Keyword.get(suggestions, :enabled, false) do
1483 api = Keyword.get(suggestions, :third_party_engine, "")
1484 timeout = Keyword.get(suggestions, :timeout, 5000)
1485 limit = Keyword.get(suggestions, :limit, 23)
1487 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1489 user = user.nickname
1493 |> String.replace("{{host}}", host)
1494 |> String.replace("{{user}}", user)
1496 with {:ok, %{status: 200, body: body}} <-
1501 recv_timeout: timeout,
1505 {:ok, data} <- Jason.decode(body) do
1508 |> Enum.slice(0, limit)
1513 case User.get_or_fetch(x["acct"]) do
1520 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1523 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1529 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1536 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1537 with %Activity{} = activity <- Activity.get_by_id(status_id),
1538 true <- Visibility.visible_for_user?(activity, user) do
1542 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1552 def reports(%{assigns: %{user: user}} = conn, params) do
1553 case CommonAPI.report(user, params) do
1556 |> put_view(ReportView)
1557 |> try_render("report.json", %{activity: activity})
1561 |> put_status(:bad_request)
1562 |> json(%{error: err})
1566 def try_render(conn, target, params)
1567 when is_binary(target) do
1568 res = render(conn, target, params)
1573 |> json(%{error: "Can't display this activity"})
1579 def try_render(conn, _, _) do
1582 |> json(%{error: "Can't display this activity"})
1585 defp present?(nil), do: false
1586 defp present?(false), do: false
1587 defp present?(_), do: true