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.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"])
209 last = List.last(activities)
210 first = List.first(activities)
216 {next_url, prev_url} =
220 Pleroma.Web.Endpoint,
223 Map.merge(params, %{max_id: min})
226 Pleroma.Web.Endpoint,
229 Map.merge(params, %{since_id: max})
235 Pleroma.Web.Endpoint,
237 Map.merge(params, %{max_id: min})
240 Pleroma.Web.Endpoint,
242 Map.merge(params, %{since_id: max})
248 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
254 def home_timeline(%{assigns: %{user: user}} = conn, params) do
257 |> Map.put("type", ["Create", "Announce"])
258 |> Map.put("blocking_user", user)
259 |> Map.put("muting_user", user)
260 |> Map.put("user", user)
263 [user.ap_id | user.following]
264 |> ActivityPub.fetch_activities(params)
265 |> ActivityPub.contain_timeline(user)
269 |> add_link_headers(:home_timeline, activities)
270 |> put_view(StatusView)
271 |> render("index.json", %{activities: activities, for: user, as: :activity})
274 def public_timeline(%{assigns: %{user: user}} = conn, params) do
275 local_only = params["local"] in [true, "True", "true", "1"]
279 |> Map.put("type", ["Create", "Announce"])
280 |> Map.put("local_only", local_only)
281 |> Map.put("blocking_user", user)
282 |> Map.put("muting_user", user)
283 |> ActivityPub.fetch_public_activities()
287 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
288 |> put_view(StatusView)
289 |> render("index.json", %{activities: activities, for: user, as: :activity})
292 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
293 with %User{} = user <- User.get_by_id(params["id"]) do
294 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
297 |> add_link_headers(:user_statuses, activities, params["id"])
298 |> put_view(StatusView)
299 |> render("index.json", %{
300 activities: activities,
307 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
310 |> Map.put("type", "Create")
311 |> Map.put("blocking_user", user)
312 |> Map.put("user", user)
313 |> Map.put(:visibility, "direct")
317 |> ActivityPub.fetch_activities_query(params)
321 |> add_link_headers(:dm_timeline, activities)
322 |> put_view(StatusView)
323 |> render("index.json", %{activities: activities, for: user, as: :activity})
326 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
327 with %Activity{} = activity <- Activity.get_by_id(id),
328 true <- Visibility.visible_for_user?(activity, user) do
330 |> put_view(StatusView)
331 |> try_render("status.json", %{activity: activity, for: user})
335 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
336 with %Activity{} = activity <- Activity.get_by_id(id),
338 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
339 "blocking_user" => user,
343 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
345 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
346 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
352 activities: grouped_activities[true] || [],
356 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
361 activities: grouped_activities[false] || [],
365 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
372 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
373 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
375 |> add_link_headers(:scheduled_statuses, scheduled_activities)
376 |> put_view(ScheduledActivityView)
377 |> render("index.json", %{scheduled_activities: scheduled_activities})
381 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
382 with %ScheduledActivity{} = scheduled_activity <-
383 ScheduledActivity.get(user, scheduled_activity_id) do
385 |> put_view(ScheduledActivityView)
386 |> render("show.json", %{scheduled_activity: scheduled_activity})
388 _ -> {:error, :not_found}
392 def update_scheduled_status(
393 %{assigns: %{user: user}} = conn,
394 %{"id" => scheduled_activity_id} = params
396 with %ScheduledActivity{} = scheduled_activity <-
397 ScheduledActivity.get(user, scheduled_activity_id),
398 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
400 |> put_view(ScheduledActivityView)
401 |> render("show.json", %{scheduled_activity: scheduled_activity})
403 nil -> {:error, :not_found}
408 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
409 with %ScheduledActivity{} = scheduled_activity <-
410 ScheduledActivity.get(user, scheduled_activity_id),
411 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
413 |> put_view(ScheduledActivityView)
414 |> render("show.json", %{scheduled_activity: scheduled_activity})
416 nil -> {:error, :not_found}
421 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
422 when length(media_ids) > 0 do
425 |> Map.put("status", ".")
427 post_status(conn, params)
430 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
433 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
436 case get_req_header(conn, "idempotency-key") do
438 _ -> Ecto.UUID.generate()
441 scheduled_at = params["scheduled_at"]
443 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
444 with {:ok, scheduled_activity} <-
445 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
447 |> put_view(ScheduledActivityView)
448 |> render("show.json", %{scheduled_activity: scheduled_activity})
451 params = Map.drop(params, ["scheduled_at"])
454 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
455 CommonAPI.post(user, params)
459 |> put_view(StatusView)
460 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
464 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
465 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
471 |> json(%{error: "Can't delete this post"})
475 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
476 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
478 |> put_view(StatusView)
479 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
483 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
484 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
485 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
487 |> put_view(StatusView)
488 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
492 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
493 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
494 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
496 |> put_view(StatusView)
497 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
501 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
502 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
503 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
505 |> put_view(StatusView)
506 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
510 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
511 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
513 |> put_view(StatusView)
514 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
518 |> put_resp_content_type("application/json")
519 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
523 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
524 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
526 |> put_view(StatusView)
527 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
531 def bookmark_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.bookmark(user, activity.data["object"]["id"]) do
537 |> put_view(StatusView)
538 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
542 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
543 with %Activity{} = activity <- Activity.get_by_id(id),
544 %User{} = user <- User.get_by_nickname(user.nickname),
545 true <- Visibility.visible_for_user?(activity, user),
546 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
548 |> put_view(StatusView)
549 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
553 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
554 activity = Activity.get_by_id(id)
556 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
558 |> put_view(StatusView)
559 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
563 |> put_resp_content_type("application/json")
564 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
568 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
569 activity = Activity.get_by_id(id)
571 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
573 |> put_view(StatusView)
574 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
578 def notifications(%{assigns: %{user: user}} = conn, params) do
579 notifications = MastodonAPI.get_notifications(user, params)
582 |> add_link_headers(:notifications, notifications)
583 |> put_view(NotificationView)
584 |> render("index.json", %{notifications: notifications, for: user})
587 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
588 with {:ok, notification} <- Notification.get(user, id) do
590 |> put_view(NotificationView)
591 |> render("show.json", %{notification: notification, for: user})
595 |> put_resp_content_type("application/json")
596 |> send_resp(403, Jason.encode!(%{"error" => reason}))
600 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
601 Notification.clear(user)
605 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
606 with {:ok, _notif} <- Notification.dismiss(user, id) do
611 |> put_resp_content_type("application/json")
612 |> send_resp(403, Jason.encode!(%{"error" => reason}))
616 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
618 q = from(u in User, where: u.id in ^id)
619 targets = Repo.all(q)
622 |> put_view(AccountView)
623 |> render("relationships.json", %{user: user, targets: targets})
626 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
627 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
629 def update_media(%{assigns: %{user: user}} = conn, data) do
630 with %Object{} = object <- Repo.get(Object, data["id"]),
631 true <- Object.authorize_mutation(object, user),
632 true <- is_binary(data["description"]),
633 description <- data["description"] do
634 new_data = %{object.data | "name" => description}
638 |> Object.change(%{data: new_data})
641 attachment_data = Map.put(new_data, "id", object.id)
644 |> put_view(StatusView)
645 |> render("attachment.json", %{attachment: attachment_data})
649 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
650 with {:ok, object} <-
653 actor: User.ap_id(user),
654 description: Map.get(data, "description")
656 attachment_data = Map.put(object.data, "id", object.id)
659 |> put_view(StatusView)
660 |> render("attachment.json", %{attachment: attachment_data})
664 def favourited_by(conn, %{"id" => id}) do
665 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
666 q = from(u in User, where: u.ap_id in ^likes)
670 |> put_view(AccountView)
671 |> render(AccountView, "accounts.json", %{users: users, as: :user})
677 def reblogged_by(conn, %{"id" => id}) do
678 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
679 q = from(u in User, where: u.ap_id in ^announces)
683 |> put_view(AccountView)
684 |> render("accounts.json", %{users: users, as: :user})
690 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
691 local_only = params["local"] in [true, "True", "true", "1"]
694 [params["tag"], params["any"]]
698 |> Enum.map(&String.downcase(&1))
703 |> Enum.map(&String.downcase(&1))
708 |> Enum.map(&String.downcase(&1))
712 |> Map.put("type", "Create")
713 |> Map.put("local_only", local_only)
714 |> Map.put("blocking_user", user)
715 |> Map.put("muting_user", user)
716 |> Map.put("tag", tags)
717 |> Map.put("tag_all", tag_all)
718 |> Map.put("tag_reject", tag_reject)
719 |> ActivityPub.fetch_public_activities()
723 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
724 |> put_view(StatusView)
725 |> render("index.json", %{activities: activities, for: user, as: :activity})
728 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
729 with %User{} = user <- User.get_by_id(id),
730 followers <- MastodonAPI.get_followers(user, params) do
733 for_user && user.id == for_user.id -> followers
734 user.info.hide_followers -> []
739 |> add_link_headers(:followers, followers, user)
740 |> put_view(AccountView)
741 |> render("accounts.json", %{users: followers, as: :user})
745 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
746 with %User{} = user <- User.get_by_id(id),
747 followers <- MastodonAPI.get_friends(user, params) do
750 for_user && user.id == for_user.id -> followers
751 user.info.hide_follows -> []
756 |> add_link_headers(:following, followers, user)
757 |> put_view(AccountView)
758 |> render("accounts.json", %{users: followers, as: :user})
762 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
763 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
765 |> put_view(AccountView)
766 |> render("accounts.json", %{users: follow_requests, as: :user})
770 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
771 with %User{} = follower <- User.get_by_id(id),
772 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
774 |> put_view(AccountView)
775 |> render("relationship.json", %{user: followed, target: follower})
779 |> put_resp_content_type("application/json")
780 |> send_resp(403, Jason.encode!(%{"error" => message}))
784 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
785 with %User{} = follower <- User.get_by_id(id),
786 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
788 |> put_view(AccountView)
789 |> render("relationship.json", %{user: followed, target: follower})
793 |> put_resp_content_type("application/json")
794 |> send_resp(403, Jason.encode!(%{"error" => message}))
798 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
799 with %User{} = followed <- User.get_by_id(id),
800 false <- User.following?(follower, followed),
801 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
803 |> put_view(AccountView)
804 |> render("relationship.json", %{user: follower, target: followed})
807 followed = User.get_cached_by_id(id)
810 case conn.params["reblogs"] do
811 true -> CommonAPI.show_reblogs(follower, followed)
812 false -> CommonAPI.hide_reblogs(follower, followed)
816 |> put_view(AccountView)
817 |> render("relationship.json", %{user: follower, target: followed})
821 |> put_resp_content_type("application/json")
822 |> send_resp(403, Jason.encode!(%{"error" => message}))
826 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
827 with %User{} = followed <- User.get_by_nickname(uri),
828 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
830 |> put_view(AccountView)
831 |> render("account.json", %{user: followed, for: follower})
835 |> put_resp_content_type("application/json")
836 |> send_resp(403, Jason.encode!(%{"error" => message}))
840 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
841 with %User{} = followed <- User.get_by_id(id),
842 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
844 |> put_view(AccountView)
845 |> render("relationship.json", %{user: follower, target: followed})
849 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
850 with %User{} = muted <- User.get_by_id(id),
851 {:ok, muter} <- User.mute(muter, muted) do
853 |> put_view(AccountView)
854 |> render("relationship.json", %{user: muter, target: muted})
858 |> put_resp_content_type("application/json")
859 |> send_resp(403, Jason.encode!(%{"error" => message}))
863 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
864 with %User{} = muted <- User.get_by_id(id),
865 {:ok, muter} <- User.unmute(muter, muted) do
867 |> put_view(AccountView)
868 |> render("relationship.json", %{user: muter, target: muted})
872 |> put_resp_content_type("application/json")
873 |> send_resp(403, Jason.encode!(%{"error" => message}))
877 def mutes(%{assigns: %{user: user}} = conn, _) do
878 with muted_accounts <- User.muted_users(user) do
879 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
884 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
885 with %User{} = blocked <- User.get_by_id(id),
886 {:ok, blocker} <- User.block(blocker, blocked),
887 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
889 |> put_view(AccountView)
890 |> render("relationship.json", %{user: blocker, target: blocked})
894 |> put_resp_content_type("application/json")
895 |> send_resp(403, Jason.encode!(%{"error" => message}))
899 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
900 with %User{} = blocked <- User.get_by_id(id),
901 {:ok, blocker} <- User.unblock(blocker, blocked),
902 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
904 |> put_view(AccountView)
905 |> render("relationship.json", %{user: blocker, target: blocked})
909 |> put_resp_content_type("application/json")
910 |> send_resp(403, Jason.encode!(%{"error" => message}))
914 def blocks(%{assigns: %{user: user}} = conn, _) do
915 with blocked_accounts <- User.blocked_users(user) do
916 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
921 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
922 json(conn, info.domain_blocks || [])
925 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
926 User.block_domain(blocker, domain)
930 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
931 User.unblock_domain(blocker, domain)
935 def status_search(user, query) do
937 if Regex.match?(~r/https?:/, query) do
938 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
939 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
940 true <- Visibility.visible_for_user?(activity, user) do
950 where: fragment("?->>'type' = 'Create'", a.data),
951 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
954 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
959 order_by: [desc: :id]
962 Repo.all(q) ++ fetched
965 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
966 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
968 statuses = status_search(user, query)
970 tags_path = Web.base_url() <> "/tag/"
976 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
977 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
978 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
981 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
983 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
990 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
991 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
993 statuses = status_search(user, query)
999 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1000 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1003 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1005 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1012 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1013 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1015 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1020 def favourites(%{assigns: %{user: user}} = conn, params) do
1023 |> Map.put("type", "Create")
1024 |> Map.put("favorited_by", user.ap_id)
1025 |> Map.put("blocking_user", user)
1028 ActivityPub.fetch_activities([], params)
1032 |> add_link_headers(:favourites, activities)
1033 |> put_view(StatusView)
1034 |> render("index.json", %{activities: activities, for: user, as: :activity})
1037 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1038 user = User.get_by_id(user.id)
1042 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1046 |> put_view(StatusView)
1047 |> render("index.json", %{activities: activities, for: user, as: :activity})
1050 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1051 lists = Pleroma.List.for_user(user, opts)
1052 res = ListView.render("lists.json", lists: lists)
1056 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1057 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1058 res = ListView.render("list.json", list: list)
1064 |> json(%{error: "Record not found"})
1068 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1069 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1070 res = ListView.render("lists.json", lists: lists)
1074 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1075 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1076 {:ok, _list} <- Pleroma.List.delete(list) do
1084 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1085 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1086 res = ListView.render("list.json", list: list)
1091 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1093 |> Enum.each(fn account_id ->
1094 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1095 %User{} = followed <- User.get_by_id(account_id) do
1096 Pleroma.List.follow(list, followed)
1103 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1105 |> Enum.each(fn account_id ->
1106 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1107 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1108 Pleroma.List.unfollow(list, followed)
1115 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1116 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1117 {:ok, users} = Pleroma.List.get_following(list) do
1119 |> put_view(AccountView)
1120 |> render("accounts.json", %{users: users, as: :user})
1124 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1125 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1126 {:ok, list} <- Pleroma.List.rename(list, title) do
1127 res = ListView.render("list.json", list: list)
1135 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1136 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1139 |> Map.put("type", "Create")
1140 |> Map.put("blocking_user", user)
1141 |> Map.put("muting_user", user)
1143 # we must filter the following list for the user to avoid leaking statuses the user
1144 # does not actually have permission to see (for more info, peruse security issue #270).
1147 |> Enum.filter(fn x -> x in user.following end)
1148 |> ActivityPub.fetch_activities_bounded(following, params)
1152 |> put_view(StatusView)
1153 |> render("index.json", %{activities: activities, for: user, as: :activity})
1158 |> json(%{error: "Error."})
1162 def index(%{assigns: %{user: user}} = conn, _params) do
1163 token = get_session(conn, :oauth_token)
1166 mastodon_emoji = mastodonized_emoji()
1168 limit = Config.get([:instance, :limit])
1171 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1173 flavour = get_user_flavour(user)
1178 streaming_api_base_url:
1179 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1180 access_token: token,
1182 domain: Pleroma.Web.Endpoint.host(),
1185 unfollow_modal: false,
1188 auto_play_gif: false,
1189 display_sensitive_media: false,
1190 reduce_motion: false,
1191 max_toot_chars: limit,
1192 mascot: "/images/pleroma-fox-tan-smol.png"
1195 delete_others_notice: present?(user.info.is_moderator),
1196 admin: present?(user.info.is_admin)
1200 default_privacy: user.info.default_scope,
1201 default_sensitive: false,
1202 allow_content_types: Config.get([:instance, :allowed_post_formats])
1204 media_attachments: %{
1205 accept_content_types: [
1221 user.info.settings ||
1251 push_subscription: nil,
1253 custom_emojis: mastodon_emoji,
1259 |> put_layout(false)
1260 |> put_view(MastodonView)
1261 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1264 |> put_session(:return_to, conn.request_path)
1265 |> redirect(to: "/web/login")
1269 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1270 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1272 with changeset <- Ecto.Changeset.change(user),
1273 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1274 {:ok, _user} <- User.update_and_set_cache(changeset) do
1279 |> put_resp_content_type("application/json")
1280 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1284 @supported_flavours ["glitch", "vanilla"]
1286 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1287 when flavour in @supported_flavours do
1288 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1290 with changeset <- Ecto.Changeset.change(user),
1291 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1292 {:ok, user} <- User.update_and_set_cache(changeset),
1293 flavour <- user.info.flavour do
1298 |> put_resp_content_type("application/json")
1299 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1303 def set_flavour(conn, _params) do
1306 |> json(%{error: "Unsupported flavour"})
1309 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1310 json(conn, get_user_flavour(user))
1313 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1317 defp get_user_flavour(_) do
1321 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1322 redirect(conn, to: local_mastodon_root_path(conn))
1325 @doc "Local Mastodon FE login init action"
1326 def login(conn, %{"code" => auth_token}) do
1327 with {:ok, app} <- get_or_make_app(),
1328 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1329 {:ok, token} <- Token.exchange_token(app, auth) do
1331 |> put_session(:oauth_token, token.token)
1332 |> redirect(to: local_mastodon_root_path(conn))
1336 @doc "Local Mastodon FE callback action"
1337 def login(conn, _) do
1338 with {:ok, app} <- get_or_make_app() do
1343 response_type: "code",
1344 client_id: app.client_id,
1346 scope: Enum.join(app.scopes, " ")
1349 redirect(conn, to: path)
1353 defp local_mastodon_root_path(conn) do
1354 case get_session(conn, :return_to) do
1356 mastodon_api_path(conn, :index, ["getting-started"])
1359 delete_session(conn, :return_to)
1364 defp get_or_make_app do
1365 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1366 scopes = ["read", "write", "follow", "push"]
1368 with %App{} = app <- Repo.get_by(App, find_attrs) do
1370 if app.scopes == scopes do
1374 |> Ecto.Changeset.change(%{scopes: scopes})
1382 App.register_changeset(
1384 Map.put(find_attrs, :scopes, scopes)
1391 def logout(conn, _) do
1394 |> redirect(to: "/")
1397 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1398 Logger.debug("Unimplemented, returning unmodified relationship")
1400 with %User{} = target <- User.get_by_id(id) do
1402 |> put_view(AccountView)
1403 |> render("relationship.json", %{user: user, target: target})
1407 def empty_array(conn, _) do
1408 Logger.debug("Unimplemented, returning an empty array")
1412 def empty_object(conn, _) do
1413 Logger.debug("Unimplemented, returning an empty object")
1417 def get_filters(%{assigns: %{user: user}} = conn, _) do
1418 filters = Filter.get_filters(user)
1419 res = FilterView.render("filters.json", filters: filters)
1424 %{assigns: %{user: user}} = conn,
1425 %{"phrase" => phrase, "context" => context} = params
1431 hide: Map.get(params, "irreversible", nil),
1432 whole_word: Map.get(params, "boolean", true)
1436 {:ok, response} = Filter.create(query)
1437 res = FilterView.render("filter.json", filter: response)
1441 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1442 filter = Filter.get(filter_id, user)
1443 res = FilterView.render("filter.json", filter: filter)
1448 %{assigns: %{user: user}} = conn,
1449 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1453 filter_id: filter_id,
1456 hide: Map.get(params, "irreversible", nil),
1457 whole_word: Map.get(params, "boolean", true)
1461 {:ok, response} = Filter.update(query)
1462 res = FilterView.render("filter.json", filter: response)
1466 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1469 filter_id: filter_id
1472 {:ok, _} = Filter.delete(query)
1478 def errors(conn, {:error, %Changeset{} = changeset}) do
1481 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1482 |> Enum.map_join(", ", fn {_k, v} -> v end)
1486 |> json(%{error: error_message})
1489 def errors(conn, {:error, :not_found}) do
1492 |> json(%{error: "Record not found"})
1495 def errors(conn, _) do
1498 |> json("Something went wrong")
1501 def suggestions(%{assigns: %{user: user}} = conn, _) do
1502 suggestions = Config.get(:suggestions)
1504 if Keyword.get(suggestions, :enabled, false) do
1505 api = Keyword.get(suggestions, :third_party_engine, "")
1506 timeout = Keyword.get(suggestions, :timeout, 5000)
1507 limit = Keyword.get(suggestions, :limit, 23)
1509 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1511 user = user.nickname
1515 |> String.replace("{{host}}", host)
1516 |> String.replace("{{user}}", user)
1518 with {:ok, %{status: 200, body: body}} <-
1523 recv_timeout: timeout,
1527 {:ok, data} <- Jason.decode(body) do
1530 |> Enum.slice(0, limit)
1535 case User.get_or_fetch(x["acct"]) do
1542 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1545 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1551 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1558 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1559 with %Activity{} = activity <- Activity.get_by_id(status_id),
1560 true <- Visibility.visible_for_user?(activity, user) do
1564 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1574 def reports(%{assigns: %{user: user}} = conn, params) do
1575 case CommonAPI.report(user, params) do
1578 |> put_view(ReportView)
1579 |> try_render("report.json", %{activity: activity})
1583 |> put_status(:bad_request)
1584 |> json(%{error: err})
1588 def conversations(%{assigns: %{user: user}} = conn, params) do
1589 participations = Participation.for_user_with_last_activity_id(user, params)
1592 Enum.map(participations, fn participation ->
1593 activity = Activity.get_by_id_with_object(participation.last_activity_id)
1595 last_status = StatusView.render("status.json", %{activity: activity, for: user})
1598 AccountView.render("accounts.json", %{
1599 users: participation.conversation.users,
1604 id: participation.id |> to_string(),
1606 unread: !participation.read,
1607 last_status: last_status
1612 |> add_link_headers(:conversations, participations)
1613 |> json(conversations)
1616 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1617 with %Participation{} = participation <-
1618 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1619 {:ok, participation} <- Participation.mark_as_read(participation) do
1622 id: participation.id,
1625 unread: !participation.read,
1632 def try_render(conn, target, params)
1633 when is_binary(target) do
1634 res = render(conn, target, params)
1639 |> json(%{error: "Can't display this activity"})
1645 def try_render(conn, _, _) do
1648 |> json(%{error: "Can't display this activity"})
1651 defp present?(nil), do: false
1652 defp present?(false), do: false
1653 defp present?(_), do: true