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
12 alias Pleroma.Notification
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 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", "min_id"])
209 last = List.last(activities)
216 |> Map.get("limit", "20")
217 |> String.to_integer()
220 if length(activities) <= limit do
226 |> Enum.at(limit * -1)
230 {next_url, prev_url} =
234 Pleroma.Web.Endpoint,
237 Map.merge(params, %{max_id: max_id})
240 Pleroma.Web.Endpoint,
243 Map.merge(params, %{min_id: min_id})
249 Pleroma.Web.Endpoint,
251 Map.merge(params, %{max_id: max_id})
254 Pleroma.Web.Endpoint,
256 Map.merge(params, %{min_id: min_id})
262 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
268 def home_timeline(%{assigns: %{user: user}} = conn, params) do
271 |> Map.put("type", ["Create", "Announce"])
272 |> Map.put("blocking_user", user)
273 |> Map.put("muting_user", user)
274 |> Map.put("user", user)
277 [user.ap_id | user.following]
278 |> ActivityPub.fetch_activities(params)
279 |> ActivityPub.contain_timeline(user)
283 |> add_link_headers(:home_timeline, activities)
284 |> put_view(StatusView)
285 |> render("index.json", %{activities: activities, for: user, as: :activity})
288 def public_timeline(%{assigns: %{user: user}} = conn, params) do
289 local_only = params["local"] in [true, "True", "true", "1"]
293 |> Map.put("type", ["Create", "Announce"])
294 |> Map.put("local_only", local_only)
295 |> Map.put("blocking_user", user)
296 |> Map.put("muting_user", user)
297 |> ActivityPub.fetch_public_activities()
301 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
302 |> put_view(StatusView)
303 |> render("index.json", %{activities: activities, for: user, as: :activity})
306 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
307 with %User{} = user <- User.get_by_id(params["id"]) do
308 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
311 |> add_link_headers(:user_statuses, activities, params["id"])
312 |> put_view(StatusView)
313 |> render("index.json", %{
314 activities: activities,
321 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
324 |> Map.put("type", "Create")
325 |> Map.put("blocking_user", user)
326 |> Map.put("user", user)
327 |> Map.put(:visibility, "direct")
331 |> ActivityPub.fetch_activities_query(params)
332 |> Pagination.fetch_paginated(params)
335 |> add_link_headers(:dm_timeline, activities)
336 |> put_view(StatusView)
337 |> render("index.json", %{activities: activities, for: user, as: :activity})
340 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
341 with %Activity{} = activity <- Activity.get_by_id(id),
342 true <- Visibility.visible_for_user?(activity, user) do
344 |> put_view(StatusView)
345 |> try_render("status.json", %{activity: activity, for: user})
349 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
350 with %Activity{} = activity <- Activity.get_by_id(id),
352 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
353 "blocking_user" => user,
357 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
359 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
360 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
366 activities: grouped_activities[true] || [],
370 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
375 activities: grouped_activities[false] || [],
379 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
386 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
387 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
389 |> add_link_headers(:scheduled_statuses, scheduled_activities)
390 |> put_view(ScheduledActivityView)
391 |> render("index.json", %{scheduled_activities: scheduled_activities})
395 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
396 with %ScheduledActivity{} = scheduled_activity <-
397 ScheduledActivity.get(user, scheduled_activity_id) do
399 |> put_view(ScheduledActivityView)
400 |> render("show.json", %{scheduled_activity: scheduled_activity})
402 _ -> {:error, :not_found}
406 def update_scheduled_status(
407 %{assigns: %{user: user}} = conn,
408 %{"id" => scheduled_activity_id} = params
410 with %ScheduledActivity{} = scheduled_activity <-
411 ScheduledActivity.get(user, scheduled_activity_id),
412 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
414 |> put_view(ScheduledActivityView)
415 |> render("show.json", %{scheduled_activity: scheduled_activity})
417 nil -> {:error, :not_found}
422 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
423 with %ScheduledActivity{} = scheduled_activity <-
424 ScheduledActivity.get(user, scheduled_activity_id),
425 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
427 |> put_view(ScheduledActivityView)
428 |> render("show.json", %{scheduled_activity: scheduled_activity})
430 nil -> {:error, :not_found}
435 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
436 when length(media_ids) > 0 do
439 |> Map.put("status", ".")
441 post_status(conn, params)
444 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
447 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
450 case get_req_header(conn, "idempotency-key") do
452 _ -> Ecto.UUID.generate()
455 scheduled_at = params["scheduled_at"]
457 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
458 with {:ok, scheduled_activity} <-
459 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
461 |> put_view(ScheduledActivityView)
462 |> render("show.json", %{scheduled_activity: scheduled_activity})
465 params = Map.drop(params, ["scheduled_at"])
468 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
469 CommonAPI.post(user, params)
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
479 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
485 |> json(%{error: "Can't delete this post"})
489 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
490 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
492 |> put_view(StatusView)
493 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
497 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
498 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
499 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
501 |> put_view(StatusView)
502 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
506 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
507 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
508 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
510 |> put_view(StatusView)
511 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
515 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
516 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
519 |> put_view(StatusView)
520 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
524 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
525 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
527 |> put_view(StatusView)
528 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
532 |> put_resp_content_type("application/json")
533 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
537 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
538 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
540 |> put_view(StatusView)
541 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
545 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
546 with %Activity{} = activity <- Activity.get_by_id(id),
547 %User{} = user <- User.get_by_nickname(user.nickname),
548 true <- Visibility.visible_for_user?(activity, user),
549 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
551 |> put_view(StatusView)
552 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
556 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
557 with %Activity{} = activity <- Activity.get_by_id(id),
558 %User{} = user <- User.get_by_nickname(user.nickname),
559 true <- Visibility.visible_for_user?(activity, user),
560 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
562 |> put_view(StatusView)
563 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
567 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
568 activity = Activity.get_by_id(id)
570 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
572 |> put_view(StatusView)
573 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
577 |> put_resp_content_type("application/json")
578 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
582 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
583 activity = Activity.get_by_id(id)
585 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
587 |> put_view(StatusView)
588 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
592 def notifications(%{assigns: %{user: user}} = conn, params) do
593 notifications = MastodonAPI.get_notifications(user, params)
596 |> add_link_headers(:notifications, notifications)
597 |> put_view(NotificationView)
598 |> render("index.json", %{notifications: notifications, for: user})
601 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
602 with {:ok, notification} <- Notification.get(user, id) do
604 |> put_view(NotificationView)
605 |> render("show.json", %{notification: notification, for: user})
609 |> put_resp_content_type("application/json")
610 |> send_resp(403, Jason.encode!(%{"error" => reason}))
614 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
615 Notification.clear(user)
619 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
620 with {:ok, _notif} <- Notification.dismiss(user, id) do
625 |> put_resp_content_type("application/json")
626 |> send_resp(403, Jason.encode!(%{"error" => reason}))
630 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
631 Notification.destroy_multiple(user, ids)
635 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
637 q = from(u in User, where: u.id in ^id)
638 targets = Repo.all(q)
641 |> put_view(AccountView)
642 |> render("relationships.json", %{user: user, targets: targets})
645 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
646 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
648 def update_media(%{assigns: %{user: user}} = conn, data) do
649 with %Object{} = object <- Repo.get(Object, data["id"]),
650 true <- Object.authorize_mutation(object, user),
651 true <- is_binary(data["description"]),
652 description <- data["description"] do
653 new_data = %{object.data | "name" => description}
657 |> Object.change(%{data: new_data})
660 attachment_data = Map.put(new_data, "id", object.id)
663 |> put_view(StatusView)
664 |> render("attachment.json", %{attachment: attachment_data})
668 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
669 with {:ok, object} <-
672 actor: User.ap_id(user),
673 description: Map.get(data, "description")
675 attachment_data = Map.put(object.data, "id", object.id)
678 |> put_view(StatusView)
679 |> render("attachment.json", %{attachment: attachment_data})
683 def favourited_by(conn, %{"id" => id}) do
684 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
685 q = from(u in User, where: u.ap_id in ^likes)
689 |> put_view(AccountView)
690 |> render(AccountView, "accounts.json", %{users: users, as: :user})
696 def reblogged_by(conn, %{"id" => id}) do
697 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
698 q = from(u in User, where: u.ap_id in ^announces)
702 |> put_view(AccountView)
703 |> render("accounts.json", %{users: users, as: :user})
709 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
710 local_only = params["local"] in [true, "True", "true", "1"]
713 [params["tag"], params["any"]]
717 |> Enum.map(&String.downcase(&1))
722 |> Enum.map(&String.downcase(&1))
727 |> Enum.map(&String.downcase(&1))
731 |> Map.put("type", "Create")
732 |> Map.put("local_only", local_only)
733 |> Map.put("blocking_user", user)
734 |> Map.put("muting_user", user)
735 |> Map.put("tag", tags)
736 |> Map.put("tag_all", tag_all)
737 |> Map.put("tag_reject", tag_reject)
738 |> ActivityPub.fetch_public_activities()
742 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
743 |> put_view(StatusView)
744 |> render("index.json", %{activities: activities, for: user, as: :activity})
747 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
748 with %User{} = user <- User.get_by_id(id),
749 followers <- MastodonAPI.get_followers(user, params) do
752 for_user && user.id == for_user.id -> followers
753 user.info.hide_followers -> []
758 |> add_link_headers(:followers, followers, user)
759 |> put_view(AccountView)
760 |> render("accounts.json", %{users: followers, as: :user})
764 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
765 with %User{} = user <- User.get_by_id(id),
766 followers <- MastodonAPI.get_friends(user, params) do
769 for_user && user.id == for_user.id -> followers
770 user.info.hide_follows -> []
775 |> add_link_headers(:following, followers, user)
776 |> put_view(AccountView)
777 |> render("accounts.json", %{users: followers, as: :user})
781 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
782 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
784 |> put_view(AccountView)
785 |> render("accounts.json", %{users: follow_requests, as: :user})
789 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
790 with %User{} = follower <- User.get_by_id(id),
791 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
793 |> put_view(AccountView)
794 |> render("relationship.json", %{user: followed, target: follower})
798 |> put_resp_content_type("application/json")
799 |> send_resp(403, Jason.encode!(%{"error" => message}))
803 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
804 with %User{} = follower <- User.get_by_id(id),
805 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
807 |> put_view(AccountView)
808 |> render("relationship.json", %{user: followed, target: follower})
812 |> put_resp_content_type("application/json")
813 |> send_resp(403, Jason.encode!(%{"error" => message}))
817 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
818 with %User{} = followed <- User.get_by_id(id),
819 false <- User.following?(follower, followed),
820 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
822 |> put_view(AccountView)
823 |> render("relationship.json", %{user: follower, target: followed})
826 followed = User.get_cached_by_id(id)
829 case conn.params["reblogs"] do
830 true -> CommonAPI.show_reblogs(follower, followed)
831 false -> CommonAPI.hide_reblogs(follower, followed)
835 |> put_view(AccountView)
836 |> render("relationship.json", %{user: follower, target: followed})
840 |> put_resp_content_type("application/json")
841 |> send_resp(403, Jason.encode!(%{"error" => message}))
845 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
846 with %User{} = followed <- User.get_by_nickname(uri),
847 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
849 |> put_view(AccountView)
850 |> render("account.json", %{user: followed, for: follower})
854 |> put_resp_content_type("application/json")
855 |> send_resp(403, Jason.encode!(%{"error" => message}))
859 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
860 with %User{} = followed <- User.get_by_id(id),
861 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
863 |> put_view(AccountView)
864 |> render("relationship.json", %{user: follower, target: followed})
868 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
869 with %User{} = muted <- User.get_by_id(id),
870 {:ok, muter} <- User.mute(muter, muted) do
872 |> put_view(AccountView)
873 |> render("relationship.json", %{user: muter, target: muted})
877 |> put_resp_content_type("application/json")
878 |> send_resp(403, Jason.encode!(%{"error" => message}))
882 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
883 with %User{} = muted <- User.get_by_id(id),
884 {:ok, muter} <- User.unmute(muter, muted) do
886 |> put_view(AccountView)
887 |> render("relationship.json", %{user: muter, target: muted})
891 |> put_resp_content_type("application/json")
892 |> send_resp(403, Jason.encode!(%{"error" => message}))
896 def mutes(%{assigns: %{user: user}} = conn, _) do
897 with muted_accounts <- User.muted_users(user) do
898 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
903 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
904 with %User{} = blocked <- User.get_by_id(id),
905 {:ok, blocker} <- User.block(blocker, blocked),
906 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
908 |> put_view(AccountView)
909 |> render("relationship.json", %{user: blocker, target: blocked})
913 |> put_resp_content_type("application/json")
914 |> send_resp(403, Jason.encode!(%{"error" => message}))
918 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
919 with %User{} = blocked <- User.get_by_id(id),
920 {:ok, blocker} <- User.unblock(blocker, blocked),
921 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
923 |> put_view(AccountView)
924 |> render("relationship.json", %{user: blocker, target: blocked})
928 |> put_resp_content_type("application/json")
929 |> send_resp(403, Jason.encode!(%{"error" => message}))
933 def blocks(%{assigns: %{user: user}} = conn, _) do
934 with blocked_accounts <- User.blocked_users(user) do
935 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
940 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
941 json(conn, info.domain_blocks || [])
944 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
945 User.block_domain(blocker, domain)
949 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
950 User.unblock_domain(blocker, domain)
954 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
955 with %User{} = subscription_target <- User.get_cached_by_id(id),
956 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
958 |> put_view(AccountView)
959 |> render("relationship.json", %{user: user, target: subscription_target})
963 |> put_resp_content_type("application/json")
964 |> send_resp(403, Jason.encode!(%{"error" => message}))
968 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
969 with %User{} = subscription_target <- User.get_cached_by_id(id),
970 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
972 |> put_view(AccountView)
973 |> render("relationship.json", %{user: user, target: subscription_target})
977 |> put_resp_content_type("application/json")
978 |> send_resp(403, Jason.encode!(%{"error" => message}))
982 def status_search(user, query) do
984 if Regex.match?(~r/https?:/, query) do
985 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
986 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
987 true <- Visibility.visible_for_user?(activity, user) do
997 where: fragment("?->>'type' = 'Create'", a.data),
998 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1001 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
1006 order_by: [desc: :id]
1009 Repo.all(q) ++ fetched
1012 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1013 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1015 statuses = status_search(user, query)
1017 tags_path = Web.base_url() <> "/tag/"
1023 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1024 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1025 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1028 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1030 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1037 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1038 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1040 statuses = status_search(user, query)
1046 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1047 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1050 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1052 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1059 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1060 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1062 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1067 def favourites(%{assigns: %{user: user}} = conn, params) do
1070 |> Map.put("type", "Create")
1071 |> Map.put("favorited_by", user.ap_id)
1072 |> Map.put("blocking_user", user)
1075 ActivityPub.fetch_activities([], params)
1079 |> add_link_headers(:favourites, activities)
1080 |> put_view(StatusView)
1081 |> render("index.json", %{activities: activities, for: user, as: :activity})
1084 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1085 user = User.get_by_id(user.id)
1089 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1093 |> put_view(StatusView)
1094 |> render("index.json", %{activities: activities, for: user, as: :activity})
1097 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1098 lists = Pleroma.List.for_user(user, opts)
1099 res = ListView.render("lists.json", lists: lists)
1103 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1104 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1105 res = ListView.render("list.json", list: list)
1111 |> json(%{error: "Record not found"})
1115 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1116 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1117 res = ListView.render("lists.json", lists: lists)
1121 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1122 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1123 {:ok, _list} <- Pleroma.List.delete(list) do
1131 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1132 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1133 res = ListView.render("list.json", list: list)
1138 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1140 |> Enum.each(fn account_id ->
1141 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1142 %User{} = followed <- User.get_by_id(account_id) do
1143 Pleroma.List.follow(list, followed)
1150 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1152 |> Enum.each(fn account_id ->
1153 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1154 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1155 Pleroma.List.unfollow(list, followed)
1162 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1163 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1164 {:ok, users} = Pleroma.List.get_following(list) do
1166 |> put_view(AccountView)
1167 |> render("accounts.json", %{users: users, as: :user})
1171 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1172 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1173 {:ok, list} <- Pleroma.List.rename(list, title) do
1174 res = ListView.render("list.json", list: list)
1182 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1183 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1186 |> Map.put("type", "Create")
1187 |> Map.put("blocking_user", user)
1188 |> Map.put("muting_user", user)
1190 # we must filter the following list for the user to avoid leaking statuses the user
1191 # does not actually have permission to see (for more info, peruse security issue #270).
1194 |> Enum.filter(fn x -> x in user.following end)
1195 |> ActivityPub.fetch_activities_bounded(following, params)
1199 |> put_view(StatusView)
1200 |> render("index.json", %{activities: activities, for: user, as: :activity})
1205 |> json(%{error: "Error."})
1209 def index(%{assigns: %{user: user}} = conn, _params) do
1210 token = get_session(conn, :oauth_token)
1213 mastodon_emoji = mastodonized_emoji()
1215 limit = Config.get([:instance, :limit])
1218 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1220 flavour = get_user_flavour(user)
1225 streaming_api_base_url:
1226 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1227 access_token: token,
1229 domain: Pleroma.Web.Endpoint.host(),
1232 unfollow_modal: false,
1235 auto_play_gif: false,
1236 display_sensitive_media: false,
1237 reduce_motion: false,
1238 max_toot_chars: limit,
1239 mascot: "/images/pleroma-fox-tan-smol.png"
1242 delete_others_notice: present?(user.info.is_moderator),
1243 admin: present?(user.info.is_admin)
1247 default_privacy: user.info.default_scope,
1248 default_sensitive: false,
1249 allow_content_types: Config.get([:instance, :allowed_post_formats])
1251 media_attachments: %{
1252 accept_content_types: [
1268 user.info.settings ||
1298 push_subscription: nil,
1300 custom_emojis: mastodon_emoji,
1306 |> put_layout(false)
1307 |> put_view(MastodonView)
1308 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1311 |> put_session(:return_to, conn.request_path)
1312 |> redirect(to: "/web/login")
1316 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1317 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1319 with changeset <- Ecto.Changeset.change(user),
1320 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1321 {:ok, _user} <- User.update_and_set_cache(changeset) do
1326 |> put_resp_content_type("application/json")
1327 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1331 @supported_flavours ["glitch", "vanilla"]
1333 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1334 when flavour in @supported_flavours do
1335 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1337 with changeset <- Ecto.Changeset.change(user),
1338 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1339 {:ok, user} <- User.update_and_set_cache(changeset),
1340 flavour <- user.info.flavour do
1345 |> put_resp_content_type("application/json")
1346 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1350 def set_flavour(conn, _params) do
1353 |> json(%{error: "Unsupported flavour"})
1356 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1357 json(conn, get_user_flavour(user))
1360 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1364 defp get_user_flavour(_) do
1368 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1369 redirect(conn, to: local_mastodon_root_path(conn))
1372 @doc "Local Mastodon FE login init action"
1373 def login(conn, %{"code" => auth_token}) do
1374 with {:ok, app} <- get_or_make_app(),
1375 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1376 {:ok, token} <- Token.exchange_token(app, auth) do
1378 |> put_session(:oauth_token, token.token)
1379 |> redirect(to: local_mastodon_root_path(conn))
1383 @doc "Local Mastodon FE callback action"
1384 def login(conn, _) do
1385 with {:ok, app} <- get_or_make_app() do
1390 response_type: "code",
1391 client_id: app.client_id,
1393 scope: Enum.join(app.scopes, " ")
1396 redirect(conn, to: path)
1400 defp local_mastodon_root_path(conn) do
1401 case get_session(conn, :return_to) do
1403 mastodon_api_path(conn, :index, ["getting-started"])
1406 delete_session(conn, :return_to)
1411 defp get_or_make_app do
1412 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1413 scopes = ["read", "write", "follow", "push"]
1415 with %App{} = app <- Repo.get_by(App, find_attrs) do
1417 if app.scopes == scopes do
1421 |> Ecto.Changeset.change(%{scopes: scopes})
1429 App.register_changeset(
1431 Map.put(find_attrs, :scopes, scopes)
1438 def logout(conn, _) do
1441 |> redirect(to: "/")
1444 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1445 Logger.debug("Unimplemented, returning unmodified relationship")
1447 with %User{} = target <- User.get_by_id(id) do
1449 |> put_view(AccountView)
1450 |> render("relationship.json", %{user: user, target: target})
1454 def empty_array(conn, _) do
1455 Logger.debug("Unimplemented, returning an empty array")
1459 def empty_object(conn, _) do
1460 Logger.debug("Unimplemented, returning an empty object")
1464 def get_filters(%{assigns: %{user: user}} = conn, _) do
1465 filters = Filter.get_filters(user)
1466 res = FilterView.render("filters.json", filters: filters)
1471 %{assigns: %{user: user}} = conn,
1472 %{"phrase" => phrase, "context" => context} = params
1478 hide: Map.get(params, "irreversible", nil),
1479 whole_word: Map.get(params, "boolean", true)
1483 {:ok, response} = Filter.create(query)
1484 res = FilterView.render("filter.json", filter: response)
1488 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1489 filter = Filter.get(filter_id, user)
1490 res = FilterView.render("filter.json", filter: filter)
1495 %{assigns: %{user: user}} = conn,
1496 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1500 filter_id: filter_id,
1503 hide: Map.get(params, "irreversible", nil),
1504 whole_word: Map.get(params, "boolean", true)
1508 {:ok, response} = Filter.update(query)
1509 res = FilterView.render("filter.json", filter: response)
1513 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1516 filter_id: filter_id
1519 {:ok, _} = Filter.delete(query)
1525 def errors(conn, {:error, %Changeset{} = changeset}) do
1528 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1529 |> Enum.map_join(", ", fn {_k, v} -> v end)
1533 |> json(%{error: error_message})
1536 def errors(conn, {:error, :not_found}) do
1539 |> json(%{error: "Record not found"})
1542 def errors(conn, _) do
1545 |> json("Something went wrong")
1548 def suggestions(%{assigns: %{user: user}} = conn, _) do
1549 suggestions = Config.get(:suggestions)
1551 if Keyword.get(suggestions, :enabled, false) do
1552 api = Keyword.get(suggestions, :third_party_engine, "")
1553 timeout = Keyword.get(suggestions, :timeout, 5000)
1554 limit = Keyword.get(suggestions, :limit, 23)
1556 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1558 user = user.nickname
1562 |> String.replace("{{host}}", host)
1563 |> String.replace("{{user}}", user)
1565 with {:ok, %{status: 200, body: body}} <-
1570 recv_timeout: timeout,
1574 {:ok, data} <- Jason.decode(body) do
1577 |> Enum.slice(0, limit)
1582 case User.get_or_fetch(x["acct"]) do
1589 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1592 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1598 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1605 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1606 with %Activity{} = activity <- Activity.get_by_id(status_id),
1607 true <- Visibility.visible_for_user?(activity, user) do
1611 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1621 def reports(%{assigns: %{user: user}} = conn, params) do
1622 case CommonAPI.report(user, params) do
1625 |> put_view(ReportView)
1626 |> try_render("report.json", %{activity: activity})
1630 |> put_status(:bad_request)
1631 |> json(%{error: err})
1635 def try_render(conn, target, params)
1636 when is_binary(target) do
1637 res = render(conn, target, params)
1642 |> json(%{error: "Can't display this activity"})
1648 def try_render(conn, _, _) do
1651 |> json(%{error: "Can't display this activity"})
1654 defp present?(nil), do: false
1655 defp present?(false), do: false
1656 defp present?(_), do: true