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.RichMedia
48 alias Pleroma.Web.TwitterAPI.TwitterAPI
50 alias Pleroma.Web.ControllerHelper
54 require Pleroma.Constants
56 @rate_limited_relations_actions ~w(follow unfollow)a
58 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
59 post_status delete_status)a
63 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
64 when action in ~w(reblog_status unreblog_status)a
69 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
70 when action in ~w(fav_status unfav_status)a
75 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
78 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
79 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
80 plug(RateLimiter, :app_account_creation when action == :account_register)
81 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
82 plug(RateLimiter, :password_reset when action == :password_reset)
83 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
85 @local_mastodon_name "Mastodon-Local"
87 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
89 def create_app(conn, params) do
90 scopes = Scopes.fetch_scopes(params, ["read"])
94 |> Map.drop(["scope", "scopes"])
95 |> Map.put("scopes", scopes)
97 with cs <- App.register_changeset(%App{}, app_attrs),
98 false <- cs.changes[:client_name] == @local_mastodon_name,
99 {:ok, app} <- Repo.insert(cs) do
102 |> render("show.json", %{app: app})
111 value_function \\ fn x -> {:ok, x} end
113 if Map.has_key?(params, params_field) do
114 case value_function.(params[params_field]) do
115 {:ok, new_value} -> Map.put(map, map_field, new_value)
123 def update_credentials(%{assigns: %{user: user}} = conn, params) do
128 |> add_if_present(params, "display_name", :name)
129 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
130 |> add_if_present(params, "avatar", :avatar, fn value ->
131 with %Plug.Upload{} <- value,
132 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
139 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
143 |> Map.get(:emoji, [])
144 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
151 :hide_followers_count,
157 :skip_thread_containment
159 |> Enum.reduce(%{}, fn key, acc ->
160 add_if_present(acc, params, to_string(key), key, fn value ->
161 {:ok, ControllerHelper.truthy_param?(value)}
164 |> add_if_present(params, "default_scope", :default_scope)
165 |> add_if_present(params, "fields", :fields, fn fields ->
166 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
170 |> add_if_present(params, "fields", :raw_fields)
171 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
172 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
174 |> add_if_present(params, "header", :banner, fn value ->
175 with %Plug.Upload{} <- value,
176 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
182 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
183 with %Plug.Upload{} <- value,
184 {:ok, object} <- ActivityPub.upload(value, type: :background) do
190 |> Map.put(:emoji, user_info_emojis)
192 info_cng = User.Info.profile_update(user.info, info_params)
194 with changeset <- User.update_changeset(user, user_params),
195 changeset <- Changeset.put_embed(changeset, :info, info_cng),
196 {:ok, user} <- User.update_and_set_cache(changeset) do
197 if original_user != user do
198 CommonAPI.update(user)
203 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
206 _e -> render_error(conn, :forbidden, "Invalid request")
210 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
211 change = Changeset.change(user, %{avatar: nil})
212 {:ok, user} = User.update_and_set_cache(change)
213 CommonAPI.update(user)
215 json(conn, %{url: nil})
218 def update_avatar(%{assigns: %{user: user}} = conn, params) do
219 {:ok, object} = ActivityPub.upload(params, type: :avatar)
220 change = Changeset.change(user, %{avatar: object.data})
221 {:ok, user} = User.update_and_set_cache(change)
222 CommonAPI.update(user)
223 %{"url" => [%{"href" => href} | _]} = object.data
225 json(conn, %{url: href})
228 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
229 with new_info <- %{"banner" => %{}},
230 info_cng <- User.Info.profile_update(user.info, new_info),
231 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
232 {:ok, user} <- User.update_and_set_cache(changeset) do
233 CommonAPI.update(user)
235 json(conn, %{url: nil})
239 def update_banner(%{assigns: %{user: user}} = conn, params) do
240 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
241 new_info <- %{"banner" => object.data},
242 info_cng <- User.Info.profile_update(user.info, new_info),
243 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
244 {:ok, user} <- User.update_and_set_cache(changeset) do
245 CommonAPI.update(user)
246 %{"url" => [%{"href" => href} | _]} = object.data
248 json(conn, %{url: href})
252 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
253 with new_info <- %{"background" => %{}},
254 info_cng <- User.Info.profile_update(user.info, new_info),
255 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
256 {:ok, _user} <- User.update_and_set_cache(changeset) do
257 json(conn, %{url: nil})
261 def update_background(%{assigns: %{user: user}} = conn, params) do
262 with {:ok, object} <- ActivityPub.upload(params, type: :background),
263 new_info <- %{"background" => object.data},
264 info_cng <- User.Info.profile_update(user.info, new_info),
265 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
266 {:ok, _user} <- User.update_and_set_cache(changeset) do
267 %{"url" => [%{"href" => href} | _]} = object.data
269 json(conn, %{url: href})
273 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
274 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
277 AccountView.render("account.json", %{
280 with_pleroma_settings: true,
281 with_chat_token: chat_token
287 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
288 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
291 |> render("short.json", %{app: app})
295 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
296 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
297 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
298 account = AccountView.render("account.json", %{user: user, for: for_user})
301 _e -> render_error(conn, :not_found, "Can't find user")
305 @mastodon_api_level "2.7.2"
307 def masto_instance(conn, _params) do
308 instance = Config.get(:instance)
312 title: Keyword.get(instance, :name),
313 description: Keyword.get(instance, :description),
314 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
315 email: Keyword.get(instance, :email),
317 streaming_api: Pleroma.Web.Endpoint.websocket_url()
319 stats: Stats.get_stats(),
320 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
322 registrations: Pleroma.Config.get([:instance, :registrations_open]),
323 # Extra (not present in Mastodon):
324 max_toot_chars: Keyword.get(instance, :limit),
325 poll_limits: Keyword.get(instance, :poll_limits)
331 def peers(conn, _params) do
332 json(conn, Stats.get_peers())
335 defp mastodonized_emoji do
336 Pleroma.Emoji.get_all()
337 |> Enum.map(fn {shortcode, relative_url, tags} ->
338 url = to_string(URI.merge(Web.base_url(), relative_url))
341 "shortcode" => shortcode,
343 "visible_in_picker" => true,
346 # Assuming that a comma is authorized in the category name
347 "category" => (tags -- ["Custom"]) |> Enum.join(",")
352 def custom_emojis(conn, _params) do
353 mastodon_emoji = mastodonized_emoji()
354 json(conn, mastodon_emoji)
357 def home_timeline(%{assigns: %{user: user}} = conn, params) do
360 |> Map.put("type", ["Create", "Announce"])
361 |> Map.put("blocking_user", user)
362 |> Map.put("muting_user", user)
363 |> Map.put("user", user)
366 [user.ap_id | user.following]
367 |> ActivityPub.fetch_activities(params)
371 |> add_link_headers(activities)
372 |> put_view(StatusView)
373 |> render("index.json", %{activities: activities, for: user, as: :activity})
376 def public_timeline(%{assigns: %{user: user}} = conn, params) do
377 local_only = params["local"] in [true, "True", "true", "1"]
381 |> Map.put("type", ["Create", "Announce"])
382 |> Map.put("local_only", local_only)
383 |> Map.put("blocking_user", user)
384 |> Map.put("muting_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" => id}) do
1534 with %Activity{} = activity <- Activity.get_by_id(id),
1535 true <- Visibility.visible_for_user?(activity, user) do
1536 data = RichMedia.Helpers.fetch_data_for_activity(activity)
1539 |> put_view(StatusView)
1540 |> render("card.json", data)
1542 _e -> {:error, :not_found}
1546 def reports(%{assigns: %{user: user}} = conn, params) do
1547 case CommonAPI.report(user, params) do
1550 |> put_view(ReportView)
1551 |> try_render("report.json", %{activity: activity})
1555 |> put_status(:bad_request)
1556 |> json(%{error: err})
1560 def account_register(
1561 %{assigns: %{app: app}} = conn,
1562 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1570 "captcha_answer_data",
1574 |> Map.put("nickname", nickname)
1575 |> Map.put("fullname", params["fullname"] || nickname)
1576 |> Map.put("bio", params["bio"] || "")
1577 |> Map.put("confirm", params["password"])
1579 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1580 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1582 token_type: "Bearer",
1583 access_token: token.token,
1585 created_at: Token.Utils.format_created_at(token)
1590 |> put_status(:bad_request)
1595 def account_register(%{assigns: %{app: _app}} = conn, _) do
1596 render_error(conn, :bad_request, "Missing parameters")
1599 def account_register(conn, _) do
1600 render_error(conn, :forbidden, "Invalid credentials")
1603 def conversations(%{assigns: %{user: user}} = conn, params) do
1604 participations = Participation.for_user_with_last_activity_id(user, params)
1607 Enum.map(participations, fn participation ->
1608 ConversationView.render("participation.json", %{participation: participation, for: user})
1612 |> add_link_headers(participations)
1613 |> json(conversations)
1616 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1617 with %Participation{} = participation <-
1618 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1619 {:ok, participation} <- Participation.mark_as_read(participation) do
1620 participation_view =
1621 ConversationView.render("participation.json", %{participation: participation, for: user})
1624 |> json(participation_view)
1628 def password_reset(conn, params) do
1629 nickname_or_email = params["email"] || params["nickname"]
1631 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1633 |> put_status(:no_content)
1636 {:error, "unknown user"} ->
1637 send_resp(conn, :not_found, "")
1640 send_resp(conn, :bad_request, "")
1644 def account_confirmation_resend(conn, params) do
1645 nickname_or_email = params["email"] || params["nickname"]
1647 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1648 {:ok, _} <- User.try_send_confirmation_email(user) do
1650 |> json_response(:no_content, "")
1654 defp try_render(conn, target, params)
1655 when is_binary(target) do
1656 case render(conn, target, params) do
1657 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1662 defp try_render(conn, _, _) do
1663 render_error(conn, :not_implemented, "Can't display this activity")
1666 defp present?(nil), do: false
1667 defp present?(false), do: false
1668 defp present?(_), do: true