1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Notification
13 alias Pleroma.Object.Fetcher
14 alias Pleroma.Pagination
16 alias Pleroma.ScheduledActivity
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.AppView
25 alias Pleroma.Web.MastodonAPI.FilterView
26 alias Pleroma.Web.MastodonAPI.ListView
27 alias Pleroma.Web.MastodonAPI.MastodonAPI
28 alias Pleroma.Web.MastodonAPI.MastodonView
29 alias Pleroma.Web.MastodonAPI.NotificationView
30 alias Pleroma.Web.MastodonAPI.ReportView
31 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
32 alias Pleroma.Web.MastodonAPI.StatusView
33 alias Pleroma.Web.MediaProxy
34 alias Pleroma.Web.OAuth.App
35 alias Pleroma.Web.OAuth.Authorization
36 alias Pleroma.Web.OAuth.Token
38 alias Pleroma.Web.ControllerHelper
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 = ControllerHelper.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
99 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
100 |> Enum.reduce(%{}, fn key, acc ->
101 add_if_present(acc, params, to_string(key), key, fn value ->
102 {:ok, ControllerHelper.truthy_param?(value)}
105 |> add_if_present(params, "header", :banner, fn value ->
106 with %Plug.Upload{} <- value,
107 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
114 info_cng = User.Info.profile_update(user.info, info_params)
116 with changeset <- User.update_changeset(user, user_params),
117 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
118 {:ok, user} <- User.update_and_set_cache(changeset) do
119 if original_user != user do
120 CommonAPI.update(user)
123 json(conn, AccountView.render("account.json", %{user: user, for: user}))
128 |> json(%{error: "Invalid request"})
132 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
133 account = AccountView.render("account.json", %{user: user, for: user})
137 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
138 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
141 |> render("short.json", %{app: app})
145 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
146 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
147 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
148 account = AccountView.render("account.json", %{user: user, for: for_user})
154 |> json(%{error: "Can't find user"})
158 @mastodon_api_level "2.5.0"
160 def masto_instance(conn, _params) do
161 instance = Config.get(:instance)
165 title: Keyword.get(instance, :name),
166 description: Keyword.get(instance, :description),
167 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
168 email: Keyword.get(instance, :email),
170 streaming_api: Pleroma.Web.Endpoint.websocket_url()
172 stats: Stats.get_stats(),
173 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
175 registrations: Pleroma.Config.get([:instance, :registrations_open]),
176 # Extra (not present in Mastodon):
177 max_toot_chars: Keyword.get(instance, :limit)
183 def peers(conn, _params) do
184 json(conn, Stats.get_peers())
187 defp mastodonized_emoji do
188 Pleroma.Emoji.get_all()
189 |> Enum.map(fn {shortcode, relative_url, tags} ->
190 url = to_string(URI.merge(Web.base_url(), relative_url))
193 "shortcode" => shortcode,
195 "visible_in_picker" => true,
202 def custom_emojis(conn, _params) do
203 mastodon_emoji = mastodonized_emoji()
204 json(conn, mastodon_emoji)
207 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
210 |> Map.drop(["since_id", "max_id", "min_id"])
213 last = List.last(activities)
220 |> Map.get("limit", "20")
221 |> String.to_integer()
224 if length(activities) <= limit do
230 |> Enum.at(limit * -1)
234 {next_url, prev_url} =
238 Pleroma.Web.Endpoint,
241 Map.merge(params, %{max_id: max_id})
244 Pleroma.Web.Endpoint,
247 Map.merge(params, %{min_id: min_id})
253 Pleroma.Web.Endpoint,
255 Map.merge(params, %{max_id: max_id})
258 Pleroma.Web.Endpoint,
260 Map.merge(params, %{min_id: min_id})
266 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
272 def home_timeline(%{assigns: %{user: user}} = conn, params) do
275 |> Map.put("type", ["Create", "Announce"])
276 |> Map.put("blocking_user", user)
277 |> Map.put("muting_user", user)
278 |> Map.put("user", user)
281 [user.ap_id | user.following]
282 |> ActivityPub.fetch_activities(params)
283 |> ActivityPub.contain_timeline(user)
287 |> add_link_headers(:home_timeline, activities)
288 |> put_view(StatusView)
289 |> render("index.json", %{activities: activities, for: user, as: :activity})
292 def public_timeline(%{assigns: %{user: user}} = conn, params) do
293 local_only = params["local"] in [true, "True", "true", "1"]
297 |> Map.put("type", ["Create", "Announce"])
298 |> Map.put("local_only", local_only)
299 |> Map.put("blocking_user", user)
300 |> Map.put("muting_user", user)
301 |> ActivityPub.fetch_public_activities()
305 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
306 |> put_view(StatusView)
307 |> render("index.json", %{activities: activities, for: user, as: :activity})
310 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
311 with %User{} = user <- User.get_cached_by_id(params["id"]) do
312 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
315 |> add_link_headers(:user_statuses, activities, params["id"])
316 |> put_view(StatusView)
317 |> render("index.json", %{
318 activities: activities,
325 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
328 |> Map.put("type", "Create")
329 |> Map.put("blocking_user", user)
330 |> Map.put("user", user)
331 |> Map.put(:visibility, "direct")
335 |> ActivityPub.fetch_activities_query(params)
336 |> Pagination.fetch_paginated(params)
339 |> add_link_headers(:dm_timeline, activities)
340 |> put_view(StatusView)
341 |> render("index.json", %{activities: activities, for: user, as: :activity})
344 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
345 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
346 true <- Visibility.visible_for_user?(activity, user) do
348 |> put_view(StatusView)
349 |> try_render("status.json", %{activity: activity, for: user})
353 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
354 with %Activity{} = activity <- Activity.get_by_id(id),
356 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
357 "blocking_user" => user,
361 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
363 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
364 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
370 activities: grouped_activities[true] || [],
374 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
379 activities: grouped_activities[false] || [],
383 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
390 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
391 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
393 |> add_link_headers(:scheduled_statuses, scheduled_activities)
394 |> put_view(ScheduledActivityView)
395 |> render("index.json", %{scheduled_activities: scheduled_activities})
399 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
400 with %ScheduledActivity{} = scheduled_activity <-
401 ScheduledActivity.get(user, scheduled_activity_id) do
403 |> put_view(ScheduledActivityView)
404 |> render("show.json", %{scheduled_activity: scheduled_activity})
406 _ -> {:error, :not_found}
410 def update_scheduled_status(
411 %{assigns: %{user: user}} = conn,
412 %{"id" => scheduled_activity_id} = params
414 with %ScheduledActivity{} = scheduled_activity <-
415 ScheduledActivity.get(user, scheduled_activity_id),
416 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
418 |> put_view(ScheduledActivityView)
419 |> render("show.json", %{scheduled_activity: scheduled_activity})
421 nil -> {:error, :not_found}
426 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
427 with %ScheduledActivity{} = scheduled_activity <-
428 ScheduledActivity.get(user, scheduled_activity_id),
429 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
431 |> put_view(ScheduledActivityView)
432 |> render("show.json", %{scheduled_activity: scheduled_activity})
434 nil -> {:error, :not_found}
439 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
440 when length(media_ids) > 0 do
443 |> Map.put("status", ".")
445 post_status(conn, params)
448 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
451 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
454 case get_req_header(conn, "idempotency-key") do
456 _ -> Ecto.UUID.generate()
459 scheduled_at = params["scheduled_at"]
461 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
462 with {:ok, scheduled_activity} <-
463 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
465 |> put_view(ScheduledActivityView)
466 |> render("show.json", %{scheduled_activity: scheduled_activity})
469 params = Map.drop(params, ["scheduled_at"])
472 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
473 CommonAPI.post(user, params)
477 |> put_view(StatusView)
478 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
482 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
483 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
489 |> json(%{error: "Can't delete this post"})
493 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
494 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
495 %Activity{} = announce <- Activity.normalize(announce.data) do
497 |> put_view(StatusView)
498 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
502 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
503 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
504 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
506 |> put_view(StatusView)
507 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
511 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
512 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
513 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
515 |> put_view(StatusView)
516 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
520 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
521 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
522 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
524 |> put_view(StatusView)
525 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
529 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
530 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
532 |> put_view(StatusView)
533 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
537 |> put_resp_content_type("application/json")
538 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
542 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
543 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
545 |> put_view(StatusView)
546 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
550 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
551 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
552 %Object{} = object <- Object.normalize(activity),
553 %User{} = user <- User.get_cached_by_nickname(user.nickname),
554 true <- Visibility.visible_for_user?(activity, user),
555 {:ok, user} <- User.bookmark(user, object.data["id"]) do
557 |> put_view(StatusView)
558 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
562 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
563 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
564 %Object{} = object <- Object.normalize(activity),
565 %User{} = user <- User.get_cached_by_nickname(user.nickname),
566 true <- Visibility.visible_for_user?(activity, user),
567 {:ok, user} <- User.unbookmark(user, object.data["id"]) do
569 |> put_view(StatusView)
570 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
574 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
575 activity = Activity.get_by_id(id)
577 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
579 |> put_view(StatusView)
580 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
584 |> put_resp_content_type("application/json")
585 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
589 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
590 activity = Activity.get_by_id(id)
592 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
594 |> put_view(StatusView)
595 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
599 def notifications(%{assigns: %{user: user}} = conn, params) do
600 notifications = MastodonAPI.get_notifications(user, params)
603 |> add_link_headers(:notifications, notifications)
604 |> put_view(NotificationView)
605 |> render("index.json", %{notifications: notifications, for: user})
608 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
609 with {:ok, notification} <- Notification.get(user, id) do
611 |> put_view(NotificationView)
612 |> render("show.json", %{notification: notification, for: user})
616 |> put_resp_content_type("application/json")
617 |> send_resp(403, Jason.encode!(%{"error" => reason}))
621 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
622 Notification.clear(user)
626 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
627 with {:ok, _notif} <- Notification.dismiss(user, id) do
632 |> put_resp_content_type("application/json")
633 |> send_resp(403, Jason.encode!(%{"error" => reason}))
637 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
638 Notification.destroy_multiple(user, ids)
642 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
644 q = from(u in User, where: u.id in ^id)
645 targets = Repo.all(q)
648 |> put_view(AccountView)
649 |> render("relationships.json", %{user: user, targets: targets})
652 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
653 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
655 def update_media(%{assigns: %{user: user}} = conn, data) do
656 with %Object{} = object <- Repo.get(Object, data["id"]),
657 true <- Object.authorize_mutation(object, user),
658 true <- is_binary(data["description"]),
659 description <- data["description"] do
660 new_data = %{object.data | "name" => description}
664 |> Object.change(%{data: new_data})
667 attachment_data = Map.put(new_data, "id", object.id)
670 |> put_view(StatusView)
671 |> render("attachment.json", %{attachment: attachment_data})
675 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
676 with {:ok, object} <-
679 actor: User.ap_id(user),
680 description: Map.get(data, "description")
682 attachment_data = Map.put(object.data, "id", object.id)
685 |> put_view(StatusView)
686 |> render("attachment.json", %{attachment: attachment_data})
690 def favourited_by(conn, %{"id" => id}) do
691 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
692 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
693 q = from(u in User, where: u.ap_id in ^likes)
697 |> put_view(AccountView)
698 |> render(AccountView, "accounts.json", %{users: users, as: :user})
704 def reblogged_by(conn, %{"id" => id}) do
705 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
706 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
707 q = from(u in User, where: u.ap_id in ^announces)
711 |> put_view(AccountView)
712 |> render("accounts.json", %{users: users, as: :user})
718 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
719 local_only = params["local"] in [true, "True", "true", "1"]
722 [params["tag"], params["any"]]
726 |> Enum.map(&String.downcase(&1))
731 |> Enum.map(&String.downcase(&1))
736 |> Enum.map(&String.downcase(&1))
740 |> Map.put("type", "Create")
741 |> Map.put("local_only", local_only)
742 |> Map.put("blocking_user", user)
743 |> Map.put("muting_user", user)
744 |> Map.put("tag", tags)
745 |> Map.put("tag_all", tag_all)
746 |> Map.put("tag_reject", tag_reject)
747 |> ActivityPub.fetch_public_activities()
751 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
752 |> put_view(StatusView)
753 |> render("index.json", %{activities: activities, for: user, as: :activity})
756 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
757 with %User{} = user <- User.get_cached_by_id(id),
758 followers <- MastodonAPI.get_followers(user, params) do
761 for_user && user.id == for_user.id -> followers
762 user.info.hide_followers -> []
767 |> add_link_headers(:followers, followers, user)
768 |> put_view(AccountView)
769 |> render("accounts.json", %{users: followers, as: :user})
773 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
774 with %User{} = user <- User.get_cached_by_id(id),
775 followers <- MastodonAPI.get_friends(user, params) do
778 for_user && user.id == for_user.id -> followers
779 user.info.hide_follows -> []
784 |> add_link_headers(:following, followers, user)
785 |> put_view(AccountView)
786 |> render("accounts.json", %{users: followers, as: :user})
790 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
791 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
793 |> put_view(AccountView)
794 |> render("accounts.json", %{users: follow_requests, as: :user})
798 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
799 with %User{} = follower <- User.get_cached_by_id(id),
800 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
802 |> put_view(AccountView)
803 |> render("relationship.json", %{user: followed, target: follower})
807 |> put_resp_content_type("application/json")
808 |> send_resp(403, Jason.encode!(%{"error" => message}))
812 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
813 with %User{} = follower <- User.get_cached_by_id(id),
814 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
816 |> put_view(AccountView)
817 |> render("relationship.json", %{user: followed, target: follower})
821 |> put_resp_content_type("application/json")
822 |> send_resp(403, Jason.encode!(%{"error" => message}))
826 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
827 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
828 {_, true} <- {:followed, follower.id != followed.id},
829 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
831 |> put_view(AccountView)
832 |> render("relationship.json", %{user: follower, target: followed})
839 |> put_resp_content_type("application/json")
840 |> send_resp(403, Jason.encode!(%{"error" => message}))
844 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
845 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
846 {_, true} <- {:followed, follower.id != followed.id},
847 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
849 |> put_view(AccountView)
850 |> render("account.json", %{user: followed, for: follower})
857 |> put_resp_content_type("application/json")
858 |> send_resp(403, Jason.encode!(%{"error" => message}))
862 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
863 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
864 {_, true} <- {:followed, follower.id != followed.id},
865 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
867 |> put_view(AccountView)
868 |> render("relationship.json", %{user: follower, target: followed})
878 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
879 with %User{} = muted <- User.get_cached_by_id(id),
880 {:ok, muter} <- User.mute(muter, muted) do
882 |> put_view(AccountView)
883 |> render("relationship.json", %{user: muter, target: muted})
887 |> put_resp_content_type("application/json")
888 |> send_resp(403, Jason.encode!(%{"error" => message}))
892 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
893 with %User{} = muted <- User.get_cached_by_id(id),
894 {:ok, muter} <- User.unmute(muter, muted) do
896 |> put_view(AccountView)
897 |> render("relationship.json", %{user: muter, target: muted})
901 |> put_resp_content_type("application/json")
902 |> send_resp(403, Jason.encode!(%{"error" => message}))
906 def mutes(%{assigns: %{user: user}} = conn, _) do
907 with muted_accounts <- User.muted_users(user) do
908 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
913 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
914 with %User{} = blocked <- User.get_cached_by_id(id),
915 {:ok, blocker} <- User.block(blocker, blocked),
916 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
918 |> put_view(AccountView)
919 |> render("relationship.json", %{user: blocker, target: blocked})
923 |> put_resp_content_type("application/json")
924 |> send_resp(403, Jason.encode!(%{"error" => message}))
928 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
929 with %User{} = blocked <- User.get_cached_by_id(id),
930 {:ok, blocker} <- User.unblock(blocker, blocked),
931 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
933 |> put_view(AccountView)
934 |> render("relationship.json", %{user: blocker, target: blocked})
938 |> put_resp_content_type("application/json")
939 |> send_resp(403, Jason.encode!(%{"error" => message}))
943 def blocks(%{assigns: %{user: user}} = conn, _) do
944 with blocked_accounts <- User.blocked_users(user) do
945 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
950 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
951 json(conn, info.domain_blocks || [])
954 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
955 User.block_domain(blocker, domain)
959 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
960 User.unblock_domain(blocker, domain)
964 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
965 with %User{} = subscription_target <- User.get_cached_by_id(id),
966 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
968 |> put_view(AccountView)
969 |> render("relationship.json", %{user: user, target: subscription_target})
973 |> put_resp_content_type("application/json")
974 |> send_resp(403, Jason.encode!(%{"error" => message}))
978 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
979 with %User{} = subscription_target <- User.get_cached_by_id(id),
980 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
982 |> put_view(AccountView)
983 |> render("relationship.json", %{user: user, target: subscription_target})
987 |> put_resp_content_type("application/json")
988 |> send_resp(403, Jason.encode!(%{"error" => message}))
992 def status_search(user, query) do
994 if Regex.match?(~r/https?:/, query) do
995 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
996 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
997 true <- Visibility.visible_for_user?(activity, user) do
1006 [a, o] in Activity.with_preloaded_object(Activity),
1007 where: fragment("?->>'type' = 'Create'", a.data),
1008 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1011 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1016 order_by: [desc: :id]
1019 Repo.all(q) ++ fetched
1022 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1023 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1025 statuses = status_search(user, query)
1027 tags_path = Web.base_url() <> "/tag/"
1033 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1034 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1035 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1038 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1040 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1047 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1048 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1050 statuses = status_search(user, query)
1056 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1057 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1060 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1062 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1069 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1070 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1072 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1077 def favourites(%{assigns: %{user: user}} = conn, params) do
1080 |> Map.put("type", "Create")
1081 |> Map.put("favorited_by", user.ap_id)
1082 |> Map.put("blocking_user", user)
1085 ActivityPub.fetch_activities([], params)
1089 |> add_link_headers(:favourites, activities)
1090 |> put_view(StatusView)
1091 |> render("index.json", %{activities: activities, for: user, as: :activity})
1094 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1095 with %User{} = user <- User.get_by_id(id),
1096 false <- user.info.hide_favorites do
1099 |> Map.put("type", "Create")
1100 |> Map.put("favorited_by", user.ap_id)
1101 |> Map.put("blocking_user", for_user)
1105 ["https://www.w3.org/ns/activitystreams#Public"] ++
1106 [for_user.ap_id | for_user.following]
1108 ["https://www.w3.org/ns/activitystreams#Public"]
1113 |> ActivityPub.fetch_activities(params)
1117 |> add_link_headers(:favourites, activities)
1118 |> put_view(StatusView)
1119 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1122 {:error, :not_found}
1127 |> json(%{error: "Can't get favorites"})
1131 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1132 user = User.get_cached_by_id(user.id)
1136 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1140 |> put_view(StatusView)
1141 |> render("index.json", %{activities: activities, for: user, as: :activity})
1144 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1145 lists = Pleroma.List.for_user(user, opts)
1146 res = ListView.render("lists.json", lists: lists)
1150 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1151 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1152 res = ListView.render("list.json", list: list)
1158 |> json(%{error: "Record not found"})
1162 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1163 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1164 res = ListView.render("lists.json", lists: lists)
1168 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1169 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1170 {:ok, _list} <- Pleroma.List.delete(list) do
1178 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1179 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1180 res = ListView.render("list.json", list: list)
1185 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1187 |> Enum.each(fn account_id ->
1188 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1189 %User{} = followed <- User.get_cached_by_id(account_id) do
1190 Pleroma.List.follow(list, followed)
1197 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1199 |> Enum.each(fn account_id ->
1200 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1201 %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
1202 Pleroma.List.unfollow(list, followed)
1209 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1210 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1211 {:ok, users} = Pleroma.List.get_following(list) do
1213 |> put_view(AccountView)
1214 |> render("accounts.json", %{users: users, as: :user})
1218 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1219 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1220 {:ok, list} <- Pleroma.List.rename(list, title) do
1221 res = ListView.render("list.json", list: list)
1229 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1230 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1233 |> Map.put("type", "Create")
1234 |> Map.put("blocking_user", user)
1235 |> Map.put("muting_user", user)
1237 # we must filter the following list for the user to avoid leaking statuses the user
1238 # does not actually have permission to see (for more info, peruse security issue #270).
1241 |> Enum.filter(fn x -> x in user.following end)
1242 |> ActivityPub.fetch_activities_bounded(following, params)
1246 |> put_view(StatusView)
1247 |> render("index.json", %{activities: activities, for: user, as: :activity})
1252 |> json(%{error: "Error."})
1256 def index(%{assigns: %{user: user}} = conn, _params) do
1257 token = get_session(conn, :oauth_token)
1260 mastodon_emoji = mastodonized_emoji()
1262 limit = Config.get([:instance, :limit])
1265 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1267 flavour = get_user_flavour(user)
1272 streaming_api_base_url:
1273 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1274 access_token: token,
1276 domain: Pleroma.Web.Endpoint.host(),
1279 unfollow_modal: false,
1282 auto_play_gif: false,
1283 display_sensitive_media: false,
1284 reduce_motion: false,
1285 max_toot_chars: limit,
1286 mascot: "/images/pleroma-fox-tan-smol.png"
1289 delete_others_notice: present?(user.info.is_moderator),
1290 admin: present?(user.info.is_admin)
1294 default_privacy: user.info.default_scope,
1295 default_sensitive: false,
1296 allow_content_types: Config.get([:instance, :allowed_post_formats])
1298 media_attachments: %{
1299 accept_content_types: [
1315 user.info.settings ||
1345 push_subscription: nil,
1347 custom_emojis: mastodon_emoji,
1353 |> put_layout(false)
1354 |> put_view(MastodonView)
1355 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1358 |> put_session(:return_to, conn.request_path)
1359 |> redirect(to: "/web/login")
1363 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1364 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1366 with changeset <- Ecto.Changeset.change(user),
1367 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1368 {:ok, _user} <- User.update_and_set_cache(changeset) do
1373 |> put_resp_content_type("application/json")
1374 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1378 @supported_flavours ["glitch", "vanilla"]
1380 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1381 when flavour in @supported_flavours do
1382 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1384 with changeset <- Ecto.Changeset.change(user),
1385 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1386 {:ok, user} <- User.update_and_set_cache(changeset),
1387 flavour <- user.info.flavour do
1392 |> put_resp_content_type("application/json")
1393 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1397 def set_flavour(conn, _params) do
1400 |> json(%{error: "Unsupported flavour"})
1403 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1404 json(conn, get_user_flavour(user))
1407 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1411 defp get_user_flavour(_) do
1415 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1416 redirect(conn, to: local_mastodon_root_path(conn))
1419 @doc "Local Mastodon FE login init action"
1420 def login(conn, %{"code" => auth_token}) do
1421 with {:ok, app} <- get_or_make_app(),
1422 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1423 {:ok, token} <- Token.exchange_token(app, auth) do
1425 |> put_session(:oauth_token, token.token)
1426 |> redirect(to: local_mastodon_root_path(conn))
1430 @doc "Local Mastodon FE callback action"
1431 def login(conn, _) do
1432 with {:ok, app} <- get_or_make_app() do
1437 response_type: "code",
1438 client_id: app.client_id,
1440 scope: Enum.join(app.scopes, " ")
1443 redirect(conn, to: path)
1447 defp local_mastodon_root_path(conn) do
1448 case get_session(conn, :return_to) do
1450 mastodon_api_path(conn, :index, ["getting-started"])
1453 delete_session(conn, :return_to)
1458 defp get_or_make_app do
1459 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1460 scopes = ["read", "write", "follow", "push"]
1462 with %App{} = app <- Repo.get_by(App, find_attrs) do
1464 if app.scopes == scopes do
1468 |> Ecto.Changeset.change(%{scopes: scopes})
1476 App.register_changeset(
1478 Map.put(find_attrs, :scopes, scopes)
1485 def logout(conn, _) do
1488 |> redirect(to: "/")
1491 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1492 Logger.debug("Unimplemented, returning unmodified relationship")
1494 with %User{} = target <- User.get_cached_by_id(id) do
1496 |> put_view(AccountView)
1497 |> render("relationship.json", %{user: user, target: target})
1501 def empty_array(conn, _) do
1502 Logger.debug("Unimplemented, returning an empty array")
1506 def empty_object(conn, _) do
1507 Logger.debug("Unimplemented, returning an empty object")
1511 def get_filters(%{assigns: %{user: user}} = conn, _) do
1512 filters = Filter.get_filters(user)
1513 res = FilterView.render("filters.json", filters: filters)
1518 %{assigns: %{user: user}} = conn,
1519 %{"phrase" => phrase, "context" => context} = params
1525 hide: Map.get(params, "irreversible", nil),
1526 whole_word: Map.get(params, "boolean", true)
1530 {:ok, response} = Filter.create(query)
1531 res = FilterView.render("filter.json", filter: response)
1535 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1536 filter = Filter.get(filter_id, user)
1537 res = FilterView.render("filter.json", filter: filter)
1542 %{assigns: %{user: user}} = conn,
1543 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1547 filter_id: filter_id,
1550 hide: Map.get(params, "irreversible", nil),
1551 whole_word: Map.get(params, "boolean", true)
1555 {:ok, response} = Filter.update(query)
1556 res = FilterView.render("filter.json", filter: response)
1560 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1563 filter_id: filter_id
1566 {:ok, _} = Filter.delete(query)
1572 def errors(conn, {:error, %Changeset{} = changeset}) do
1575 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1576 |> Enum.map_join(", ", fn {_k, v} -> v end)
1580 |> json(%{error: error_message})
1583 def errors(conn, {:error, :not_found}) do
1586 |> json(%{error: "Record not found"})
1589 def errors(conn, _) do
1592 |> json("Something went wrong")
1595 def suggestions(%{assigns: %{user: user}} = conn, _) do
1596 suggestions = Config.get(:suggestions)
1598 if Keyword.get(suggestions, :enabled, false) do
1599 api = Keyword.get(suggestions, :third_party_engine, "")
1600 timeout = Keyword.get(suggestions, :timeout, 5000)
1601 limit = Keyword.get(suggestions, :limit, 23)
1603 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1605 user = user.nickname
1609 |> String.replace("{{host}}", host)
1610 |> String.replace("{{user}}", user)
1612 with {:ok, %{status: 200, body: body}} <-
1617 recv_timeout: timeout,
1621 {:ok, data} <- Jason.decode(body) do
1624 |> Enum.slice(0, limit)
1629 case User.get_or_fetch(x["acct"]) do
1636 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1639 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1645 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1652 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1653 with %Activity{} = activity <- Activity.get_by_id(status_id),
1654 true <- Visibility.visible_for_user?(activity, user) do
1658 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1668 def reports(%{assigns: %{user: user}} = conn, params) do
1669 case CommonAPI.report(user, params) do
1672 |> put_view(ReportView)
1673 |> try_render("report.json", %{activity: activity})
1677 |> put_status(:bad_request)
1678 |> json(%{error: err})
1682 def try_render(conn, target, params)
1683 when is_binary(target) do
1684 res = render(conn, target, params)
1689 |> json(%{error: "Can't display this activity"})
1695 def try_render(conn, _, _) do
1698 |> json(%{error: "Can't display this activity"})
1701 defp present?(nil), do: false
1702 defp present?(false), do: false
1703 defp present?(_), do: true