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} <- {:followed, User.get_by_id(id)},
819 {_, true} <- {:followed, follower.id != followed.id},
820 false <- User.following?(follower, followed),
821 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
823 |> put_view(AccountView)
824 |> render("relationship.json", %{user: follower, target: followed})
830 followed = User.get_cached_by_id(id)
833 case conn.params["reblogs"] do
834 true -> CommonAPI.show_reblogs(follower, followed)
835 false -> CommonAPI.hide_reblogs(follower, followed)
839 |> put_view(AccountView)
840 |> render("relationship.json", %{user: follower, target: followed})
844 |> put_resp_content_type("application/json")
845 |> send_resp(403, Jason.encode!(%{"error" => message}))
849 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
850 with {_, %User{} = followed} <- {:followed, User.get_by_nickname(uri)},
851 {_, true} <- {:followed, follower.id != followed.id},
852 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
854 |> put_view(AccountView)
855 |> render("account.json", %{user: followed, for: follower})
862 |> put_resp_content_type("application/json")
863 |> send_resp(403, Jason.encode!(%{"error" => message}))
867 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
868 with {_, %User{} = followed} <- {:followed, User.get_by_id(id)},
869 {_, true} <- {:followed, follower.id != followed.id},
870 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
872 |> put_view(AccountView)
873 |> render("relationship.json", %{user: follower, target: followed})
883 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
884 with %User{} = muted <- User.get_by_id(id),
885 {:ok, muter} <- User.mute(muter, muted) do
887 |> put_view(AccountView)
888 |> render("relationship.json", %{user: muter, target: muted})
892 |> put_resp_content_type("application/json")
893 |> send_resp(403, Jason.encode!(%{"error" => message}))
897 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
898 with %User{} = muted <- User.get_by_id(id),
899 {:ok, muter} <- User.unmute(muter, muted) do
901 |> put_view(AccountView)
902 |> render("relationship.json", %{user: muter, target: muted})
906 |> put_resp_content_type("application/json")
907 |> send_resp(403, Jason.encode!(%{"error" => message}))
911 def mutes(%{assigns: %{user: user}} = conn, _) do
912 with muted_accounts <- User.muted_users(user) do
913 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
918 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
919 with %User{} = blocked <- User.get_by_id(id),
920 {:ok, blocker} <- User.block(blocker, blocked),
921 {:ok, _activity} <- ActivityPub.block(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 unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
934 with %User{} = blocked <- User.get_by_id(id),
935 {:ok, blocker} <- User.unblock(blocker, blocked),
936 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: blocker, target: blocked})
943 |> put_resp_content_type("application/json")
944 |> send_resp(403, Jason.encode!(%{"error" => message}))
948 def blocks(%{assigns: %{user: user}} = conn, _) do
949 with blocked_accounts <- User.blocked_users(user) do
950 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
955 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
956 json(conn, info.domain_blocks || [])
959 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
960 User.block_domain(blocker, domain)
964 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
965 User.unblock_domain(blocker, domain)
969 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
970 with %User{} = subscription_target <- User.get_cached_by_id(id),
971 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
973 |> put_view(AccountView)
974 |> render("relationship.json", %{user: user, target: subscription_target})
978 |> put_resp_content_type("application/json")
979 |> send_resp(403, Jason.encode!(%{"error" => message}))
983 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
984 with %User{} = subscription_target <- User.get_cached_by_id(id),
985 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
987 |> put_view(AccountView)
988 |> render("relationship.json", %{user: user, target: subscription_target})
992 |> put_resp_content_type("application/json")
993 |> send_resp(403, Jason.encode!(%{"error" => message}))
997 def status_search(user, query) do
999 if Regex.match?(~r/https?:/, query) do
1000 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
1001 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1002 true <- Visibility.visible_for_user?(activity, user) do
1012 where: fragment("?->>'type' = 'Create'", a.data),
1013 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1016 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
1021 order_by: [desc: :id]
1024 Repo.all(q) ++ fetched
1027 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1028 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1030 statuses = status_search(user, query)
1032 tags_path = Web.base_url() <> "/tag/"
1038 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1039 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1040 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1043 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1045 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1052 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1053 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1055 statuses = status_search(user, query)
1061 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1062 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1065 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1067 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1074 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1075 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1077 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1082 def favourites(%{assigns: %{user: user}} = conn, params) do
1085 |> Map.put("type", "Create")
1086 |> Map.put("favorited_by", user.ap_id)
1087 |> Map.put("blocking_user", user)
1090 ActivityPub.fetch_activities([], params)
1094 |> add_link_headers(:favourites, activities)
1095 |> put_view(StatusView)
1096 |> render("index.json", %{activities: activities, for: user, as: :activity})
1099 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1100 user = User.get_by_id(user.id)
1104 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1108 |> put_view(StatusView)
1109 |> render("index.json", %{activities: activities, for: user, as: :activity})
1112 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1113 lists = Pleroma.List.for_user(user, opts)
1114 res = ListView.render("lists.json", lists: lists)
1118 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1119 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1120 res = ListView.render("list.json", list: list)
1126 |> json(%{error: "Record not found"})
1130 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1131 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1132 res = ListView.render("lists.json", lists: lists)
1136 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1137 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1138 {:ok, _list} <- Pleroma.List.delete(list) do
1146 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1147 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1148 res = ListView.render("list.json", list: list)
1153 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1155 |> Enum.each(fn account_id ->
1156 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1157 %User{} = followed <- User.get_by_id(account_id) do
1158 Pleroma.List.follow(list, followed)
1165 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1167 |> Enum.each(fn account_id ->
1168 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1169 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1170 Pleroma.List.unfollow(list, followed)
1177 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1178 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1179 {:ok, users} = Pleroma.List.get_following(list) do
1181 |> put_view(AccountView)
1182 |> render("accounts.json", %{users: users, as: :user})
1186 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1187 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1188 {:ok, list} <- Pleroma.List.rename(list, title) do
1189 res = ListView.render("list.json", list: list)
1197 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1198 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1201 |> Map.put("type", "Create")
1202 |> Map.put("blocking_user", user)
1203 |> Map.put("muting_user", user)
1205 # we must filter the following list for the user to avoid leaking statuses the user
1206 # does not actually have permission to see (for more info, peruse security issue #270).
1209 |> Enum.filter(fn x -> x in user.following end)
1210 |> ActivityPub.fetch_activities_bounded(following, params)
1214 |> put_view(StatusView)
1215 |> render("index.json", %{activities: activities, for: user, as: :activity})
1220 |> json(%{error: "Error."})
1224 def index(%{assigns: %{user: user}} = conn, _params) do
1225 token = get_session(conn, :oauth_token)
1228 mastodon_emoji = mastodonized_emoji()
1230 limit = Config.get([:instance, :limit])
1233 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1235 flavour = get_user_flavour(user)
1240 streaming_api_base_url:
1241 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1242 access_token: token,
1244 domain: Pleroma.Web.Endpoint.host(),
1247 unfollow_modal: false,
1250 auto_play_gif: false,
1251 display_sensitive_media: false,
1252 reduce_motion: false,
1253 max_toot_chars: limit,
1254 mascot: "/images/pleroma-fox-tan-smol.png"
1257 delete_others_notice: present?(user.info.is_moderator),
1258 admin: present?(user.info.is_admin)
1262 default_privacy: user.info.default_scope,
1263 default_sensitive: false,
1264 allow_content_types: Config.get([:instance, :allowed_post_formats])
1266 media_attachments: %{
1267 accept_content_types: [
1283 user.info.settings ||
1313 push_subscription: nil,
1315 custom_emojis: mastodon_emoji,
1321 |> put_layout(false)
1322 |> put_view(MastodonView)
1323 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1326 |> put_session(:return_to, conn.request_path)
1327 |> redirect(to: "/web/login")
1331 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1332 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1334 with changeset <- Ecto.Changeset.change(user),
1335 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1336 {:ok, _user} <- User.update_and_set_cache(changeset) do
1341 |> put_resp_content_type("application/json")
1342 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1346 @supported_flavours ["glitch", "vanilla"]
1348 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1349 when flavour in @supported_flavours do
1350 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1352 with changeset <- Ecto.Changeset.change(user),
1353 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1354 {:ok, user} <- User.update_and_set_cache(changeset),
1355 flavour <- user.info.flavour do
1360 |> put_resp_content_type("application/json")
1361 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1365 def set_flavour(conn, _params) do
1368 |> json(%{error: "Unsupported flavour"})
1371 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1372 json(conn, get_user_flavour(user))
1375 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1379 defp get_user_flavour(_) do
1383 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1384 redirect(conn, to: local_mastodon_root_path(conn))
1387 @doc "Local Mastodon FE login init action"
1388 def login(conn, %{"code" => auth_token}) do
1389 with {:ok, app} <- get_or_make_app(),
1390 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1391 {:ok, token} <- Token.exchange_token(app, auth) do
1393 |> put_session(:oauth_token, token.token)
1394 |> redirect(to: local_mastodon_root_path(conn))
1398 @doc "Local Mastodon FE callback action"
1399 def login(conn, _) do
1400 with {:ok, app} <- get_or_make_app() do
1405 response_type: "code",
1406 client_id: app.client_id,
1408 scope: Enum.join(app.scopes, " ")
1411 redirect(conn, to: path)
1415 defp local_mastodon_root_path(conn) do
1416 case get_session(conn, :return_to) do
1418 mastodon_api_path(conn, :index, ["getting-started"])
1421 delete_session(conn, :return_to)
1426 defp get_or_make_app do
1427 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1428 scopes = ["read", "write", "follow", "push"]
1430 with %App{} = app <- Repo.get_by(App, find_attrs) do
1432 if app.scopes == scopes do
1436 |> Ecto.Changeset.change(%{scopes: scopes})
1444 App.register_changeset(
1446 Map.put(find_attrs, :scopes, scopes)
1453 def logout(conn, _) do
1456 |> redirect(to: "/")
1459 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1460 Logger.debug("Unimplemented, returning unmodified relationship")
1462 with %User{} = target <- User.get_by_id(id) do
1464 |> put_view(AccountView)
1465 |> render("relationship.json", %{user: user, target: target})
1469 def empty_array(conn, _) do
1470 Logger.debug("Unimplemented, returning an empty array")
1474 def empty_object(conn, _) do
1475 Logger.debug("Unimplemented, returning an empty object")
1479 def get_filters(%{assigns: %{user: user}} = conn, _) do
1480 filters = Filter.get_filters(user)
1481 res = FilterView.render("filters.json", filters: filters)
1486 %{assigns: %{user: user}} = conn,
1487 %{"phrase" => phrase, "context" => context} = params
1493 hide: Map.get(params, "irreversible", nil),
1494 whole_word: Map.get(params, "boolean", true)
1498 {:ok, response} = Filter.create(query)
1499 res = FilterView.render("filter.json", filter: response)
1503 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1504 filter = Filter.get(filter_id, user)
1505 res = FilterView.render("filter.json", filter: filter)
1510 %{assigns: %{user: user}} = conn,
1511 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1515 filter_id: filter_id,
1518 hide: Map.get(params, "irreversible", nil),
1519 whole_word: Map.get(params, "boolean", true)
1523 {:ok, response} = Filter.update(query)
1524 res = FilterView.render("filter.json", filter: response)
1528 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1531 filter_id: filter_id
1534 {:ok, _} = Filter.delete(query)
1540 def errors(conn, {:error, %Changeset{} = changeset}) do
1543 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1544 |> Enum.map_join(", ", fn {_k, v} -> v end)
1548 |> json(%{error: error_message})
1551 def errors(conn, {:error, :not_found}) do
1554 |> json(%{error: "Record not found"})
1557 def errors(conn, _) do
1560 |> json("Something went wrong")
1563 def suggestions(%{assigns: %{user: user}} = conn, _) do
1564 suggestions = Config.get(:suggestions)
1566 if Keyword.get(suggestions, :enabled, false) do
1567 api = Keyword.get(suggestions, :third_party_engine, "")
1568 timeout = Keyword.get(suggestions, :timeout, 5000)
1569 limit = Keyword.get(suggestions, :limit, 23)
1571 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1573 user = user.nickname
1577 |> String.replace("{{host}}", host)
1578 |> String.replace("{{user}}", user)
1580 with {:ok, %{status: 200, body: body}} <-
1585 recv_timeout: timeout,
1589 {:ok, data} <- Jason.decode(body) do
1592 |> Enum.slice(0, limit)
1597 case User.get_or_fetch(x["acct"]) do
1604 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1607 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1613 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1620 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1621 with %Activity{} = activity <- Activity.get_by_id(status_id),
1622 true <- Visibility.visible_for_user?(activity, user) do
1626 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1636 def reports(%{assigns: %{user: user}} = conn, params) do
1637 case CommonAPI.report(user, params) do
1640 |> put_view(ReportView)
1641 |> try_render("report.json", %{activity: activity})
1645 |> put_status(:bad_request)
1646 |> json(%{error: err})
1650 def try_render(conn, target, params)
1651 when is_binary(target) do
1652 res = render(conn, target, params)
1657 |> json(%{error: "Can't display this activity"})
1663 def try_render(conn, _, _) do
1666 |> json(%{error: "Can't display this activity"})
1669 defp present?(nil), do: false
1670 defp present?(false), do: false
1671 defp present?(_), do: true