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
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
17 alias Pleroma.Formatter
19 alias Pleroma.Notification
21 alias Pleroma.Pagination
22 alias Pleroma.Plugs.RateLimiter
24 alias Pleroma.ScheduledActivity
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Visibility
30 alias Pleroma.Web.CommonAPI
31 alias Pleroma.Web.MastodonAPI.AccountView
32 alias Pleroma.Web.MastodonAPI.AppView
33 alias Pleroma.Web.MastodonAPI.ConversationView
34 alias Pleroma.Web.MastodonAPI.FilterView
35 alias Pleroma.Web.MastodonAPI.ListView
36 alias Pleroma.Web.MastodonAPI.MastodonAPI
37 alias Pleroma.Web.MastodonAPI.MastodonView
38 alias Pleroma.Web.MastodonAPI.NotificationView
39 alias Pleroma.Web.MastodonAPI.ReportView
40 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
41 alias Pleroma.Web.MastodonAPI.StatusView
42 alias Pleroma.Web.MediaProxy
43 alias Pleroma.Web.OAuth.App
44 alias Pleroma.Web.OAuth.Authorization
45 alias Pleroma.Web.OAuth.Scopes
46 alias Pleroma.Web.OAuth.Token
47 alias Pleroma.Web.TwitterAPI.TwitterAPI
49 alias Pleroma.Web.ControllerHelper
53 require Pleroma.Constants
55 @rate_limited_relations_actions ~w(follow unfollow)a
57 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
58 post_status delete_status)a
62 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
63 when action in ~w(reblog_status unreblog_status)a
68 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
69 when action in ~w(fav_status unfav_status)a
74 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
77 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
78 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
79 plug(RateLimiter, :app_account_creation when action == :account_register)
80 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
81 plug(RateLimiter, :password_reset when action == :password_reset)
82 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
84 @local_mastodon_name "Mastodon-Local"
86 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
88 def create_app(conn, params) do
89 scopes = Scopes.fetch_scopes(params, ["read"])
93 |> Map.drop(["scope", "scopes"])
94 |> Map.put("scopes", scopes)
96 with cs <- App.register_changeset(%App{}, app_attrs),
97 false <- cs.changes[:client_name] == @local_mastodon_name,
98 {:ok, app} <- Repo.insert(cs) do
101 |> render("show.json", %{app: app})
110 value_function \\ fn x -> {:ok, x} end
112 if Map.has_key?(params, params_field) do
113 case value_function.(params[params_field]) do
114 {:ok, new_value} -> Map.put(map, map_field, new_value)
122 def update_credentials(%{assigns: %{user: user}} = conn, params) do
127 |> add_if_present(params, "display_name", :name)
128 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
129 |> add_if_present(params, "avatar", :avatar, fn value ->
130 with %Plug.Upload{} <- value,
131 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
138 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
142 |> Map.get(:emoji, [])
143 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
150 :hide_followers_count,
156 :skip_thread_containment
158 |> Enum.reduce(%{}, fn key, acc ->
159 add_if_present(acc, params, to_string(key), key, fn value ->
160 {:ok, ControllerHelper.truthy_param?(value)}
163 |> add_if_present(params, "default_scope", :default_scope)
164 |> add_if_present(params, "fields", :fields, fn fields ->
165 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
169 |> add_if_present(params, "fields", :raw_fields)
170 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
171 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
173 |> add_if_present(params, "header", :banner, fn value ->
174 with %Plug.Upload{} <- value,
175 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
181 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
182 with %Plug.Upload{} <- value,
183 {:ok, object} <- ActivityPub.upload(value, type: :background) do
189 |> Map.put(:emoji, user_info_emojis)
191 info_cng = User.Info.profile_update(user.info, info_params)
193 with changeset <- User.update_changeset(user, user_params),
194 changeset <- Changeset.put_embed(changeset, :info, info_cng),
195 {:ok, user} <- User.update_and_set_cache(changeset) do
196 if original_user != user do
197 CommonAPI.update(user)
202 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
205 _e -> render_error(conn, :forbidden, "Invalid request")
209 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
210 change = Changeset.change(user, %{avatar: nil})
211 {:ok, user} = User.update_and_set_cache(change)
212 CommonAPI.update(user)
214 json(conn, %{url: nil})
217 def update_avatar(%{assigns: %{user: user}} = conn, params) do
218 {:ok, object} = ActivityPub.upload(params, type: :avatar)
219 change = Changeset.change(user, %{avatar: object.data})
220 {:ok, user} = User.update_and_set_cache(change)
221 CommonAPI.update(user)
222 %{"url" => [%{"href" => href} | _]} = object.data
224 json(conn, %{url: href})
227 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
228 with new_info <- %{"banner" => %{}},
229 info_cng <- User.Info.profile_update(user.info, new_info),
230 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
231 {:ok, user} <- User.update_and_set_cache(changeset) do
232 CommonAPI.update(user)
234 json(conn, %{url: nil})
238 def update_banner(%{assigns: %{user: user}} = conn, params) do
239 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
240 new_info <- %{"banner" => object.data},
241 info_cng <- User.Info.profile_update(user.info, new_info),
242 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
243 {:ok, user} <- User.update_and_set_cache(changeset) do
244 CommonAPI.update(user)
245 %{"url" => [%{"href" => href} | _]} = object.data
247 json(conn, %{url: href})
251 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
252 with new_info <- %{"background" => %{}},
253 info_cng <- User.Info.profile_update(user.info, new_info),
254 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
255 {:ok, _user} <- User.update_and_set_cache(changeset) do
256 json(conn, %{url: nil})
260 def update_background(%{assigns: %{user: user}} = conn, params) do
261 with {:ok, object} <- ActivityPub.upload(params, type: :background),
262 new_info <- %{"background" => object.data},
263 info_cng <- User.Info.profile_update(user.info, new_info),
264 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
265 {:ok, _user} <- User.update_and_set_cache(changeset) do
266 %{"url" => [%{"href" => href} | _]} = object.data
268 json(conn, %{url: href})
272 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
273 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
276 AccountView.render("account.json", %{
279 with_pleroma_settings: true,
280 with_chat_token: chat_token
286 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
287 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
290 |> render("short.json", %{app: app})
294 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
295 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
296 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
297 account = AccountView.render("account.json", %{user: user, for: for_user})
300 _e -> render_error(conn, :not_found, "Can't find user")
304 @mastodon_api_level "2.7.2"
306 def masto_instance(conn, _params) do
307 instance = Config.get(:instance)
311 title: Keyword.get(instance, :name),
312 description: Keyword.get(instance, :description),
313 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
314 email: Keyword.get(instance, :email),
316 streaming_api: Pleroma.Web.Endpoint.websocket_url()
318 stats: Stats.get_stats(),
319 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
321 registrations: Pleroma.Config.get([:instance, :registrations_open]),
322 # Extra (not present in Mastodon):
323 max_toot_chars: Keyword.get(instance, :limit),
324 poll_limits: Keyword.get(instance, :poll_limits)
330 def peers(conn, _params) do
331 json(conn, Stats.get_peers())
334 defp mastodonized_emoji do
335 Pleroma.Emoji.get_all()
336 |> Enum.map(fn {shortcode, relative_url, tags} ->
337 url = to_string(URI.merge(Web.base_url(), relative_url))
340 "shortcode" => shortcode,
342 "visible_in_picker" => true,
345 # Assuming that a comma is authorized in the category name
346 "category" => (tags -- ["Custom"]) |> Enum.join(",")
351 def custom_emojis(conn, _params) do
352 mastodon_emoji = mastodonized_emoji()
353 json(conn, mastodon_emoji)
356 def home_timeline(%{assigns: %{user: user}} = conn, params) do
359 |> Map.put("type", ["Create", "Announce"])
360 |> Map.put("blocking_user", user)
361 |> Map.put("muting_user", user)
362 |> Map.put("user", user)
365 [user.ap_id | user.following]
366 |> ActivityPub.fetch_activities(params)
370 |> add_link_headers(activities)
371 |> put_view(StatusView)
372 |> render("index.json", %{activities: activities, for: user, as: :activity})
375 def public_timeline(%{assigns: %{user: user}} = conn, params) do
376 local_only = params["local"] in [true, "True", "true", "1"]
380 |> Map.put("type", ["Create", "Announce"])
381 |> Map.put("local_only", local_only)
382 |> Map.put("blocking_user", user)
383 |> Map.put("muting_user", user)
384 |> Map.put("user", user)
385 |> ActivityPub.fetch_public_activities()
389 |> add_link_headers(activities, %{"local" => local_only})
390 |> put_view(StatusView)
391 |> render("index.json", %{activities: activities, for: user, as: :activity})
394 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
395 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
398 |> Map.put("tag", params["tagged"])
400 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
403 |> add_link_headers(activities)
404 |> put_view(StatusView)
405 |> render("index.json", %{
406 activities: activities,
413 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
416 |> Map.put("type", "Create")
417 |> Map.put("blocking_user", user)
418 |> Map.put("user", user)
419 |> Map.put(:visibility, "direct")
423 |> ActivityPub.fetch_activities_query(params)
424 |> Pagination.fetch_paginated(params)
427 |> add_link_headers(activities)
428 |> put_view(StatusView)
429 |> render("index.json", %{activities: activities, for: user, as: :activity})
432 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
438 |> Activity.all_by_ids_with_object()
439 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
442 |> put_view(StatusView)
443 |> render("index.json", activities: activities, for: user, as: :activity)
446 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
447 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
448 true <- Visibility.visible_for_user?(activity, user) do
450 |> put_view(StatusView)
451 |> try_render("status.json", %{activity: activity, for: user})
455 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
456 with %Activity{} = activity <- Activity.get_by_id(id),
458 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
459 "blocking_user" => user,
461 "exclude_id" => activity.id
463 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
466 StatusView.render("index.json",
468 activities: grouped_activities[true] || [],
472 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
474 StatusView.render("index.json",
476 activities: grouped_activities[false] || [],
480 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
487 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
488 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
489 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
490 true <- Visibility.visible_for_user?(activity, user) do
492 |> put_view(StatusView)
493 |> try_render("poll.json", %{object: object, for: user})
495 error when is_nil(error) or error == false ->
496 render_error(conn, :not_found, "Record not found")
500 defp get_cached_vote_or_vote(user, object, choices) do
501 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
504 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
505 case CommonAPI.vote(user, object, choices) do
506 {:error, _message} = res -> {:ignore, res}
507 res -> {:commit, res}
514 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
515 with %Object{} = object <- Object.get_by_id(id),
516 true <- object.data["type"] == "Question",
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
518 true <- Visibility.visible_for_user?(activity, user),
519 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
521 |> put_view(StatusView)
522 |> try_render("poll.json", %{object: object, for: user})
525 render_error(conn, :not_found, "Record not found")
528 render_error(conn, :not_found, "Record not found")
532 |> put_status(:unprocessable_entity)
533 |> json(%{error: message})
537 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
538 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
540 |> add_link_headers(scheduled_activities)
541 |> put_view(ScheduledActivityView)
542 |> render("index.json", %{scheduled_activities: scheduled_activities})
546 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
547 with %ScheduledActivity{} = scheduled_activity <-
548 ScheduledActivity.get(user, scheduled_activity_id) do
550 |> put_view(ScheduledActivityView)
551 |> render("show.json", %{scheduled_activity: scheduled_activity})
553 _ -> {:error, :not_found}
557 def update_scheduled_status(
558 %{assigns: %{user: user}} = conn,
559 %{"id" => scheduled_activity_id} = params
561 with %ScheduledActivity{} = scheduled_activity <-
562 ScheduledActivity.get(user, scheduled_activity_id),
563 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
565 |> put_view(ScheduledActivityView)
566 |> render("show.json", %{scheduled_activity: scheduled_activity})
568 nil -> {:error, :not_found}
573 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
574 with %ScheduledActivity{} = scheduled_activity <-
575 ScheduledActivity.get(user, scheduled_activity_id),
576 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
578 |> put_view(ScheduledActivityView)
579 |> render("show.json", %{scheduled_activity: scheduled_activity})
581 nil -> {:error, :not_found}
586 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
589 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
591 scheduled_at = params["scheduled_at"]
593 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
594 with {:ok, scheduled_activity} <-
595 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
597 |> put_view(ScheduledActivityView)
598 |> render("show.json", %{scheduled_activity: scheduled_activity})
601 params = Map.drop(params, ["scheduled_at"])
603 case CommonAPI.post(user, params) do
606 |> put_status(:unprocessable_entity)
607 |> json(%{error: message})
611 |> put_view(StatusView)
612 |> try_render("status.json", %{
616 with_direct_conversation_id: true
622 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
623 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
626 _e -> render_error(conn, :forbidden, "Can't delete this post")
630 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
631 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
632 %Activity{} = announce <- Activity.normalize(announce.data) do
634 |> put_view(StatusView)
635 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
639 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
640 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
641 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
643 |> put_view(StatusView)
644 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
648 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
649 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
650 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
658 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
659 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
661 |> put_view(StatusView)
662 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
666 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
667 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
675 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
677 |> put_view(StatusView)
678 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
682 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
683 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
684 %User{} = user <- User.get_cached_by_nickname(user.nickname),
685 true <- Visibility.visible_for_user?(activity, user),
686 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
688 |> put_view(StatusView)
689 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
693 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
694 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
695 %User{} = user <- User.get_cached_by_nickname(user.nickname),
696 true <- Visibility.visible_for_user?(activity, user),
697 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
699 |> put_view(StatusView)
700 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
704 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
705 activity = Activity.get_by_id(id)
707 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
709 |> put_view(StatusView)
710 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
714 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
715 activity = Activity.get_by_id(id)
717 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
719 |> put_view(StatusView)
720 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
724 def notifications(%{assigns: %{user: user}} = conn, params) do
725 notifications = MastodonAPI.get_notifications(user, params)
728 |> add_link_headers(notifications)
729 |> put_view(NotificationView)
730 |> render("index.json", %{notifications: notifications, for: user})
733 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
734 with {:ok, notification} <- Notification.get(user, id) do
736 |> put_view(NotificationView)
737 |> render("show.json", %{notification: notification, for: user})
741 |> put_status(:forbidden)
742 |> json(%{"error" => reason})
746 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
747 Notification.clear(user)
751 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
752 with {:ok, _notif} <- Notification.dismiss(user, id) do
757 |> put_status(:forbidden)
758 |> json(%{"error" => reason})
762 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
763 Notification.destroy_multiple(user, ids)
767 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
768 targets = User.get_all_by_ids(List.wrap(id))
771 |> put_view(AccountView)
772 |> render("relationships.json", %{user: user, targets: targets})
775 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
776 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
779 %{assigns: %{user: user}} = conn,
780 %{"id" => id, "description" => description} = _
782 when is_binary(description) do
783 with %Object{} = object <- Repo.get(Object, id),
784 true <- Object.authorize_mutation(object, user),
785 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
786 attachment_data = Map.put(data, "id", object.id)
789 |> put_view(StatusView)
790 |> render("attachment.json", %{attachment: attachment_data})
794 def update_media(_conn, _data), do: {:error, :bad_request}
796 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
797 with {:ok, object} <-
800 actor: User.ap_id(user),
801 description: Map.get(data, "description")
803 attachment_data = Map.put(object.data, "id", object.id)
806 |> put_view(StatusView)
807 |> render("attachment.json", %{attachment: attachment_data})
811 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
812 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
813 %{} = attachment_data <- Map.put(object.data, "id", object.id),
814 %{type: "image"} = rendered <-
815 StatusView.render("attachment.json", %{attachment: attachment_data}),
816 {:ok, _user} = User.update_mascot(user, rendered) do
819 %{type: _type} = _ ->
820 render_error(conn, :unsupported_media_type, "mascots can only be images")
827 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
828 mascot = User.get_mascot(user)
833 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
834 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
835 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
836 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
837 q = from(u in User, where: u.ap_id in ^likes)
841 |> Enum.filter(&(not User.blocks?(user, &1)))
844 |> put_view(AccountView)
845 |> render("accounts.json", %{for: user, users: users, as: :user})
847 {:visible, false} -> {:error, :not_found}
852 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
853 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
854 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
855 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
856 q = from(u in User, where: u.ap_id in ^announces)
860 |> Enum.filter(&(not User.blocks?(user, &1)))
863 |> put_view(AccountView)
864 |> render("accounts.json", %{for: user, users: users, as: :user})
866 {:visible, false} -> {:error, :not_found}
871 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
872 local_only = params["local"] in [true, "True", "true", "1"]
875 [params["tag"], params["any"]]
879 |> Enum.map(&String.downcase(&1))
884 |> Enum.map(&String.downcase(&1))
889 |> Enum.map(&String.downcase(&1))
893 |> Map.put("type", "Create")
894 |> Map.put("local_only", local_only)
895 |> Map.put("blocking_user", user)
896 |> Map.put("muting_user", user)
897 |> Map.put("user", user)
898 |> Map.put("tag", tags)
899 |> Map.put("tag_all", tag_all)
900 |> Map.put("tag_reject", tag_reject)
901 |> ActivityPub.fetch_public_activities()
905 |> add_link_headers(activities, %{"local" => local_only})
906 |> put_view(StatusView)
907 |> render("index.json", %{activities: activities, for: user, as: :activity})
910 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
911 with %User{} = user <- User.get_cached_by_id(id),
912 followers <- MastodonAPI.get_followers(user, params) do
915 for_user && user.id == for_user.id -> followers
916 user.info.hide_followers -> []
921 |> add_link_headers(followers)
922 |> put_view(AccountView)
923 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
927 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
928 with %User{} = user <- User.get_cached_by_id(id),
929 followers <- MastodonAPI.get_friends(user, params) do
932 for_user && user.id == for_user.id -> followers
933 user.info.hide_follows -> []
938 |> add_link_headers(followers)
939 |> put_view(AccountView)
940 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
944 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
945 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
947 |> put_view(AccountView)
948 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
952 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
953 with %User{} = follower <- User.get_cached_by_id(id),
954 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
956 |> put_view(AccountView)
957 |> render("relationship.json", %{user: followed, target: follower})
961 |> put_status(:forbidden)
962 |> json(%{error: message})
966 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
967 with %User{} = follower <- User.get_cached_by_id(id),
968 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
970 |> put_view(AccountView)
971 |> render("relationship.json", %{user: followed, target: follower})
975 |> put_status(:forbidden)
976 |> json(%{error: message})
980 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
981 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
982 {_, true} <- {:followed, follower.id != followed.id},
983 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
985 |> put_view(AccountView)
986 |> render("relationship.json", %{user: follower, target: followed})
993 |> put_status(:forbidden)
994 |> json(%{error: message})
998 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
999 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1000 {_, true} <- {:followed, follower.id != followed.id},
1001 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1003 |> put_view(AccountView)
1004 |> render("account.json", %{user: followed, for: follower})
1007 {:error, :not_found}
1009 {:error, message} ->
1011 |> put_status(:forbidden)
1012 |> json(%{error: message})
1016 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1017 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1018 {_, true} <- {:followed, follower.id != followed.id},
1019 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1021 |> put_view(AccountView)
1022 |> render("relationship.json", %{user: follower, target: followed})
1025 {:error, :not_found}
1032 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1034 if Map.has_key?(params, "notifications"),
1035 do: params["notifications"] in [true, "True", "true", "1"],
1038 with %User{} = muted <- User.get_cached_by_id(id),
1039 {:ok, muter} <- User.mute(muter, muted, notifications) do
1041 |> put_view(AccountView)
1042 |> render("relationship.json", %{user: muter, target: muted})
1044 {:error, message} ->
1046 |> put_status(:forbidden)
1047 |> json(%{error: message})
1051 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1052 with %User{} = muted <- User.get_cached_by_id(id),
1053 {:ok, muter} <- User.unmute(muter, muted) do
1055 |> put_view(AccountView)
1056 |> render("relationship.json", %{user: muter, target: muted})
1058 {:error, message} ->
1060 |> put_status(:forbidden)
1061 |> json(%{error: message})
1065 def mutes(%{assigns: %{user: user}} = conn, _) do
1066 with muted_accounts <- User.muted_users(user) do
1067 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1072 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1073 with %User{} = blocked <- User.get_cached_by_id(id),
1074 {:ok, blocker} <- User.block(blocker, blocked),
1075 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1077 |> put_view(AccountView)
1078 |> render("relationship.json", %{user: blocker, target: blocked})
1080 {:error, message} ->
1082 |> put_status(:forbidden)
1083 |> json(%{error: message})
1087 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1088 with %User{} = blocked <- User.get_cached_by_id(id),
1089 {:ok, blocker} <- User.unblock(blocker, blocked),
1090 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1092 |> put_view(AccountView)
1093 |> render("relationship.json", %{user: blocker, target: blocked})
1095 {:error, message} ->
1097 |> put_status(:forbidden)
1098 |> json(%{error: message})
1102 def blocks(%{assigns: %{user: user}} = conn, _) do
1103 with blocked_accounts <- User.blocked_users(user) do
1104 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1109 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1110 json(conn, info.domain_blocks || [])
1113 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1114 User.block_domain(blocker, domain)
1118 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1119 User.unblock_domain(blocker, domain)
1123 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1124 with %User{} = subscription_target <- User.get_cached_by_id(id),
1125 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1127 |> put_view(AccountView)
1128 |> render("relationship.json", %{user: user, target: subscription_target})
1130 nil -> {:error, :not_found}
1135 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1136 with %User{} = subscription_target <- User.get_cached_by_id(id),
1137 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1139 |> put_view(AccountView)
1140 |> render("relationship.json", %{user: user, target: subscription_target})
1142 nil -> {:error, :not_found}
1147 def favourites(%{assigns: %{user: user}} = conn, params) do
1150 |> Map.put("type", "Create")
1151 |> Map.put("favorited_by", user.ap_id)
1152 |> Map.put("blocking_user", user)
1155 ActivityPub.fetch_activities([], params)
1159 |> add_link_headers(activities)
1160 |> put_view(StatusView)
1161 |> render("index.json", %{activities: activities, for: user, as: :activity})
1164 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1165 with %User{} = user <- User.get_by_id(id),
1166 false <- user.info.hide_favorites do
1169 |> Map.put("type", "Create")
1170 |> Map.put("favorited_by", user.ap_id)
1171 |> Map.put("blocking_user", for_user)
1175 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1177 [Pleroma.Constants.as_public()]
1182 |> ActivityPub.fetch_activities(params)
1186 |> add_link_headers(activities)
1187 |> put_view(StatusView)
1188 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1190 nil -> {:error, :not_found}
1191 true -> render_error(conn, :forbidden, "Can't get favorites")
1195 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1196 user = User.get_cached_by_id(user.id)
1199 Bookmark.for_user_query(user.id)
1200 |> Pagination.fetch_paginated(params)
1204 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1207 |> add_link_headers(bookmarks)
1208 |> put_view(StatusView)
1209 |> render("index.json", %{activities: activities, for: user, as: :activity})
1212 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1213 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1216 |> put_view(ListView)
1217 |> render("index.json", %{lists: lists})
1220 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1221 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1224 |> Map.put("type", "Create")
1225 |> Map.put("blocking_user", user)
1226 |> Map.put("user", user)
1227 |> Map.put("muting_user", user)
1229 # we must filter the following list for the user to avoid leaking statuses the user
1230 # does not actually have permission to see (for more info, peruse security issue #270).
1233 |> Enum.filter(fn x -> x in user.following end)
1234 |> ActivityPub.fetch_activities_bounded(following, params)
1238 |> put_view(StatusView)
1239 |> render("index.json", %{activities: activities, for: user, as: :activity})
1241 _e -> render_error(conn, :forbidden, "Error.")
1245 def index(%{assigns: %{user: user}} = conn, _params) do
1246 token = get_session(conn, :oauth_token)
1249 mastodon_emoji = mastodonized_emoji()
1251 limit = Config.get([:instance, :limit])
1254 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1259 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1260 access_token: token,
1262 domain: Pleroma.Web.Endpoint.host(),
1265 unfollow_modal: false,
1268 auto_play_gif: false,
1269 display_sensitive_media: false,
1270 reduce_motion: false,
1271 max_toot_chars: limit,
1272 mascot: User.get_mascot(user)["url"]
1274 poll_limits: Config.get([:instance, :poll_limits]),
1276 delete_others_notice: present?(user.info.is_moderator),
1277 admin: present?(user.info.is_admin)
1281 default_privacy: user.info.default_scope,
1282 default_sensitive: false,
1283 allow_content_types: Config.get([:instance, :allowed_post_formats])
1285 media_attachments: %{
1286 accept_content_types: [
1302 user.info.settings ||
1332 push_subscription: nil,
1334 custom_emojis: mastodon_emoji,
1340 |> put_layout(false)
1341 |> put_view(MastodonView)
1342 |> render("index.html", %{initial_state: initial_state})
1345 |> put_session(:return_to, conn.request_path)
1346 |> redirect(to: "/web/login")
1350 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1351 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1353 with changeset <- Changeset.change(user),
1354 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1355 {:ok, _user} <- User.update_and_set_cache(changeset) do
1360 |> put_status(:internal_server_error)
1361 |> json(%{error: inspect(e)})
1365 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1366 redirect(conn, to: local_mastodon_root_path(conn))
1369 @doc "Local Mastodon FE login init action"
1370 def login(conn, %{"code" => auth_token}) do
1371 with {:ok, app} <- get_or_make_app(),
1372 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
1373 {:ok, token} <- Token.exchange_token(app, auth) do
1375 |> put_session(:oauth_token, token.token)
1376 |> redirect(to: local_mastodon_root_path(conn))
1380 @doc "Local Mastodon FE callback action"
1381 def login(conn, _) do
1382 with {:ok, app} <- get_or_make_app() do
1384 o_auth_path(conn, :authorize,
1385 response_type: "code",
1386 client_id: app.client_id,
1388 scope: Enum.join(app.scopes, " ")
1391 redirect(conn, to: path)
1395 defp local_mastodon_root_path(conn) do
1396 case get_session(conn, :return_to) do
1398 mastodon_api_path(conn, :index, ["getting-started"])
1401 delete_session(conn, :return_to)
1406 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
1407 defp get_or_make_app do
1409 %{client_name: @local_mastodon_name, redirect_uris: "."},
1410 ["read", "write", "follow", "push"]
1414 def logout(conn, _) do
1417 |> redirect(to: "/")
1420 # Stubs for unimplemented mastodon api
1422 def empty_array(conn, _) do
1423 Logger.debug("Unimplemented, returning an empty array")
1427 def get_filters(%{assigns: %{user: user}} = conn, _) do
1428 filters = Filter.get_filters(user)
1429 res = FilterView.render("filters.json", filters: filters)
1434 %{assigns: %{user: user}} = conn,
1435 %{"phrase" => phrase, "context" => context} = params
1441 hide: Map.get(params, "irreversible", false),
1442 whole_word: Map.get(params, "boolean", true)
1446 {:ok, response} = Filter.create(query)
1447 res = FilterView.render("filter.json", filter: response)
1451 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1452 filter = Filter.get(filter_id, user)
1453 res = FilterView.render("filter.json", filter: filter)
1458 %{assigns: %{user: user}} = conn,
1459 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1463 filter_id: filter_id,
1466 hide: Map.get(params, "irreversible", nil),
1467 whole_word: Map.get(params, "boolean", true)
1471 {:ok, response} = Filter.update(query)
1472 res = FilterView.render("filter.json", filter: response)
1476 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1479 filter_id: filter_id
1482 {:ok, _} = Filter.delete(query)
1486 def suggestions(%{assigns: %{user: user}} = conn, _) do
1487 suggestions = Config.get(:suggestions)
1489 if Keyword.get(suggestions, :enabled, false) do
1490 api = Keyword.get(suggestions, :third_party_engine, "")
1491 timeout = Keyword.get(suggestions, :timeout, 5000)
1492 limit = Keyword.get(suggestions, :limit, 23)
1494 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1496 user = user.nickname
1500 |> String.replace("{{host}}", host)
1501 |> String.replace("{{user}}", user)
1503 with {:ok, %{status: 200, body: body}} <-
1504 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1505 {:ok, data} <- Jason.decode(body) do
1508 |> Enum.slice(0, limit)
1511 |> Map.put("id", fetch_suggestion_id(x))
1512 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1513 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1519 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1526 defp fetch_suggestion_id(attrs) do
1527 case User.get_or_fetch(attrs["acct"]) do
1528 {:ok, %User{id: id}} -> id
1533 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1534 with %Activity{} = activity <- Activity.get_by_id(status_id),
1535 true <- Visibility.visible_for_user?(activity, user) do
1539 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1549 def reports(%{assigns: %{user: user}} = conn, params) do
1550 case CommonAPI.report(user, params) do
1553 |> put_view(ReportView)
1554 |> try_render("report.json", %{activity: activity})
1558 |> put_status(:bad_request)
1559 |> json(%{error: err})
1563 def account_register(
1564 %{assigns: %{app: app}} = conn,
1565 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1573 "captcha_answer_data",
1577 |> Map.put("nickname", nickname)
1578 |> Map.put("fullname", params["fullname"] || nickname)
1579 |> Map.put("bio", params["bio"] || "")
1580 |> Map.put("confirm", params["password"])
1582 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1583 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1585 token_type: "Bearer",
1586 access_token: token.token,
1588 created_at: Token.Utils.format_created_at(token)
1593 |> put_status(:bad_request)
1598 def account_register(%{assigns: %{app: _app}} = conn, _) do
1599 render_error(conn, :bad_request, "Missing parameters")
1602 def account_register(conn, _) do
1603 render_error(conn, :forbidden, "Invalid credentials")
1606 def conversations(%{assigns: %{user: user}} = conn, params) do
1607 participations = Participation.for_user_with_last_activity_id(user, params)
1610 Enum.map(participations, fn participation ->
1611 ConversationView.render("participation.json", %{participation: participation, for: user})
1615 |> add_link_headers(participations)
1616 |> json(conversations)
1619 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1620 with %Participation{} = participation <-
1621 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1622 {:ok, participation} <- Participation.mark_as_read(participation) do
1623 participation_view =
1624 ConversationView.render("participation.json", %{participation: participation, for: user})
1627 |> json(participation_view)
1631 def password_reset(conn, params) do
1632 nickname_or_email = params["email"] || params["nickname"]
1634 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1636 |> put_status(:no_content)
1639 {:error, "unknown user"} ->
1640 send_resp(conn, :not_found, "")
1643 send_resp(conn, :bad_request, "")
1647 def account_confirmation_resend(conn, params) do
1648 nickname_or_email = params["email"] || params["nickname"]
1650 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1651 {:ok, _} <- User.try_send_confirmation_email(user) do
1653 |> json_response(:no_content, "")
1657 defp try_render(conn, target, params)
1658 when is_binary(target) do
1659 case render(conn, target, params) do
1660 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1665 defp try_render(conn, _, _) do
1666 render_error(conn, :not_implemented, "Can't display this activity")
1669 defp present?(nil), do: false
1670 defp present?(false), do: false
1671 defp present?(_), do: true