1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Conversation.Participation
13 alias Pleroma.Formatter
15 alias Pleroma.Notification
17 alias Pleroma.Pagination
19 alias Pleroma.ScheduledActivity
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
44 alias Pleroma.Web.ControllerHelper
50 Pleroma.Plugs.RateLimitPlug,
52 max_requests: Config.get([:app_account_creation, :max_requests]),
53 interval: Config.get([:app_account_creation, :interval])
55 when action in [:account_register]
58 plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search])
60 @local_mastodon_name "Mastodon-Local"
62 action_fallback(:errors)
64 def create_app(conn, params) do
65 scopes = Scopes.fetch_scopes(params, ["read"])
69 |> Map.drop(["scope", "scopes"])
70 |> Map.put("scopes", scopes)
72 with cs <- App.register_changeset(%App{}, app_attrs),
73 false <- cs.changes[:client_name] == @local_mastodon_name,
74 {:ok, app} <- Repo.insert(cs) do
77 |> render("show.json", %{app: app})
86 value_function \\ fn x -> {:ok, x} end
88 if Map.has_key?(params, params_field) do
89 case value_function.(params[params_field]) do
90 {:ok, new_value} -> Map.put(map, map_field, new_value)
98 def update_credentials(%{assigns: %{user: user}} = conn, params) do
103 |> add_if_present(params, "display_name", :name)
104 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
105 |> add_if_present(params, "avatar", :avatar, fn value ->
106 with %Plug.Upload{} <- value,
107 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
114 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
117 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
128 :skip_thread_containment
130 |> Enum.reduce(%{}, fn key, acc ->
131 add_if_present(acc, params, to_string(key), key, fn value ->
132 {:ok, ControllerHelper.truthy_param?(value)}
135 |> add_if_present(params, "default_scope", :default_scope)
136 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
137 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
139 |> add_if_present(params, "header", :banner, fn value ->
140 with %Plug.Upload{} <- value,
141 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
147 |> Map.put(:emoji, user_info_emojis)
149 info_cng = User.Info.profile_update(user.info, info_params)
151 with changeset <- User.update_changeset(user, user_params),
152 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
153 {:ok, user} <- User.update_and_set_cache(changeset) do
154 if original_user != user do
155 CommonAPI.update(user)
160 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
166 |> json(%{error: "Invalid request"})
170 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
172 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
177 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
178 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
181 |> render("short.json", %{app: app})
185 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
186 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
187 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
188 account = AccountView.render("account.json", %{user: user, for: for_user})
194 |> json(%{error: "Can't find user"})
198 @mastodon_api_level "2.7.2"
200 def masto_instance(conn, _params) do
201 instance = Config.get(:instance)
205 title: Keyword.get(instance, :name),
206 description: Keyword.get(instance, :description),
207 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
208 email: Keyword.get(instance, :email),
210 streaming_api: Pleroma.Web.Endpoint.websocket_url()
212 stats: Stats.get_stats(),
213 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
215 registrations: Pleroma.Config.get([:instance, :registrations_open]),
216 # Extra (not present in Mastodon):
217 max_toot_chars: Keyword.get(instance, :limit),
218 poll_limits: Keyword.get(instance, :poll_limits)
224 def peers(conn, _params) do
225 json(conn, Stats.get_peers())
228 defp mastodonized_emoji do
229 Pleroma.Emoji.get_all()
230 |> Enum.map(fn {shortcode, relative_url, tags} ->
231 url = to_string(URI.merge(Web.base_url(), relative_url))
234 "shortcode" => shortcode,
236 "visible_in_picker" => true,
243 def custom_emojis(conn, _params) do
244 mastodon_emoji = mastodonized_emoji()
245 json(conn, mastodon_emoji)
248 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
251 |> Map.drop(["since_id", "max_id", "min_id"])
254 last = List.last(activities)
261 |> Map.get("limit", "20")
262 |> String.to_integer()
265 if length(activities) <= limit do
271 |> Enum.at(limit * -1)
275 {next_url, prev_url} =
279 Pleroma.Web.Endpoint,
282 Map.merge(params, %{max_id: max_id})
285 Pleroma.Web.Endpoint,
288 Map.merge(params, %{min_id: min_id})
294 Pleroma.Web.Endpoint,
296 Map.merge(params, %{max_id: max_id})
299 Pleroma.Web.Endpoint,
301 Map.merge(params, %{min_id: min_id})
307 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
313 def home_timeline(%{assigns: %{user: user}} = conn, params) do
316 |> Map.put("type", ["Create", "Announce"])
317 |> Map.put("blocking_user", user)
318 |> Map.put("muting_user", user)
319 |> Map.put("user", user)
322 [user.ap_id | user.following]
323 |> ActivityPub.fetch_activities(params)
327 |> add_link_headers(:home_timeline, activities)
328 |> put_view(StatusView)
329 |> render("index.json", %{activities: activities, for: user, as: :activity})
332 def public_timeline(%{assigns: %{user: user}} = conn, params) do
333 local_only = params["local"] in [true, "True", "true", "1"]
337 |> Map.put("type", ["Create", "Announce"])
338 |> Map.put("local_only", local_only)
339 |> Map.put("blocking_user", user)
340 |> Map.put("muting_user", user)
341 |> ActivityPub.fetch_public_activities()
345 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
346 |> put_view(StatusView)
347 |> render("index.json", %{activities: activities, for: user, as: :activity})
350 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
351 with %User{} = user <- User.get_cached_by_id(params["id"]) do
352 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
355 |> add_link_headers(:user_statuses, activities, params["id"])
356 |> put_view(StatusView)
357 |> render("index.json", %{
358 activities: activities,
365 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
368 |> Map.put("type", "Create")
369 |> Map.put("blocking_user", user)
370 |> Map.put("user", user)
371 |> Map.put(:visibility, "direct")
375 |> ActivityPub.fetch_activities_query(params)
376 |> Pagination.fetch_paginated(params)
379 |> add_link_headers(:dm_timeline, activities)
380 |> put_view(StatusView)
381 |> render("index.json", %{activities: activities, for: user, as: :activity})
384 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
385 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
386 true <- Visibility.visible_for_user?(activity, user) do
388 |> put_view(StatusView)
389 |> try_render("status.json", %{activity: activity, for: user})
393 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
394 with %Activity{} = activity <- Activity.get_by_id(id),
396 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
397 "blocking_user" => user,
401 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
403 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
404 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
410 activities: grouped_activities[true] || [],
414 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
419 activities: grouped_activities[false] || [],
423 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
430 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
431 with %Object{} = object <- Object.get_by_id(id),
432 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
433 true <- Visibility.visible_for_user?(activity, user) do
435 |> put_view(StatusView)
436 |> try_render("poll.json", %{object: object, for: user})
441 |> json(%{error: "Record not found"})
446 |> json(%{error: "Record not found"})
450 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
451 with %Object{} = object <- Object.get_by_id(id),
452 true <- object.data["type"] == "Question",
453 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
454 true <- Visibility.visible_for_user?(activity, user),
455 {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do
457 |> put_view(StatusView)
458 |> try_render("poll.json", %{object: object, for: user})
463 |> json(%{error: "Record not found"})
468 |> json(%{error: "Record not found"})
473 |> json(%{error: message})
477 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
478 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
480 |> add_link_headers(:scheduled_statuses, scheduled_activities)
481 |> put_view(ScheduledActivityView)
482 |> render("index.json", %{scheduled_activities: scheduled_activities})
486 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
487 with %ScheduledActivity{} = scheduled_activity <-
488 ScheduledActivity.get(user, scheduled_activity_id) do
490 |> put_view(ScheduledActivityView)
491 |> render("show.json", %{scheduled_activity: scheduled_activity})
493 _ -> {:error, :not_found}
497 def update_scheduled_status(
498 %{assigns: %{user: user}} = conn,
499 %{"id" => scheduled_activity_id} = params
501 with %ScheduledActivity{} = scheduled_activity <-
502 ScheduledActivity.get(user, scheduled_activity_id),
503 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
505 |> put_view(ScheduledActivityView)
506 |> render("show.json", %{scheduled_activity: scheduled_activity})
508 nil -> {:error, :not_found}
513 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
514 with %ScheduledActivity{} = scheduled_activity <-
515 ScheduledActivity.get(user, scheduled_activity_id),
516 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
518 |> put_view(ScheduledActivityView)
519 |> render("show.json", %{scheduled_activity: scheduled_activity})
521 nil -> {:error, :not_found}
526 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
527 when length(media_ids) > 0 do
530 |> Map.put("status", ".")
532 post_status(conn, params)
535 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
538 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
540 scheduled_at = params["scheduled_at"]
542 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
543 with {:ok, scheduled_activity} <-
544 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
546 |> put_view(ScheduledActivityView)
547 |> render("show.json", %{scheduled_activity: scheduled_activity})
550 params = Map.drop(params, ["scheduled_at"])
552 case get_cached_status_or_post(conn, params) do
553 {:ignore, message} ->
556 |> json(%{error: message})
561 |> json(%{error: message})
565 |> put_view(StatusView)
566 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
571 defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do
573 case get_req_header(conn, "idempotency-key") do
575 _ -> Ecto.UUID.generate()
578 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
579 case CommonAPI.post(user, params) do
580 {:ok, activity} -> activity
581 {:error, message} -> {:ignore, message}
586 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
587 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
593 |> json(%{error: "Can't delete this post"})
597 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
598 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
599 %Activity{} = announce <- Activity.normalize(announce.data) do
601 |> put_view(StatusView)
602 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
606 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
607 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
608 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
610 |> put_view(StatusView)
611 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
615 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
616 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
617 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
619 |> put_view(StatusView)
620 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
624 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
625 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
626 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
628 |> put_view(StatusView)
629 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
633 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
634 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
636 |> put_view(StatusView)
637 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
641 |> put_resp_content_type("application/json")
642 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
646 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
647 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
649 |> put_view(StatusView)
650 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
654 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
655 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
656 %User{} = user <- User.get_cached_by_nickname(user.nickname),
657 true <- Visibility.visible_for_user?(activity, user),
658 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
665 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
666 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
667 %User{} = user <- User.get_cached_by_nickname(user.nickname),
668 true <- Visibility.visible_for_user?(activity, user),
669 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
671 |> put_view(StatusView)
672 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
676 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
677 activity = Activity.get_by_id(id)
679 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
681 |> put_view(StatusView)
682 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
686 |> put_resp_content_type("application/json")
687 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
691 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
692 activity = Activity.get_by_id(id)
694 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
696 |> put_view(StatusView)
697 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
701 def notifications(%{assigns: %{user: user}} = conn, params) do
702 notifications = MastodonAPI.get_notifications(user, params)
705 |> add_link_headers(:notifications, notifications)
706 |> put_view(NotificationView)
707 |> render("index.json", %{notifications: notifications, for: user})
710 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
711 with {:ok, notification} <- Notification.get(user, id) do
713 |> put_view(NotificationView)
714 |> render("show.json", %{notification: notification, for: user})
718 |> put_resp_content_type("application/json")
719 |> send_resp(403, Jason.encode!(%{"error" => reason}))
723 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
724 Notification.clear(user)
728 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
729 with {:ok, _notif} <- Notification.dismiss(user, id) do
734 |> put_resp_content_type("application/json")
735 |> send_resp(403, Jason.encode!(%{"error" => reason}))
739 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
740 Notification.destroy_multiple(user, ids)
744 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
746 q = from(u in User, where: u.id in ^id)
747 targets = Repo.all(q)
750 |> put_view(AccountView)
751 |> render("relationships.json", %{user: user, targets: targets})
754 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
755 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
757 def update_media(%{assigns: %{user: user}} = conn, data) do
758 with %Object{} = object <- Repo.get(Object, data["id"]),
759 true <- Object.authorize_mutation(object, user),
760 true <- is_binary(data["description"]),
761 description <- data["description"] do
762 new_data = %{object.data | "name" => description}
766 |> Object.change(%{data: new_data})
769 attachment_data = Map.put(new_data, "id", object.id)
772 |> put_view(StatusView)
773 |> render("attachment.json", %{attachment: attachment_data})
777 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
778 with {:ok, object} <-
781 actor: User.ap_id(user),
782 description: Map.get(data, "description")
784 attachment_data = Map.put(object.data, "id", object.id)
787 |> put_view(StatusView)
788 |> render("attachment.json", %{attachment: attachment_data})
792 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
793 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
794 %{} = attachment_data <- Map.put(object.data, "id", object.id),
795 %{type: type} = rendered <-
796 StatusView.render("attachment.json", %{attachment: attachment_data}) do
797 # Reject if not an image
798 if type == "image" do
800 # Save to the user's info
801 info_changeset = User.Info.mascot_update(user.info, rendered)
805 |> Ecto.Changeset.change()
806 |> Ecto.Changeset.put_embed(:info, info_changeset)
808 {:ok, _user} = User.update_and_set_cache(user_changeset)
814 |> put_resp_content_type("application/json")
815 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
820 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
821 mascot = User.get_mascot(user)
827 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
828 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
829 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
830 q = from(u in User, where: u.ap_id in ^likes)
834 |> put_view(AccountView)
835 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
841 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
842 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
843 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
844 q = from(u in User, where: u.ap_id in ^announces)
848 |> put_view(AccountView)
849 |> render("accounts.json", %{for: user, users: users, as: :user})
855 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
856 local_only = params["local"] in [true, "True", "true", "1"]
859 [params["tag"], params["any"]]
863 |> Enum.map(&String.downcase(&1))
868 |> Enum.map(&String.downcase(&1))
873 |> Enum.map(&String.downcase(&1))
877 |> Map.put("type", "Create")
878 |> Map.put("local_only", local_only)
879 |> Map.put("blocking_user", user)
880 |> Map.put("muting_user", user)
881 |> Map.put("tag", tags)
882 |> Map.put("tag_all", tag_all)
883 |> Map.put("tag_reject", tag_reject)
884 |> ActivityPub.fetch_public_activities()
888 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
889 |> put_view(StatusView)
890 |> render("index.json", %{activities: activities, for: user, as: :activity})
893 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
894 with %User{} = user <- User.get_cached_by_id(id),
895 followers <- MastodonAPI.get_followers(user, params) do
898 for_user && user.id == for_user.id -> followers
899 user.info.hide_followers -> []
904 |> add_link_headers(:followers, followers, user)
905 |> put_view(AccountView)
906 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
910 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
911 with %User{} = user <- User.get_cached_by_id(id),
912 followers <- MastodonAPI.get_friends(user, params) do
915 for_user && user.id == for_user.id -> followers
916 user.info.hide_follows -> []
921 |> add_link_headers(:following, followers, user)
922 |> put_view(AccountView)
923 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
927 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
928 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
930 |> put_view(AccountView)
931 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
935 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
936 with %User{} = follower <- User.get_cached_by_id(id),
937 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
939 |> put_view(AccountView)
940 |> render("relationship.json", %{user: followed, target: follower})
944 |> put_resp_content_type("application/json")
945 |> send_resp(403, Jason.encode!(%{"error" => message}))
949 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
950 with %User{} = follower <- User.get_cached_by_id(id),
951 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
953 |> put_view(AccountView)
954 |> render("relationship.json", %{user: followed, target: follower})
958 |> put_resp_content_type("application/json")
959 |> send_resp(403, Jason.encode!(%{"error" => message}))
963 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
964 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
965 {_, true} <- {:followed, follower.id != followed.id},
966 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
968 |> put_view(AccountView)
969 |> render("relationship.json", %{user: follower, target: followed})
976 |> put_resp_content_type("application/json")
977 |> send_resp(403, Jason.encode!(%{"error" => message}))
981 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
982 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
983 {_, true} <- {:followed, follower.id != followed.id},
984 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
986 |> put_view(AccountView)
987 |> render("account.json", %{user: followed, for: follower})
994 |> put_resp_content_type("application/json")
995 |> send_resp(403, Jason.encode!(%{"error" => message}))
999 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1000 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1001 {_, true} <- {:followed, follower.id != followed.id},
1002 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1004 |> put_view(AccountView)
1005 |> render("relationship.json", %{user: follower, target: followed})
1008 {:error, :not_found}
1015 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1016 with %User{} = muted <- User.get_cached_by_id(id),
1017 {:ok, muter} <- User.mute(muter, muted) do
1019 |> put_view(AccountView)
1020 |> render("relationship.json", %{user: muter, target: muted})
1022 {:error, message} ->
1024 |> put_resp_content_type("application/json")
1025 |> send_resp(403, Jason.encode!(%{"error" => message}))
1029 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1030 with %User{} = muted <- User.get_cached_by_id(id),
1031 {:ok, muter} <- User.unmute(muter, muted) do
1033 |> put_view(AccountView)
1034 |> render("relationship.json", %{user: muter, target: muted})
1036 {:error, message} ->
1038 |> put_resp_content_type("application/json")
1039 |> send_resp(403, Jason.encode!(%{"error" => message}))
1043 def mutes(%{assigns: %{user: user}} = conn, _) do
1044 with muted_accounts <- User.muted_users(user) do
1045 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1050 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1051 with %User{} = blocked <- User.get_cached_by_id(id),
1052 {:ok, blocker} <- User.block(blocker, blocked),
1053 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1055 |> put_view(AccountView)
1056 |> render("relationship.json", %{user: blocker, target: blocked})
1058 {:error, message} ->
1060 |> put_resp_content_type("application/json")
1061 |> send_resp(403, Jason.encode!(%{"error" => message}))
1065 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1066 with %User{} = blocked <- User.get_cached_by_id(id),
1067 {:ok, blocker} <- User.unblock(blocker, blocked),
1068 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1070 |> put_view(AccountView)
1071 |> render("relationship.json", %{user: blocker, target: blocked})
1073 {:error, message} ->
1075 |> put_resp_content_type("application/json")
1076 |> send_resp(403, Jason.encode!(%{"error" => message}))
1080 def blocks(%{assigns: %{user: user}} = conn, _) do
1081 with blocked_accounts <- User.blocked_users(user) do
1082 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1087 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1088 json(conn, info.domain_blocks || [])
1091 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1092 User.block_domain(blocker, domain)
1096 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1097 User.unblock_domain(blocker, domain)
1101 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1102 with %User{} = subscription_target <- User.get_cached_by_id(id),
1103 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1105 |> put_view(AccountView)
1106 |> render("relationship.json", %{user: user, target: subscription_target})
1108 {:error, message} ->
1110 |> put_resp_content_type("application/json")
1111 |> send_resp(403, Jason.encode!(%{"error" => message}))
1115 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1116 with %User{} = subscription_target <- User.get_cached_by_id(id),
1117 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1119 |> put_view(AccountView)
1120 |> render("relationship.json", %{user: user, target: subscription_target})
1122 {:error, message} ->
1124 |> put_resp_content_type("application/json")
1125 |> send_resp(403, Jason.encode!(%{"error" => message}))
1129 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1130 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1131 statuses = Activity.search(user, query)
1132 tags_path = Web.base_url() <> "/tag/"
1138 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1139 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1140 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1143 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1145 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1152 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1153 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1154 statuses = Activity.search(user, query)
1160 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1161 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1164 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1166 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1173 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1174 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1176 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1181 def favourites(%{assigns: %{user: user}} = conn, params) do
1184 |> Map.put("type", "Create")
1185 |> Map.put("favorited_by", user.ap_id)
1186 |> Map.put("blocking_user", user)
1189 ActivityPub.fetch_activities([], params)
1193 |> add_link_headers(:favourites, activities)
1194 |> put_view(StatusView)
1195 |> render("index.json", %{activities: activities, for: user, as: :activity})
1198 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1199 with %User{} = user <- User.get_by_id(id),
1200 false <- user.info.hide_favorites do
1203 |> Map.put("type", "Create")
1204 |> Map.put("favorited_by", user.ap_id)
1205 |> Map.put("blocking_user", for_user)
1209 ["https://www.w3.org/ns/activitystreams#Public"] ++
1210 [for_user.ap_id | for_user.following]
1212 ["https://www.w3.org/ns/activitystreams#Public"]
1217 |> ActivityPub.fetch_activities(params)
1221 |> add_link_headers(:favourites, activities)
1222 |> put_view(StatusView)
1223 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1226 {:error, :not_found}
1231 |> json(%{error: "Can't get favorites"})
1235 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1236 user = User.get_cached_by_id(user.id)
1239 Bookmark.for_user_query(user.id)
1240 |> Pagination.fetch_paginated(params)
1244 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1247 |> add_link_headers(:bookmarks, bookmarks)
1248 |> put_view(StatusView)
1249 |> render("index.json", %{activities: activities, for: user, as: :activity})
1252 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1253 lists = Pleroma.List.for_user(user, opts)
1254 res = ListView.render("lists.json", lists: lists)
1258 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1259 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1260 res = ListView.render("list.json", list: list)
1266 |> json(%{error: "Record not found"})
1270 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1271 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1272 res = ListView.render("lists.json", lists: lists)
1276 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1277 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1278 {:ok, _list} <- Pleroma.List.delete(list) do
1286 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1287 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1288 res = ListView.render("list.json", list: list)
1293 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1295 |> Enum.each(fn account_id ->
1296 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1297 %User{} = followed <- User.get_cached_by_id(account_id) do
1298 Pleroma.List.follow(list, followed)
1305 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1307 |> Enum.each(fn account_id ->
1308 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1309 %User{} = followed <- User.get_cached_by_id(account_id) do
1310 Pleroma.List.unfollow(list, followed)
1317 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1318 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1319 {:ok, users} = Pleroma.List.get_following(list) do
1321 |> put_view(AccountView)
1322 |> render("accounts.json", %{for: user, users: users, as: :user})
1326 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1327 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1328 {:ok, list} <- Pleroma.List.rename(list, title) do
1329 res = ListView.render("list.json", list: list)
1337 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1338 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1341 |> Map.put("type", "Create")
1342 |> Map.put("blocking_user", user)
1343 |> Map.put("muting_user", user)
1345 # we must filter the following list for the user to avoid leaking statuses the user
1346 # does not actually have permission to see (for more info, peruse security issue #270).
1349 |> Enum.filter(fn x -> x in user.following end)
1350 |> ActivityPub.fetch_activities_bounded(following, params)
1354 |> put_view(StatusView)
1355 |> render("index.json", %{activities: activities, for: user, as: :activity})
1360 |> json(%{error: "Error."})
1364 def index(%{assigns: %{user: user}} = conn, _params) do
1365 token = get_session(conn, :oauth_token)
1368 mastodon_emoji = mastodonized_emoji()
1370 limit = Config.get([:instance, :limit])
1373 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1378 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1379 access_token: token,
1381 domain: Pleroma.Web.Endpoint.host(),
1384 unfollow_modal: false,
1387 auto_play_gif: false,
1388 display_sensitive_media: false,
1389 reduce_motion: false,
1390 max_toot_chars: limit,
1391 mascot: User.get_mascot(user)["url"]
1393 poll_limits: Config.get([:instance, :poll_limits]),
1395 delete_others_notice: present?(user.info.is_moderator),
1396 admin: present?(user.info.is_admin)
1400 default_privacy: user.info.default_scope,
1401 default_sensitive: false,
1402 allow_content_types: Config.get([:instance, :allowed_post_formats])
1404 media_attachments: %{
1405 accept_content_types: [
1421 user.info.settings ||
1451 push_subscription: nil,
1453 custom_emojis: mastodon_emoji,
1459 |> put_layout(false)
1460 |> put_view(MastodonView)
1461 |> render("index.html", %{initial_state: initial_state})
1464 |> put_session(:return_to, conn.request_path)
1465 |> redirect(to: "/web/login")
1469 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1470 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1472 with changeset <- Ecto.Changeset.change(user),
1473 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1474 {:ok, _user} <- User.update_and_set_cache(changeset) do
1479 |> put_resp_content_type("application/json")
1480 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1484 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1485 redirect(conn, to: local_mastodon_root_path(conn))
1488 @doc "Local Mastodon FE login init action"
1489 def login(conn, %{"code" => auth_token}) do
1490 with {:ok, app} <- get_or_make_app(),
1491 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1492 {:ok, token} <- Token.exchange_token(app, auth) do
1494 |> put_session(:oauth_token, token.token)
1495 |> redirect(to: local_mastodon_root_path(conn))
1499 @doc "Local Mastodon FE callback action"
1500 def login(conn, _) do
1501 with {:ok, app} <- get_or_make_app() do
1506 response_type: "code",
1507 client_id: app.client_id,
1509 scope: Enum.join(app.scopes, " ")
1512 redirect(conn, to: path)
1516 defp local_mastodon_root_path(conn) do
1517 case get_session(conn, :return_to) do
1519 mastodon_api_path(conn, :index, ["getting-started"])
1522 delete_session(conn, :return_to)
1527 defp get_or_make_app do
1528 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1529 scopes = ["read", "write", "follow", "push"]
1531 with %App{} = app <- Repo.get_by(App, find_attrs) do
1533 if app.scopes == scopes do
1537 |> Ecto.Changeset.change(%{scopes: scopes})
1545 App.register_changeset(
1547 Map.put(find_attrs, :scopes, scopes)
1554 def logout(conn, _) do
1557 |> redirect(to: "/")
1560 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1561 Logger.debug("Unimplemented, returning unmodified relationship")
1563 with %User{} = target <- User.get_cached_by_id(id) do
1565 |> put_view(AccountView)
1566 |> render("relationship.json", %{user: user, target: target})
1570 def empty_array(conn, _) do
1571 Logger.debug("Unimplemented, returning an empty array")
1575 def empty_object(conn, _) do
1576 Logger.debug("Unimplemented, returning an empty object")
1580 def get_filters(%{assigns: %{user: user}} = conn, _) do
1581 filters = Filter.get_filters(user)
1582 res = FilterView.render("filters.json", filters: filters)
1587 %{assigns: %{user: user}} = conn,
1588 %{"phrase" => phrase, "context" => context} = params
1594 hide: Map.get(params, "irreversible", false),
1595 whole_word: Map.get(params, "boolean", true)
1599 {:ok, response} = Filter.create(query)
1600 res = FilterView.render("filter.json", filter: response)
1604 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1605 filter = Filter.get(filter_id, user)
1606 res = FilterView.render("filter.json", filter: filter)
1611 %{assigns: %{user: user}} = conn,
1612 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1616 filter_id: filter_id,
1619 hide: Map.get(params, "irreversible", nil),
1620 whole_word: Map.get(params, "boolean", true)
1624 {:ok, response} = Filter.update(query)
1625 res = FilterView.render("filter.json", filter: response)
1629 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1632 filter_id: filter_id
1635 {:ok, _} = Filter.delete(query)
1641 def errors(conn, {:error, %Changeset{} = changeset}) do
1644 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1645 |> Enum.map_join(", ", fn {_k, v} -> v end)
1649 |> json(%{error: error_message})
1652 def errors(conn, {:error, :not_found}) do
1655 |> json(%{error: "Record not found"})
1658 def errors(conn, _) do
1661 |> json("Something went wrong")
1664 def suggestions(%{assigns: %{user: user}} = conn, _) do
1665 suggestions = Config.get(:suggestions)
1667 if Keyword.get(suggestions, :enabled, false) do
1668 api = Keyword.get(suggestions, :third_party_engine, "")
1669 timeout = Keyword.get(suggestions, :timeout, 5000)
1670 limit = Keyword.get(suggestions, :limit, 23)
1672 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1674 user = user.nickname
1678 |> String.replace("{{host}}", host)
1679 |> String.replace("{{user}}", user)
1681 with {:ok, %{status: 200, body: body}} <-
1686 recv_timeout: timeout,
1690 {:ok, data} <- Jason.decode(body) do
1693 |> Enum.slice(0, limit)
1698 case User.get_or_fetch(x["acct"]) do
1699 {:ok, %User{id: id}} -> id
1705 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1708 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1714 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1721 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1722 with %Activity{} = activity <- Activity.get_by_id(status_id),
1723 true <- Visibility.visible_for_user?(activity, user) do
1727 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1737 def reports(%{assigns: %{user: user}} = conn, params) do
1738 case CommonAPI.report(user, params) do
1741 |> put_view(ReportView)
1742 |> try_render("report.json", %{activity: activity})
1746 |> put_status(:bad_request)
1747 |> json(%{error: err})
1751 def account_register(
1752 %{assigns: %{app: app}} = conn,
1753 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1761 "captcha_answer_data",
1765 |> Map.put("nickname", nickname)
1766 |> Map.put("fullname", params["fullname"] || nickname)
1767 |> Map.put("bio", params["bio"] || "")
1768 |> Map.put("confirm", params["password"])
1770 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1771 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1773 token_type: "Bearer",
1774 access_token: token.token,
1776 created_at: Token.Utils.format_created_at(token)
1782 |> json(Jason.encode!(errors))
1786 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1789 |> json(%{error: "Missing parameters"})
1792 def account_register(conn, _) do
1795 |> json(%{error: "Invalid credentials"})
1798 def conversations(%{assigns: %{user: user}} = conn, params) do
1799 participations = Participation.for_user_with_last_activity_id(user, params)
1802 Enum.map(participations, fn participation ->
1803 ConversationView.render("participation.json", %{participation: participation, user: user})
1807 |> add_link_headers(:conversations, participations)
1808 |> json(conversations)
1811 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1812 with %Participation{} = participation <-
1813 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1814 {:ok, participation} <- Participation.mark_as_read(participation) do
1815 participation_view =
1816 ConversationView.render("participation.json", %{participation: participation, user: user})
1819 |> json(participation_view)
1823 def try_render(conn, target, params)
1824 when is_binary(target) do
1825 res = render(conn, target, params)
1830 |> json(%{error: "Can't display this activity"})
1836 def try_render(conn, _, _) do
1839 |> json(%{error: "Can't display this activity"})
1842 defp present?(nil), do: false
1843 defp present?(false), do: false
1844 defp present?(_), do: true