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.Conversation.Participation
13 alias Pleroma.Notification
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.ConversationView
26 alias Pleroma.Web.MastodonAPI.FilterView
27 alias Pleroma.Web.MastodonAPI.ListView
28 alias Pleroma.Web.MastodonAPI.MastodonAPI
29 alias Pleroma.Web.MastodonAPI.MastodonView
30 alias Pleroma.Web.MastodonAPI.NotificationView
31 alias Pleroma.Web.MastodonAPI.ReportView
32 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
33 alias Pleroma.Web.MastodonAPI.StatusView
34 alias Pleroma.Web.MediaProxy
35 alias Pleroma.Web.OAuth.App
36 alias Pleroma.Web.OAuth.Authorization
37 alias Pleroma.Web.OAuth.Token
39 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
44 @httpoison Application.get_env(:pleroma, :httpoison)
45 @local_mastodon_name "Mastodon-Local"
47 action_fallback(:errors)
49 def create_app(conn, params) do
50 scopes = oauth_scopes(params, ["read"])
54 |> Map.drop(["scope", "scopes"])
55 |> Map.put("scopes", scopes)
57 with cs <- App.register_changeset(%App{}, app_attrs),
58 false <- cs.changes[:client_name] == @local_mastodon_name,
59 {:ok, app} <- Repo.insert(cs) do
62 |> render("show.json", %{app: app})
71 value_function \\ fn x -> {:ok, x} end
73 if Map.has_key?(params, params_field) do
74 case value_function.(params[params_field]) do
75 {:ok, new_value} -> Map.put(map, map_field, new_value)
83 def update_credentials(%{assigns: %{user: user}} = conn, params) do
88 |> add_if_present(params, "display_name", :name)
89 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
90 |> add_if_present(params, "avatar", :avatar, fn value ->
91 with %Plug.Upload{} <- value,
92 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
101 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
102 |> add_if_present(params, "header", :banner, fn value ->
103 with %Plug.Upload{} <- value,
104 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
111 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
113 with changeset <- User.update_changeset(user, user_params),
114 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
115 {:ok, user} <- User.update_and_set_cache(changeset) do
116 if original_user != user do
117 CommonAPI.update(user)
120 json(conn, AccountView.render("account.json", %{user: user, for: user}))
125 |> json(%{error: "Invalid request"})
129 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
130 account = AccountView.render("account.json", %{user: user, for: user})
134 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
135 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
138 |> render("short.json", %{app: app})
142 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
143 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
144 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
145 account = AccountView.render("account.json", %{user: user, for: for_user})
151 |> json(%{error: "Can't find user"})
155 @mastodon_api_level "2.5.0"
157 def masto_instance(conn, _params) do
158 instance = Config.get(:instance)
162 title: Keyword.get(instance, :name),
163 description: Keyword.get(instance, :description),
164 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
165 email: Keyword.get(instance, :email),
167 streaming_api: Pleroma.Web.Endpoint.websocket_url()
169 stats: Stats.get_stats(),
170 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
172 registrations: Pleroma.Config.get([:instance, :registrations_open]),
173 # Extra (not present in Mastodon):
174 max_toot_chars: Keyword.get(instance, :limit)
180 def peers(conn, _params) do
181 json(conn, Stats.get_peers())
184 defp mastodonized_emoji do
185 Pleroma.Emoji.get_all()
186 |> Enum.map(fn {shortcode, relative_url, tags} ->
187 url = to_string(URI.merge(Web.base_url(), relative_url))
190 "shortcode" => shortcode,
192 "visible_in_picker" => true,
194 "tags" => String.split(tags, ",")
199 def custom_emojis(conn, _params) do
200 mastodon_emoji = mastodonized_emoji()
201 json(conn, mastodon_emoji)
204 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
207 |> Map.drop(["since_id", "max_id"])
210 last = List.last(activities)
211 first = List.first(activities)
217 {next_url, prev_url} =
221 Pleroma.Web.Endpoint,
224 Map.merge(params, %{max_id: min})
227 Pleroma.Web.Endpoint,
230 Map.merge(params, %{since_id: max})
236 Pleroma.Web.Endpoint,
238 Map.merge(params, %{max_id: min})
241 Pleroma.Web.Endpoint,
243 Map.merge(params, %{since_id: max})
249 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
255 def home_timeline(%{assigns: %{user: user}} = conn, params) do
258 |> Map.put("type", ["Create", "Announce"])
259 |> Map.put("blocking_user", user)
260 |> Map.put("muting_user", user)
261 |> Map.put("user", user)
264 [user.ap_id | user.following]
265 |> ActivityPub.fetch_activities(params)
266 |> ActivityPub.contain_timeline(user)
270 |> add_link_headers(:home_timeline, activities)
271 |> put_view(StatusView)
272 |> render("index.json", %{activities: activities, for: user, as: :activity})
275 def public_timeline(%{assigns: %{user: user}} = conn, params) do
276 local_only = params["local"] in [true, "True", "true", "1"]
280 |> Map.put("type", ["Create", "Announce"])
281 |> Map.put("local_only", local_only)
282 |> Map.put("blocking_user", user)
283 |> Map.put("muting_user", user)
284 |> ActivityPub.fetch_public_activities()
288 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
289 |> put_view(StatusView)
290 |> render("index.json", %{activities: activities, for: user, as: :activity})
293 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
294 with %User{} = user <- User.get_by_id(params["id"]) do
295 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
298 |> add_link_headers(:user_statuses, activities, params["id"])
299 |> put_view(StatusView)
300 |> render("index.json", %{
301 activities: activities,
308 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
311 |> Map.put("type", "Create")
312 |> Map.put("blocking_user", user)
313 |> Map.put("user", user)
314 |> Map.put(:visibility, "direct")
318 |> ActivityPub.fetch_activities_query(params)
322 |> add_link_headers(:dm_timeline, activities)
323 |> put_view(StatusView)
324 |> render("index.json", %{activities: activities, for: user, as: :activity})
327 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
328 with %Activity{} = activity <- Activity.get_by_id(id),
329 true <- Visibility.visible_for_user?(activity, user) do
331 |> put_view(StatusView)
332 |> try_render("status.json", %{activity: activity, for: user})
336 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
337 with %Activity{} = activity <- Activity.get_by_id(id),
339 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
340 "blocking_user" => user,
344 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
346 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
347 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
353 activities: grouped_activities[true] || [],
357 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
362 activities: grouped_activities[false] || [],
366 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
373 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
374 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
376 |> add_link_headers(:scheduled_statuses, scheduled_activities)
377 |> put_view(ScheduledActivityView)
378 |> render("index.json", %{scheduled_activities: scheduled_activities})
382 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
383 with %ScheduledActivity{} = scheduled_activity <-
384 ScheduledActivity.get(user, scheduled_activity_id) do
386 |> put_view(ScheduledActivityView)
387 |> render("show.json", %{scheduled_activity: scheduled_activity})
389 _ -> {:error, :not_found}
393 def update_scheduled_status(
394 %{assigns: %{user: user}} = conn,
395 %{"id" => scheduled_activity_id} = params
397 with %ScheduledActivity{} = scheduled_activity <-
398 ScheduledActivity.get(user, scheduled_activity_id),
399 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
401 |> put_view(ScheduledActivityView)
402 |> render("show.json", %{scheduled_activity: scheduled_activity})
404 nil -> {:error, :not_found}
409 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
410 with %ScheduledActivity{} = scheduled_activity <-
411 ScheduledActivity.get(user, scheduled_activity_id),
412 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
414 |> put_view(ScheduledActivityView)
415 |> render("show.json", %{scheduled_activity: scheduled_activity})
417 nil -> {:error, :not_found}
422 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
423 when length(media_ids) > 0 do
426 |> Map.put("status", ".")
428 post_status(conn, params)
431 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
434 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
437 case get_req_header(conn, "idempotency-key") do
439 _ -> Ecto.UUID.generate()
442 scheduled_at = params["scheduled_at"]
444 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
445 with {:ok, scheduled_activity} <-
446 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
448 |> put_view(ScheduledActivityView)
449 |> render("show.json", %{scheduled_activity: scheduled_activity})
452 params = Map.drop(params, ["scheduled_at"])
455 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
456 CommonAPI.post(user, params)
460 |> put_view(StatusView)
461 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
465 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
466 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
472 |> json(%{error: "Can't delete this post"})
476 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
477 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
479 |> put_view(StatusView)
480 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
484 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
485 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
486 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
488 |> put_view(StatusView)
489 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
493 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
494 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
495 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
497 |> put_view(StatusView)
498 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
502 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
503 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
504 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
506 |> put_view(StatusView)
507 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
511 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
512 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
514 |> put_view(StatusView)
515 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
519 |> put_resp_content_type("application/json")
520 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
524 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
527 |> put_view(StatusView)
528 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
532 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
533 with %Activity{} = activity <- Activity.get_by_id(id),
534 %User{} = user <- User.get_by_nickname(user.nickname),
535 true <- Visibility.visible_for_user?(activity, user),
536 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
538 |> put_view(StatusView)
539 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
543 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
544 with %Activity{} = activity <- Activity.get_by_id(id),
545 %User{} = user <- User.get_by_nickname(user.nickname),
546 true <- Visibility.visible_for_user?(activity, user),
547 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
549 |> put_view(StatusView)
550 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
554 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
555 activity = Activity.get_by_id(id)
557 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
559 |> put_view(StatusView)
560 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
564 |> put_resp_content_type("application/json")
565 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
569 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
570 activity = Activity.get_by_id(id)
572 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
574 |> put_view(StatusView)
575 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
579 def notifications(%{assigns: %{user: user}} = conn, params) do
580 notifications = MastodonAPI.get_notifications(user, params)
583 |> add_link_headers(:notifications, notifications)
584 |> put_view(NotificationView)
585 |> render("index.json", %{notifications: notifications, for: user})
588 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
589 with {:ok, notification} <- Notification.get(user, id) do
591 |> put_view(NotificationView)
592 |> render("show.json", %{notification: notification, for: user})
596 |> put_resp_content_type("application/json")
597 |> send_resp(403, Jason.encode!(%{"error" => reason}))
601 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
602 Notification.clear(user)
606 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
607 with {:ok, _notif} <- Notification.dismiss(user, id) do
612 |> put_resp_content_type("application/json")
613 |> send_resp(403, Jason.encode!(%{"error" => reason}))
617 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
619 q = from(u in User, where: u.id in ^id)
620 targets = Repo.all(q)
623 |> put_view(AccountView)
624 |> render("relationships.json", %{user: user, targets: targets})
627 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
628 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
630 def update_media(%{assigns: %{user: user}} = conn, data) do
631 with %Object{} = object <- Repo.get(Object, data["id"]),
632 true <- Object.authorize_mutation(object, user),
633 true <- is_binary(data["description"]),
634 description <- data["description"] do
635 new_data = %{object.data | "name" => description}
639 |> Object.change(%{data: new_data})
642 attachment_data = Map.put(new_data, "id", object.id)
645 |> put_view(StatusView)
646 |> render("attachment.json", %{attachment: attachment_data})
650 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
651 with {:ok, object} <-
654 actor: User.ap_id(user),
655 description: Map.get(data, "description")
657 attachment_data = Map.put(object.data, "id", object.id)
660 |> put_view(StatusView)
661 |> render("attachment.json", %{attachment: attachment_data})
665 def favourited_by(conn, %{"id" => id}) do
666 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
667 q = from(u in User, where: u.ap_id in ^likes)
671 |> put_view(AccountView)
672 |> render(AccountView, "accounts.json", %{users: users, as: :user})
678 def reblogged_by(conn, %{"id" => id}) do
679 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
680 q = from(u in User, where: u.ap_id in ^announces)
684 |> put_view(AccountView)
685 |> render("accounts.json", %{users: users, as: :user})
691 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
692 local_only = params["local"] in [true, "True", "true", "1"]
695 [params["tag"], params["any"]]
699 |> Enum.map(&String.downcase(&1))
704 |> Enum.map(&String.downcase(&1))
709 |> Enum.map(&String.downcase(&1))
713 |> Map.put("type", "Create")
714 |> Map.put("local_only", local_only)
715 |> Map.put("blocking_user", user)
716 |> Map.put("muting_user", user)
717 |> Map.put("tag", tags)
718 |> Map.put("tag_all", tag_all)
719 |> Map.put("tag_reject", tag_reject)
720 |> ActivityPub.fetch_public_activities()
724 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
725 |> put_view(StatusView)
726 |> render("index.json", %{activities: activities, for: user, as: :activity})
729 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
730 with %User{} = user <- User.get_by_id(id),
731 followers <- MastodonAPI.get_followers(user, params) do
734 for_user && user.id == for_user.id -> followers
735 user.info.hide_followers -> []
740 |> add_link_headers(:followers, followers, user)
741 |> put_view(AccountView)
742 |> render("accounts.json", %{users: followers, as: :user})
746 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
747 with %User{} = user <- User.get_by_id(id),
748 followers <- MastodonAPI.get_friends(user, params) do
751 for_user && user.id == for_user.id -> followers
752 user.info.hide_follows -> []
757 |> add_link_headers(:following, followers, user)
758 |> put_view(AccountView)
759 |> render("accounts.json", %{users: followers, as: :user})
763 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
764 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
766 |> put_view(AccountView)
767 |> render("accounts.json", %{users: follow_requests, as: :user})
771 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
772 with %User{} = follower <- User.get_by_id(id),
773 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
775 |> put_view(AccountView)
776 |> render("relationship.json", %{user: followed, target: follower})
780 |> put_resp_content_type("application/json")
781 |> send_resp(403, Jason.encode!(%{"error" => message}))
785 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
786 with %User{} = follower <- User.get_by_id(id),
787 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
789 |> put_view(AccountView)
790 |> render("relationship.json", %{user: followed, target: follower})
794 |> put_resp_content_type("application/json")
795 |> send_resp(403, Jason.encode!(%{"error" => message}))
799 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
800 with %User{} = followed <- User.get_by_id(id),
801 false <- User.following?(follower, followed),
802 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
804 |> put_view(AccountView)
805 |> render("relationship.json", %{user: follower, target: followed})
808 followed = User.get_cached_by_id(id)
811 case conn.params["reblogs"] do
812 true -> CommonAPI.show_reblogs(follower, followed)
813 false -> CommonAPI.hide_reblogs(follower, followed)
817 |> put_view(AccountView)
818 |> render("relationship.json", %{user: follower, target: followed})
822 |> put_resp_content_type("application/json")
823 |> send_resp(403, Jason.encode!(%{"error" => message}))
827 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
828 with %User{} = followed <- User.get_by_nickname(uri),
829 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
831 |> put_view(AccountView)
832 |> render("account.json", %{user: followed, for: follower})
836 |> put_resp_content_type("application/json")
837 |> send_resp(403, Jason.encode!(%{"error" => message}))
841 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
842 with %User{} = followed <- User.get_by_id(id),
843 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
845 |> put_view(AccountView)
846 |> render("relationship.json", %{user: follower, target: followed})
850 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
851 with %User{} = muted <- User.get_by_id(id),
852 {:ok, muter} <- User.mute(muter, muted) do
854 |> put_view(AccountView)
855 |> render("relationship.json", %{user: muter, target: muted})
859 |> put_resp_content_type("application/json")
860 |> send_resp(403, Jason.encode!(%{"error" => message}))
864 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
865 with %User{} = muted <- User.get_by_id(id),
866 {:ok, muter} <- User.unmute(muter, muted) do
868 |> put_view(AccountView)
869 |> render("relationship.json", %{user: muter, target: muted})
873 |> put_resp_content_type("application/json")
874 |> send_resp(403, Jason.encode!(%{"error" => message}))
878 def mutes(%{assigns: %{user: user}} = conn, _) do
879 with muted_accounts <- User.muted_users(user) do
880 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
885 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
886 with %User{} = blocked <- User.get_by_id(id),
887 {:ok, blocker} <- User.block(blocker, blocked),
888 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
890 |> put_view(AccountView)
891 |> render("relationship.json", %{user: blocker, target: blocked})
895 |> put_resp_content_type("application/json")
896 |> send_resp(403, Jason.encode!(%{"error" => message}))
900 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
901 with %User{} = blocked <- User.get_by_id(id),
902 {:ok, blocker} <- User.unblock(blocker, blocked),
903 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
905 |> put_view(AccountView)
906 |> render("relationship.json", %{user: blocker, target: blocked})
910 |> put_resp_content_type("application/json")
911 |> send_resp(403, Jason.encode!(%{"error" => message}))
915 def blocks(%{assigns: %{user: user}} = conn, _) do
916 with blocked_accounts <- User.blocked_users(user) do
917 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
922 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
923 json(conn, info.domain_blocks || [])
926 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
927 User.block_domain(blocker, domain)
931 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
932 User.unblock_domain(blocker, domain)
936 def status_search(user, query) do
938 if Regex.match?(~r/https?:/, query) do
939 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
940 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
941 true <- Visibility.visible_for_user?(activity, user) do
951 where: fragment("?->>'type' = 'Create'", a.data),
952 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
955 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
960 order_by: [desc: :id]
963 Repo.all(q) ++ fetched
966 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
967 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
969 statuses = status_search(user, query)
971 tags_path = Web.base_url() <> "/tag/"
977 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
978 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
979 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
982 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
984 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
991 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
992 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
994 statuses = status_search(user, query)
1000 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1001 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1004 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1006 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1013 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1014 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1016 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1021 def favourites(%{assigns: %{user: user}} = conn, params) do
1024 |> Map.put("type", "Create")
1025 |> Map.put("favorited_by", user.ap_id)
1026 |> Map.put("blocking_user", user)
1029 ActivityPub.fetch_activities([], params)
1033 |> add_link_headers(:favourites, activities)
1034 |> put_view(StatusView)
1035 |> render("index.json", %{activities: activities, for: user, as: :activity})
1038 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1039 user = User.get_by_id(user.id)
1043 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1047 |> put_view(StatusView)
1048 |> render("index.json", %{activities: activities, for: user, as: :activity})
1051 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1052 lists = Pleroma.List.for_user(user, opts)
1053 res = ListView.render("lists.json", lists: lists)
1057 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1058 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1059 res = ListView.render("list.json", list: list)
1065 |> json(%{error: "Record not found"})
1069 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1070 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1071 res = ListView.render("lists.json", lists: lists)
1075 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1076 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1077 {:ok, _list} <- Pleroma.List.delete(list) do
1085 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1086 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1087 res = ListView.render("list.json", list: list)
1092 def add_to_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 <- User.get_by_id(account_id) do
1097 Pleroma.List.follow(list, followed)
1104 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1106 |> Enum.each(fn account_id ->
1107 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1108 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1109 Pleroma.List.unfollow(list, followed)
1116 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1117 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1118 {:ok, users} = Pleroma.List.get_following(list) do
1120 |> put_view(AccountView)
1121 |> render("accounts.json", %{users: users, as: :user})
1125 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1126 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1127 {:ok, list} <- Pleroma.List.rename(list, title) do
1128 res = ListView.render("list.json", list: list)
1136 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1137 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1140 |> Map.put("type", "Create")
1141 |> Map.put("blocking_user", user)
1142 |> Map.put("muting_user", user)
1144 # we must filter the following list for the user to avoid leaking statuses the user
1145 # does not actually have permission to see (for more info, peruse security issue #270).
1148 |> Enum.filter(fn x -> x in user.following end)
1149 |> ActivityPub.fetch_activities_bounded(following, params)
1153 |> put_view(StatusView)
1154 |> render("index.json", %{activities: activities, for: user, as: :activity})
1159 |> json(%{error: "Error."})
1163 def index(%{assigns: %{user: user}} = conn, _params) do
1164 token = get_session(conn, :oauth_token)
1167 mastodon_emoji = mastodonized_emoji()
1169 limit = Config.get([:instance, :limit])
1172 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1174 flavour = get_user_flavour(user)
1179 streaming_api_base_url:
1180 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1181 access_token: token,
1183 domain: Pleroma.Web.Endpoint.host(),
1186 unfollow_modal: false,
1189 auto_play_gif: false,
1190 display_sensitive_media: false,
1191 reduce_motion: false,
1192 max_toot_chars: limit,
1193 mascot: "/images/pleroma-fox-tan-smol.png"
1196 delete_others_notice: present?(user.info.is_moderator),
1197 admin: present?(user.info.is_admin)
1201 default_privacy: user.info.default_scope,
1202 default_sensitive: false,
1203 allow_content_types: Config.get([:instance, :allowed_post_formats])
1205 media_attachments: %{
1206 accept_content_types: [
1222 user.info.settings ||
1252 push_subscription: nil,
1254 custom_emojis: mastodon_emoji,
1260 |> put_layout(false)
1261 |> put_view(MastodonView)
1262 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1265 |> put_session(:return_to, conn.request_path)
1266 |> redirect(to: "/web/login")
1270 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1271 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1273 with changeset <- Ecto.Changeset.change(user),
1274 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1275 {:ok, _user} <- User.update_and_set_cache(changeset) do
1280 |> put_resp_content_type("application/json")
1281 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1285 @supported_flavours ["glitch", "vanilla"]
1287 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1288 when flavour in @supported_flavours do
1289 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1291 with changeset <- Ecto.Changeset.change(user),
1292 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1293 {:ok, user} <- User.update_and_set_cache(changeset),
1294 flavour <- user.info.flavour do
1299 |> put_resp_content_type("application/json")
1300 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1304 def set_flavour(conn, _params) do
1307 |> json(%{error: "Unsupported flavour"})
1310 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1311 json(conn, get_user_flavour(user))
1314 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1318 defp get_user_flavour(_) do
1322 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1323 redirect(conn, to: local_mastodon_root_path(conn))
1326 @doc "Local Mastodon FE login init action"
1327 def login(conn, %{"code" => auth_token}) do
1328 with {:ok, app} <- get_or_make_app(),
1329 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1330 {:ok, token} <- Token.exchange_token(app, auth) do
1332 |> put_session(:oauth_token, token.token)
1333 |> redirect(to: local_mastodon_root_path(conn))
1337 @doc "Local Mastodon FE callback action"
1338 def login(conn, _) do
1339 with {:ok, app} <- get_or_make_app() do
1344 response_type: "code",
1345 client_id: app.client_id,
1347 scope: Enum.join(app.scopes, " ")
1350 redirect(conn, to: path)
1354 defp local_mastodon_root_path(conn) do
1355 case get_session(conn, :return_to) do
1357 mastodon_api_path(conn, :index, ["getting-started"])
1360 delete_session(conn, :return_to)
1365 defp get_or_make_app do
1366 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1367 scopes = ["read", "write", "follow", "push"]
1369 with %App{} = app <- Repo.get_by(App, find_attrs) do
1371 if app.scopes == scopes do
1375 |> Ecto.Changeset.change(%{scopes: scopes})
1383 App.register_changeset(
1385 Map.put(find_attrs, :scopes, scopes)
1392 def logout(conn, _) do
1395 |> redirect(to: "/")
1398 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1399 Logger.debug("Unimplemented, returning unmodified relationship")
1401 with %User{} = target <- User.get_by_id(id) do
1403 |> put_view(AccountView)
1404 |> render("relationship.json", %{user: user, target: target})
1408 def empty_array(conn, _) do
1409 Logger.debug("Unimplemented, returning an empty array")
1413 def empty_object(conn, _) do
1414 Logger.debug("Unimplemented, returning an empty object")
1418 def get_filters(%{assigns: %{user: user}} = conn, _) do
1419 filters = Filter.get_filters(user)
1420 res = FilterView.render("filters.json", filters: filters)
1425 %{assigns: %{user: user}} = conn,
1426 %{"phrase" => phrase, "context" => context} = params
1432 hide: Map.get(params, "irreversible", nil),
1433 whole_word: Map.get(params, "boolean", true)
1437 {:ok, response} = Filter.create(query)
1438 res = FilterView.render("filter.json", filter: response)
1442 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1443 filter = Filter.get(filter_id, user)
1444 res = FilterView.render("filter.json", filter: filter)
1449 %{assigns: %{user: user}} = conn,
1450 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1454 filter_id: filter_id,
1457 hide: Map.get(params, "irreversible", nil),
1458 whole_word: Map.get(params, "boolean", true)
1462 {:ok, response} = Filter.update(query)
1463 res = FilterView.render("filter.json", filter: response)
1467 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1470 filter_id: filter_id
1473 {:ok, _} = Filter.delete(query)
1479 def errors(conn, {:error, %Changeset{} = changeset}) do
1482 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1483 |> Enum.map_join(", ", fn {_k, v} -> v end)
1487 |> json(%{error: error_message})
1490 def errors(conn, {:error, :not_found}) do
1493 |> json(%{error: "Record not found"})
1496 def errors(conn, _) do
1499 |> json("Something went wrong")
1502 def suggestions(%{assigns: %{user: user}} = conn, _) do
1503 suggestions = Config.get(:suggestions)
1505 if Keyword.get(suggestions, :enabled, false) do
1506 api = Keyword.get(suggestions, :third_party_engine, "")
1507 timeout = Keyword.get(suggestions, :timeout, 5000)
1508 limit = Keyword.get(suggestions, :limit, 23)
1510 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1512 user = user.nickname
1516 |> String.replace("{{host}}", host)
1517 |> String.replace("{{user}}", user)
1519 with {:ok, %{status: 200, body: body}} <-
1524 recv_timeout: timeout,
1528 {:ok, data} <- Jason.decode(body) do
1531 |> Enum.slice(0, limit)
1536 case User.get_or_fetch(x["acct"]) do
1543 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1546 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1552 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1559 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1560 with %Activity{} = activity <- Activity.get_by_id(status_id),
1561 true <- Visibility.visible_for_user?(activity, user) do
1565 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1575 def reports(%{assigns: %{user: user}} = conn, params) do
1576 case CommonAPI.report(user, params) do
1579 |> put_view(ReportView)
1580 |> try_render("report.json", %{activity: activity})
1584 |> put_status(:bad_request)
1585 |> json(%{error: err})
1589 def conversations(%{assigns: %{user: user}} = conn, params) do
1590 participations = Participation.for_user_with_last_activity_id(user, params)
1593 Enum.map(participations, fn participation ->
1594 ConversationView.render("participation.json", %{participation: participation, user: user})
1598 |> add_link_headers(:conversations, participations)
1599 |> json(conversations)
1602 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1603 with %Participation{} = participation <-
1604 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1605 {:ok, participation} <- Participation.mark_as_read(participation) do
1606 participation_view =
1607 ConversationView.render("participation.json", %{participation: participation, user: user})
1610 |> json(participation_view)
1614 def try_render(conn, target, params)
1615 when is_binary(target) do
1616 res = render(conn, target, params)
1621 |> json(%{error: "Can't display this activity"})
1627 def try_render(conn, _, _) do
1630 |> json(%{error: "Can't display this activity"})
1633 defp present?(nil), do: false
1634 defp present?(false), do: false
1635 defp present?(_), do: true