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
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Web.MastodonAPI.AccountView
21 alias Pleroma.Web.MastodonAPI.AppView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonAPI
25 alias Pleroma.Web.MastodonAPI.MastodonView
26 alias Pleroma.Web.MastodonAPI.NotificationView
27 alias Pleroma.Web.MastodonAPI.ReportView
28 alias Pleroma.Web.MastodonAPI.StatusView
29 alias Pleroma.Web.MediaProxy
30 alias Pleroma.Web.OAuth.App
31 alias Pleroma.Web.OAuth.Authorization
32 alias Pleroma.Web.OAuth.Token
34 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
39 @httpoison Application.get_env(:pleroma, :httpoison)
40 @local_mastodon_name "Mastodon-Local"
42 action_fallback(:errors)
44 def create_app(conn, params) do
45 scopes = oauth_scopes(params, ["read"])
49 |> Map.drop(["scope", "scopes"])
50 |> Map.put("scopes", scopes)
52 with cs <- App.register_changeset(%App{}, app_attrs),
53 false <- cs.changes[:client_name] == @local_mastodon_name,
54 {:ok, app} <- Repo.insert(cs) do
57 |> render("show.json", %{app: app})
66 value_function \\ fn x -> {:ok, x} end
68 if Map.has_key?(params, params_field) do
69 case value_function.(params[params_field]) do
70 {:ok, new_value} -> Map.put(map, map_field, new_value)
78 def update_credentials(%{assigns: %{user: user}} = conn, params) do
83 |> add_if_present(params, "display_name", :name)
84 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
85 |> add_if_present(params, "avatar", :avatar, fn value ->
86 with %Plug.Upload{} <- value,
87 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
96 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
97 |> add_if_present(params, "header", :banner, fn value ->
98 with %Plug.Upload{} <- value,
99 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
106 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
108 with changeset <- User.update_changeset(user, user_params),
109 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
110 {:ok, user} <- User.update_and_set_cache(changeset) do
111 if original_user != user do
112 CommonAPI.update(user)
115 json(conn, AccountView.render("account.json", %{user: user, for: user}))
120 |> json(%{error: "Invalid request"})
124 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
125 account = AccountView.render("account.json", %{user: user, for: user})
129 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
130 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
133 |> render("short.json", %{app: app})
137 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
138 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
139 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
140 account = AccountView.render("account.json", %{user: user, for: for_user})
146 |> json(%{error: "Can't find user"})
150 @mastodon_api_level "2.5.0"
152 def masto_instance(conn, _params) do
153 instance = Config.get(:instance)
157 title: Keyword.get(instance, :name),
158 description: Keyword.get(instance, :description),
159 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
160 email: Keyword.get(instance, :email),
162 streaming_api: Pleroma.Web.Endpoint.websocket_url()
164 stats: Stats.get_stats(),
165 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
167 registrations: Pleroma.Config.get([:instance, :registrations_open]),
168 # Extra (not present in Mastodon):
169 max_toot_chars: Keyword.get(instance, :limit)
175 def peers(conn, _params) do
176 json(conn, Stats.get_peers())
179 defp mastodonized_emoji do
180 Pleroma.Emoji.get_all()
181 |> Enum.map(fn {shortcode, relative_url} ->
182 url = to_string(URI.merge(Web.base_url(), relative_url))
185 "shortcode" => shortcode,
187 "visible_in_picker" => true,
193 def custom_emojis(conn, _params) do
194 mastodon_emoji = mastodonized_emoji()
195 json(conn, mastodon_emoji)
198 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
201 |> Map.drop(["since_id", "max_id"])
204 last = List.last(activities)
205 first = List.first(activities)
211 {next_url, prev_url} =
215 Pleroma.Web.Endpoint,
218 Map.merge(params, %{max_id: min})
221 Pleroma.Web.Endpoint,
224 Map.merge(params, %{since_id: max})
230 Pleroma.Web.Endpoint,
232 Map.merge(params, %{max_id: min})
235 Pleroma.Web.Endpoint,
237 Map.merge(params, %{since_id: max})
243 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
249 def home_timeline(%{assigns: %{user: user}} = conn, params) do
252 |> Map.put("type", ["Create", "Announce"])
253 |> Map.put("blocking_user", user)
254 |> Map.put("muting_user", user)
255 |> Map.put("user", user)
258 [user.ap_id | user.following]
259 |> ActivityPub.fetch_activities(params)
260 |> ActivityPub.contain_timeline(user)
264 |> add_link_headers(:home_timeline, activities)
265 |> put_view(StatusView)
266 |> render("index.json", %{activities: activities, for: user, as: :activity})
269 def public_timeline(%{assigns: %{user: user}} = conn, params) do
270 local_only = params["local"] in [true, "True", "true", "1"]
274 |> Map.put("type", ["Create", "Announce"])
275 |> Map.put("local_only", local_only)
276 |> Map.put("blocking_user", user)
277 |> Map.put("muting_user", user)
278 |> ActivityPub.fetch_public_activities()
282 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
283 |> put_view(StatusView)
284 |> render("index.json", %{activities: activities, for: user, as: :activity})
287 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
288 with %User{} = user <- User.get_by_id(params["id"]) do
289 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
292 |> add_link_headers(:user_statuses, activities, params["id"])
293 |> put_view(StatusView)
294 |> render("index.json", %{
295 activities: activities,
302 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
305 |> Map.put("type", "Create")
306 |> Map.put("blocking_user", user)
307 |> Map.put("user", user)
308 |> Map.put(:visibility, "direct")
312 |> ActivityPub.fetch_activities_query(params)
316 |> add_link_headers(:dm_timeline, activities)
317 |> put_view(StatusView)
318 |> render("index.json", %{activities: activities, for: user, as: :activity})
321 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
322 with %Activity{} = activity <- Activity.get_by_id(id),
323 true <- Visibility.visible_for_user?(activity, user) do
325 |> put_view(StatusView)
326 |> try_render("status.json", %{activity: activity, for: user})
330 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
331 with %Activity{} = activity <- Activity.get_by_id(id),
333 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
334 "blocking_user" => user,
338 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
340 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
341 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
347 activities: grouped_activities[true] || [],
351 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
356 activities: grouped_activities[false] || [],
360 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
367 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
368 when length(media_ids) > 0 do
371 |> Map.put("status", ".")
373 post_status(conn, params)
376 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
379 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
382 case get_req_header(conn, "idempotency-key") do
384 _ -> Ecto.UUID.generate()
388 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
391 |> put_view(StatusView)
392 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
395 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
396 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
402 |> json(%{error: "Can't delete this post"})
406 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
407 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
409 |> put_view(StatusView)
410 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
414 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
415 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
416 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
418 |> put_view(StatusView)
419 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
423 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
424 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
425 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
427 |> put_view(StatusView)
428 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
432 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
433 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
434 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
436 |> put_view(StatusView)
437 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
441 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
442 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
444 |> put_view(StatusView)
445 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
449 |> put_resp_content_type("application/json")
450 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
454 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
455 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
457 |> put_view(StatusView)
458 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
462 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
463 with %Activity{} = activity <- Activity.get_by_id(id),
464 %User{} = user <- User.get_by_nickname(user.nickname),
465 true <- Visibility.visible_for_user?(activity, user),
466 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
468 |> put_view(StatusView)
469 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
473 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
474 with %Activity{} = activity <- Activity.get_by_id(id),
475 %User{} = user <- User.get_by_nickname(user.nickname),
476 true <- Visibility.visible_for_user?(activity, user),
477 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
479 |> put_view(StatusView)
480 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
484 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
485 activity = Activity.get_by_id(id)
487 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
489 |> put_view(StatusView)
490 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
494 |> put_resp_content_type("application/json")
495 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
499 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
500 activity = Activity.get_by_id(id)
502 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
504 |> put_view(StatusView)
505 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
509 def notifications(%{assigns: %{user: user}} = conn, params) do
510 notifications = MastodonAPI.get_notifications(user, params)
513 |> add_link_headers(:notifications, notifications)
514 |> put_view(NotificationView)
515 |> render("index.json", %{notifications: notifications, for: user})
518 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
519 with {:ok, notification} <- Notification.get(user, id) do
521 |> put_view(NotificationView)
522 |> render("show.json", %{notification: notification, for: user})
526 |> put_resp_content_type("application/json")
527 |> send_resp(403, Jason.encode!(%{"error" => reason}))
531 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
532 Notification.clear(user)
536 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
537 with {:ok, _notif} <- Notification.dismiss(user, id) do
542 |> put_resp_content_type("application/json")
543 |> send_resp(403, Jason.encode!(%{"error" => reason}))
547 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
549 q = from(u in User, where: u.id in ^id)
550 targets = Repo.all(q)
553 |> put_view(AccountView)
554 |> render("relationships.json", %{user: user, targets: targets})
557 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
558 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
560 def update_media(%{assigns: %{user: user}} = conn, data) do
561 with %Object{} = object <- Repo.get(Object, data["id"]),
562 true <- Object.authorize_mutation(object, user),
563 true <- is_binary(data["description"]),
564 description <- data["description"] do
565 new_data = %{object.data | "name" => description}
569 |> Object.change(%{data: new_data})
572 attachment_data = Map.put(new_data, "id", object.id)
575 |> put_view(StatusView)
576 |> render("attachment.json", %{attachment: attachment_data})
580 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
581 with {:ok, object} <-
584 actor: User.ap_id(user),
585 description: Map.get(data, "description")
587 attachment_data = Map.put(object.data, "id", object.id)
590 |> put_view(StatusView)
591 |> render("attachment.json", %{attachment: attachment_data})
595 def favourited_by(conn, %{"id" => id}) do
596 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
597 q = from(u in User, where: u.ap_id in ^likes)
601 |> put_view(AccountView)
602 |> render(AccountView, "accounts.json", %{users: users, as: :user})
608 def reblogged_by(conn, %{"id" => id}) do
609 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
610 q = from(u in User, where: u.ap_id in ^announces)
614 |> put_view(AccountView)
615 |> render("accounts.json", %{users: users, as: :user})
621 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
622 local_only = params["local"] in [true, "True", "true", "1"]
625 [params["tag"], params["any"]]
629 |> Enum.map(&String.downcase(&1))
634 |> Enum.map(&String.downcase(&1))
639 |> Enum.map(&String.downcase(&1))
643 |> Map.put("type", "Create")
644 |> Map.put("local_only", local_only)
645 |> Map.put("blocking_user", user)
646 |> Map.put("muting_user", user)
647 |> Map.put("tag", tags)
648 |> Map.put("tag_all", tag_all)
649 |> Map.put("tag_reject", tag_reject)
650 |> ActivityPub.fetch_public_activities()
654 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
655 |> put_view(StatusView)
656 |> render("index.json", %{activities: activities, for: user, as: :activity})
659 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
660 with %User{} = user <- User.get_by_id(id),
661 followers <- MastodonAPI.get_followers(user, params) do
664 for_user && user.id == for_user.id -> followers
665 user.info.hide_followers -> []
670 |> add_link_headers(:followers, followers, user)
671 |> put_view(AccountView)
672 |> render("accounts.json", %{users: followers, as: :user})
676 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
677 with %User{} = user <- User.get_by_id(id),
678 followers <- MastodonAPI.get_friends(user, params) do
681 for_user && user.id == for_user.id -> followers
682 user.info.hide_follows -> []
687 |> add_link_headers(:following, followers, user)
688 |> put_view(AccountView)
689 |> render("accounts.json", %{users: followers, as: :user})
693 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
694 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
696 |> put_view(AccountView)
697 |> render("accounts.json", %{users: follow_requests, as: :user})
701 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
702 with %User{} = follower <- User.get_by_id(id),
703 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
705 |> put_view(AccountView)
706 |> render("relationship.json", %{user: followed, target: follower})
710 |> put_resp_content_type("application/json")
711 |> send_resp(403, Jason.encode!(%{"error" => message}))
715 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
716 with %User{} = follower <- User.get_by_id(id),
717 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
719 |> put_view(AccountView)
720 |> render("relationship.json", %{user: followed, target: follower})
724 |> put_resp_content_type("application/json")
725 |> send_resp(403, Jason.encode!(%{"error" => message}))
729 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
730 with %User{} = followed <- User.get_by_id(id),
731 false <- User.following?(follower, followed),
732 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
734 |> put_view(AccountView)
735 |> render("relationship.json", %{user: follower, target: followed})
738 followed = User.get_cached_by_id(id)
741 case conn.params["reblogs"] do
742 true -> CommonAPI.show_reblogs(follower, followed)
743 false -> CommonAPI.hide_reblogs(follower, followed)
747 |> put_view(AccountView)
748 |> render("relationship.json", %{user: follower, target: followed})
752 |> put_resp_content_type("application/json")
753 |> send_resp(403, Jason.encode!(%{"error" => message}))
757 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
758 with %User{} = followed <- User.get_by_nickname(uri),
759 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
761 |> put_view(AccountView)
762 |> render("account.json", %{user: followed, for: follower})
766 |> put_resp_content_type("application/json")
767 |> send_resp(403, Jason.encode!(%{"error" => message}))
771 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
772 with %User{} = followed <- User.get_by_id(id),
773 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
775 |> put_view(AccountView)
776 |> render("relationship.json", %{user: follower, target: followed})
780 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
781 with %User{} = muted <- User.get_by_id(id),
782 {:ok, muter} <- User.mute(muter, muted) do
784 |> put_view(AccountView)
785 |> render("relationship.json", %{user: muter, target: muted})
789 |> put_resp_content_type("application/json")
790 |> send_resp(403, Jason.encode!(%{"error" => message}))
794 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
795 with %User{} = muted <- User.get_by_id(id),
796 {:ok, muter} <- User.unmute(muter, muted) do
798 |> put_view(AccountView)
799 |> render("relationship.json", %{user: muter, target: muted})
803 |> put_resp_content_type("application/json")
804 |> send_resp(403, Jason.encode!(%{"error" => message}))
808 def mutes(%{assigns: %{user: user}} = conn, _) do
809 with muted_accounts <- User.muted_users(user) do
810 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
815 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
816 with %User{} = blocked <- User.get_by_id(id),
817 {:ok, blocker} <- User.block(blocker, blocked),
818 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
820 |> put_view(AccountView)
821 |> render("relationship.json", %{user: blocker, target: blocked})
825 |> put_resp_content_type("application/json")
826 |> send_resp(403, Jason.encode!(%{"error" => message}))
830 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
831 with %User{} = blocked <- User.get_by_id(id),
832 {:ok, blocker} <- User.unblock(blocker, blocked),
833 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
835 |> put_view(AccountView)
836 |> render("relationship.json", %{user: blocker, target: blocked})
840 |> put_resp_content_type("application/json")
841 |> send_resp(403, Jason.encode!(%{"error" => message}))
845 def blocks(%{assigns: %{user: user}} = conn, _) do
846 with blocked_accounts <- User.blocked_users(user) do
847 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
852 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
853 json(conn, info.domain_blocks || [])
856 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
857 User.block_domain(blocker, domain)
861 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
862 User.unblock_domain(blocker, domain)
866 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
867 with %User{} = subscription_target <- User.get_cached_by_id(id),
868 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
870 |> put_view(AccountView)
871 |> render("relationship.json", %{user: user, target: subscription_target})
875 |> put_resp_content_type("application/json")
876 |> send_resp(403, Jason.encode!(%{"error" => message}))
880 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
881 with %User{} = subscription_target <- User.get_cached_by_id(id),
882 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
884 |> put_view(AccountView)
885 |> render("relationship.json", %{user: user, target: subscription_target})
889 |> put_resp_content_type("application/json")
890 |> send_resp(403, Jason.encode!(%{"error" => message}))
894 def status_search(user, query) do
896 if Regex.match?(~r/https?:/, query) do
897 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
898 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
899 true <- Visibility.visible_for_user?(activity, user) do
909 where: fragment("?->>'type' = 'Create'", a.data),
910 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
913 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
918 order_by: [desc: :id]
921 Repo.all(q) ++ fetched
924 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
925 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
927 statuses = status_search(user, query)
929 tags_path = Web.base_url() <> "/tag/"
935 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
936 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
937 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
940 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
942 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
949 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
950 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
952 statuses = status_search(user, query)
958 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
959 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
962 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
964 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
971 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
972 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
974 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
979 def favourites(%{assigns: %{user: user}} = conn, params) do
982 |> Map.put("type", "Create")
983 |> Map.put("favorited_by", user.ap_id)
984 |> Map.put("blocking_user", user)
987 ActivityPub.fetch_activities([], params)
991 |> add_link_headers(:favourites, activities)
992 |> put_view(StatusView)
993 |> render("index.json", %{activities: activities, for: user, as: :activity})
996 def bookmarks(%{assigns: %{user: user}} = conn, _) do
997 user = User.get_by_id(user.id)
1001 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1005 |> put_view(StatusView)
1006 |> render("index.json", %{activities: activities, for: user, as: :activity})
1009 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1010 lists = Pleroma.List.for_user(user, opts)
1011 res = ListView.render("lists.json", lists: lists)
1015 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1016 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1017 res = ListView.render("list.json", list: list)
1023 |> json(%{error: "Record not found"})
1027 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1028 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1029 res = ListView.render("lists.json", lists: lists)
1033 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1034 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1035 {:ok, _list} <- Pleroma.List.delete(list) do
1043 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1044 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1045 res = ListView.render("list.json", list: list)
1050 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1052 |> Enum.each(fn account_id ->
1053 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1054 %User{} = followed <- User.get_by_id(account_id) do
1055 Pleroma.List.follow(list, followed)
1062 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1064 |> Enum.each(fn account_id ->
1065 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1066 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1067 Pleroma.List.unfollow(list, followed)
1074 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1075 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1076 {:ok, users} = Pleroma.List.get_following(list) do
1078 |> put_view(AccountView)
1079 |> render("accounts.json", %{users: users, as: :user})
1083 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1084 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1085 {:ok, list} <- Pleroma.List.rename(list, title) do
1086 res = ListView.render("list.json", list: list)
1094 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1095 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1098 |> Map.put("type", "Create")
1099 |> Map.put("blocking_user", user)
1100 |> Map.put("muting_user", user)
1102 # we must filter the following list for the user to avoid leaking statuses the user
1103 # does not actually have permission to see (for more info, peruse security issue #270).
1106 |> Enum.filter(fn x -> x in user.following end)
1107 |> ActivityPub.fetch_activities_bounded(following, params)
1111 |> put_view(StatusView)
1112 |> render("index.json", %{activities: activities, for: user, as: :activity})
1117 |> json(%{error: "Error."})
1121 def index(%{assigns: %{user: user}} = conn, _params) do
1124 |> get_session(:oauth_token)
1127 mastodon_emoji = mastodonized_emoji()
1129 limit = Config.get([:instance, :limit])
1132 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1134 flavour = get_user_flavour(user)
1139 streaming_api_base_url:
1140 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1141 access_token: token,
1143 domain: Pleroma.Web.Endpoint.host(),
1146 unfollow_modal: false,
1149 auto_play_gif: false,
1150 display_sensitive_media: false,
1151 reduce_motion: false,
1152 max_toot_chars: limit,
1153 mascot: "/images/pleroma-fox-tan-smol.png"
1156 delete_others_notice: present?(user.info.is_moderator),
1157 admin: present?(user.info.is_admin)
1161 default_privacy: user.info.default_scope,
1162 default_sensitive: false,
1163 allow_content_types: Config.get([:instance, :allowed_post_formats])
1165 media_attachments: %{
1166 accept_content_types: [
1182 user.info.settings ||
1212 push_subscription: nil,
1214 custom_emojis: mastodon_emoji,
1220 |> put_layout(false)
1221 |> put_view(MastodonView)
1222 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1225 |> redirect(to: "/web/login")
1229 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1230 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1232 with changeset <- Ecto.Changeset.change(user),
1233 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1234 {:ok, _user} <- User.update_and_set_cache(changeset) do
1239 |> put_resp_content_type("application/json")
1240 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1244 @supported_flavours ["glitch", "vanilla"]
1246 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1247 when flavour in @supported_flavours do
1248 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1250 with changeset <- Ecto.Changeset.change(user),
1251 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1252 {:ok, user} <- User.update_and_set_cache(changeset),
1253 flavour <- user.info.flavour do
1258 |> put_resp_content_type("application/json")
1259 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1263 def set_flavour(conn, _params) do
1266 |> json(%{error: "Unsupported flavour"})
1269 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1270 json(conn, get_user_flavour(user))
1273 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1277 defp get_user_flavour(_) do
1281 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1282 redirect(conn, to: local_mastodon_root_path(conn))
1285 @doc "Local Mastodon FE login init action"
1286 def login(conn, %{"code" => auth_token}) do
1287 with {:ok, app} <- get_or_make_app(),
1288 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1289 {:ok, token} <- Token.exchange_token(app, auth) do
1291 |> put_session(:oauth_token, token.token)
1292 |> redirect(to: local_mastodon_root_path(conn))
1296 @doc "Local Mastodon FE callback action"
1297 def login(conn, _) do
1298 with {:ok, app} <- get_or_make_app() do
1303 response_type: "code",
1304 client_id: app.client_id,
1306 scope: Enum.join(app.scopes, " ")
1310 |> redirect(to: path)
1314 defp local_mastodon_root_path(conn), do: mastodon_api_path(conn, :index, ["getting-started"])
1316 defp get_or_make_app do
1317 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1318 scopes = ["read", "write", "follow", "push"]
1320 with %App{} = app <- Repo.get_by(App, find_attrs) do
1322 if app.scopes == scopes do
1326 |> Ecto.Changeset.change(%{scopes: scopes})
1334 App.register_changeset(
1336 Map.put(find_attrs, :scopes, scopes)
1343 def logout(conn, _) do
1346 |> redirect(to: "/")
1349 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1350 Logger.debug("Unimplemented, returning unmodified relationship")
1352 with %User{} = target <- User.get_by_id(id) do
1354 |> put_view(AccountView)
1355 |> render("relationship.json", %{user: user, target: target})
1359 def empty_array(conn, _) do
1360 Logger.debug("Unimplemented, returning an empty array")
1364 def empty_object(conn, _) do
1365 Logger.debug("Unimplemented, returning an empty object")
1369 def get_filters(%{assigns: %{user: user}} = conn, _) do
1370 filters = Filter.get_filters(user)
1371 res = FilterView.render("filters.json", filters: filters)
1376 %{assigns: %{user: user}} = conn,
1377 %{"phrase" => phrase, "context" => context} = params
1383 hide: Map.get(params, "irreversible", nil),
1384 whole_word: Map.get(params, "boolean", true)
1388 {:ok, response} = Filter.create(query)
1389 res = FilterView.render("filter.json", filter: response)
1393 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1394 filter = Filter.get(filter_id, user)
1395 res = FilterView.render("filter.json", filter: filter)
1400 %{assigns: %{user: user}} = conn,
1401 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1405 filter_id: filter_id,
1408 hide: Map.get(params, "irreversible", nil),
1409 whole_word: Map.get(params, "boolean", true)
1413 {:ok, response} = Filter.update(query)
1414 res = FilterView.render("filter.json", filter: response)
1418 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1421 filter_id: filter_id
1424 {:ok, _} = Filter.delete(query)
1430 def errors(conn, _) do
1433 |> json("Something went wrong")
1436 def suggestions(%{assigns: %{user: user}} = conn, _) do
1437 suggestions = Config.get(:suggestions)
1439 if Keyword.get(suggestions, :enabled, false) do
1440 api = Keyword.get(suggestions, :third_party_engine, "")
1441 timeout = Keyword.get(suggestions, :timeout, 5000)
1442 limit = Keyword.get(suggestions, :limit, 23)
1444 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1446 user = user.nickname
1450 |> String.replace("{{host}}", host)
1451 |> String.replace("{{user}}", user)
1453 with {:ok, %{status: 200, body: body}} <-
1458 recv_timeout: timeout,
1462 {:ok, data} <- Jason.decode(body) do
1465 |> Enum.slice(0, limit)
1470 case User.get_or_fetch(x["acct"]) do
1477 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1480 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1486 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1493 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1494 with %Activity{} = activity <- Activity.get_by_id(status_id),
1495 true <- Visibility.visible_for_user?(activity, user) do
1499 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1509 def reports(%{assigns: %{user: user}} = conn, params) do
1510 case CommonAPI.report(user, params) do
1513 |> put_view(ReportView)
1514 |> try_render("report.json", %{activity: activity})
1518 |> put_status(:bad_request)
1519 |> json(%{error: err})
1523 def try_render(conn, target, params)
1524 when is_binary(target) do
1525 res = render(conn, target, params)
1530 |> json(%{error: "Can't display this activity"})
1536 def try_render(conn, _, _) do
1539 |> json(%{error: "Can't display this activity"})
1542 defp present?(nil), do: false
1543 defp present?(false), do: false
1544 defp present?(_), do: true