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
469 activities: grouped_activities[true] || [],
473 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
478 activities: grouped_activities[false] || [],
482 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
489 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
490 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
491 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
492 true <- Visibility.visible_for_user?(activity, user) do
494 |> put_view(StatusView)
495 |> try_render("poll.json", %{object: object, for: user})
497 error when is_nil(error) or error == false ->
498 render_error(conn, :not_found, "Record not found")
502 defp get_cached_vote_or_vote(user, object, choices) do
503 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
506 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
507 case CommonAPI.vote(user, object, choices) do
508 {:error, _message} = res -> {:ignore, res}
509 res -> {:commit, res}
516 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
517 with %Object{} = object <- Object.get_by_id(id),
518 true <- object.data["type"] == "Question",
519 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
520 true <- Visibility.visible_for_user?(activity, user),
521 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
523 |> put_view(StatusView)
524 |> try_render("poll.json", %{object: object, for: user})
527 render_error(conn, :not_found, "Record not found")
530 render_error(conn, :not_found, "Record not found")
534 |> put_status(:unprocessable_entity)
535 |> json(%{error: message})
539 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
540 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
542 |> add_link_headers(scheduled_activities)
543 |> put_view(ScheduledActivityView)
544 |> render("index.json", %{scheduled_activities: scheduled_activities})
548 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
549 with %ScheduledActivity{} = scheduled_activity <-
550 ScheduledActivity.get(user, scheduled_activity_id) do
552 |> put_view(ScheduledActivityView)
553 |> render("show.json", %{scheduled_activity: scheduled_activity})
555 _ -> {:error, :not_found}
559 def update_scheduled_status(
560 %{assigns: %{user: user}} = conn,
561 %{"id" => scheduled_activity_id} = params
563 with %ScheduledActivity{} = scheduled_activity <-
564 ScheduledActivity.get(user, scheduled_activity_id),
565 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
567 |> put_view(ScheduledActivityView)
568 |> render("show.json", %{scheduled_activity: scheduled_activity})
570 nil -> {:error, :not_found}
575 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
576 with %ScheduledActivity{} = scheduled_activity <-
577 ScheduledActivity.get(user, scheduled_activity_id),
578 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
580 |> put_view(ScheduledActivityView)
581 |> render("show.json", %{scheduled_activity: scheduled_activity})
583 nil -> {:error, :not_found}
588 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
591 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
593 scheduled_at = params["scheduled_at"]
595 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
596 with {:ok, scheduled_activity} <-
597 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
599 |> put_view(ScheduledActivityView)
600 |> render("show.json", %{scheduled_activity: scheduled_activity})
603 params = Map.drop(params, ["scheduled_at"])
605 case CommonAPI.post(user, params) do
608 |> put_status(:unprocessable_entity)
609 |> json(%{error: message})
613 |> put_view(StatusView)
614 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
619 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
620 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
623 _e -> render_error(conn, :forbidden, "Can't delete this post")
627 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
628 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
629 %Activity{} = announce <- Activity.normalize(announce.data) do
631 |> put_view(StatusView)
632 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
636 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
637 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
638 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
640 |> put_view(StatusView)
641 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
645 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
646 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
647 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
649 |> put_view(StatusView)
650 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
654 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
655 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
656 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
658 |> put_view(StatusView)
659 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
663 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
664 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
666 |> put_view(StatusView)
667 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
671 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
672 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
674 |> put_view(StatusView)
675 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
679 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
680 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
681 %User{} = user <- User.get_cached_by_nickname(user.nickname),
682 true <- Visibility.visible_for_user?(activity, user),
683 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
685 |> put_view(StatusView)
686 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
690 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
691 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
692 %User{} = user <- User.get_cached_by_nickname(user.nickname),
693 true <- Visibility.visible_for_user?(activity, user),
694 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
696 |> put_view(StatusView)
697 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
701 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
702 activity = Activity.get_by_id(id)
704 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
706 |> put_view(StatusView)
707 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
711 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
712 activity = Activity.get_by_id(id)
714 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
716 |> put_view(StatusView)
717 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
721 def notifications(%{assigns: %{user: user}} = conn, params) do
722 notifications = MastodonAPI.get_notifications(user, params)
725 |> add_link_headers(notifications)
726 |> put_view(NotificationView)
727 |> render("index.json", %{notifications: notifications, for: user})
730 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
731 with {:ok, notification} <- Notification.get(user, id) do
733 |> put_view(NotificationView)
734 |> render("show.json", %{notification: notification, for: user})
738 |> put_status(:forbidden)
739 |> json(%{"error" => reason})
743 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
744 Notification.clear(user)
748 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
749 with {:ok, _notif} <- Notification.dismiss(user, id) do
754 |> put_status(:forbidden)
755 |> json(%{"error" => reason})
759 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
760 Notification.destroy_multiple(user, ids)
764 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
766 q = from(u in User, where: u.id in ^id)
767 targets = Repo.all(q)
770 |> put_view(AccountView)
771 |> render("relationships.json", %{user: user, targets: targets})
774 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
775 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
777 def update_media(%{assigns: %{user: user}} = conn, data) do
778 with %Object{} = object <- Repo.get(Object, data["id"]),
779 true <- Object.authorize_mutation(object, user),
780 true <- is_binary(data["description"]),
781 description <- data["description"] do
782 new_data = %{object.data | "name" => description}
786 |> Object.change(%{data: new_data})
789 attachment_data = Map.put(new_data, "id", object.id)
792 |> put_view(StatusView)
793 |> render("attachment.json", %{attachment: attachment_data})
797 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
798 with {:ok, object} <-
801 actor: User.ap_id(user),
802 description: Map.get(data, "description")
804 attachment_data = Map.put(object.data, "id", object.id)
807 |> put_view(StatusView)
808 |> render("attachment.json", %{attachment: attachment_data})
812 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
813 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
814 %{} = attachment_data <- Map.put(object.data, "id", object.id),
815 %{type: type} = rendered <-
816 StatusView.render("attachment.json", %{attachment: attachment_data}) do
817 # Reject if not an image
818 if type == "image" do
820 # Save to the user's info
821 info_changeset = User.Info.mascot_update(user.info, rendered)
825 |> Changeset.change()
826 |> Changeset.put_embed(:info, info_changeset)
828 {:ok, _user} = User.update_and_set_cache(user_changeset)
833 render_error(conn, :unsupported_media_type, "mascots can only be images")
838 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
839 mascot = User.get_mascot(user)
845 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
846 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
847 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
848 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
849 q = from(u in User, where: u.ap_id in ^likes)
853 |> Enum.filter(&(not User.blocks?(user, &1)))
856 |> put_view(AccountView)
857 |> render("accounts.json", %{for: user, users: users, as: :user})
859 {:visible, false} -> {:error, :not_found}
864 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
865 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
866 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
867 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
868 q = from(u in User, where: u.ap_id in ^announces)
872 |> Enum.filter(&(not User.blocks?(user, &1)))
875 |> put_view(AccountView)
876 |> render("accounts.json", %{for: user, users: users, as: :user})
878 {:visible, false} -> {:error, :not_found}
883 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
884 local_only = params["local"] in [true, "True", "true", "1"]
887 [params["tag"], params["any"]]
891 |> Enum.map(&String.downcase(&1))
896 |> Enum.map(&String.downcase(&1))
901 |> Enum.map(&String.downcase(&1))
905 |> Map.put("type", "Create")
906 |> Map.put("local_only", local_only)
907 |> Map.put("blocking_user", user)
908 |> Map.put("muting_user", user)
909 |> Map.put("user", user)
910 |> Map.put("tag", tags)
911 |> Map.put("tag_all", tag_all)
912 |> Map.put("tag_reject", tag_reject)
913 |> ActivityPub.fetch_public_activities()
917 |> add_link_headers(activities, %{"local" => local_only})
918 |> put_view(StatusView)
919 |> render("index.json", %{activities: activities, for: user, as: :activity})
922 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
923 with %User{} = user <- User.get_cached_by_id(id),
924 followers <- MastodonAPI.get_followers(user, params) do
927 for_user && user.id == for_user.id -> followers
928 user.info.hide_followers -> []
933 |> add_link_headers(followers)
934 |> put_view(AccountView)
935 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
939 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
940 with %User{} = user <- User.get_cached_by_id(id),
941 followers <- MastodonAPI.get_friends(user, params) do
944 for_user && user.id == for_user.id -> followers
945 user.info.hide_follows -> []
950 |> add_link_headers(followers)
951 |> put_view(AccountView)
952 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
956 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
957 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
959 |> put_view(AccountView)
960 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
964 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
965 with %User{} = follower <- User.get_cached_by_id(id),
966 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
968 |> put_view(AccountView)
969 |> render("relationship.json", %{user: followed, target: follower})
973 |> put_status(:forbidden)
974 |> json(%{error: message})
978 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
979 with %User{} = follower <- User.get_cached_by_id(id),
980 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
982 |> put_view(AccountView)
983 |> render("relationship.json", %{user: followed, target: follower})
987 |> put_status(:forbidden)
988 |> json(%{error: message})
992 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
993 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
994 {_, true} <- {:followed, follower.id != followed.id},
995 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
997 |> put_view(AccountView)
998 |> render("relationship.json", %{user: follower, target: followed})
1001 {:error, :not_found}
1003 {:error, message} ->
1005 |> put_status(:forbidden)
1006 |> json(%{error: message})
1010 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1011 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1012 {_, true} <- {:followed, follower.id != followed.id},
1013 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1015 |> put_view(AccountView)
1016 |> render("account.json", %{user: followed, for: follower})
1019 {:error, :not_found}
1021 {:error, message} ->
1023 |> put_status(:forbidden)
1024 |> json(%{error: message})
1028 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1029 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1030 {_, true} <- {:followed, follower.id != followed.id},
1031 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1033 |> put_view(AccountView)
1034 |> render("relationship.json", %{user: follower, target: followed})
1037 {:error, :not_found}
1044 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1046 if Map.has_key?(params, "notifications"),
1047 do: params["notifications"] in [true, "True", "true", "1"],
1050 with %User{} = muted <- User.get_cached_by_id(id),
1051 {:ok, muter} <- User.mute(muter, muted, notifications) do
1053 |> put_view(AccountView)
1054 |> render("relationship.json", %{user: muter, target: muted})
1056 {:error, message} ->
1058 |> put_status(:forbidden)
1059 |> json(%{error: message})
1063 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1064 with %User{} = muted <- User.get_cached_by_id(id),
1065 {:ok, muter} <- User.unmute(muter, muted) do
1067 |> put_view(AccountView)
1068 |> render("relationship.json", %{user: muter, target: muted})
1070 {:error, message} ->
1072 |> put_status(:forbidden)
1073 |> json(%{error: message})
1077 def mutes(%{assigns: %{user: user}} = conn, _) do
1078 with muted_accounts <- User.muted_users(user) do
1079 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1084 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1085 with %User{} = blocked <- User.get_cached_by_id(id),
1086 {:ok, blocker} <- User.block(blocker, blocked),
1087 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1089 |> put_view(AccountView)
1090 |> render("relationship.json", %{user: blocker, target: blocked})
1092 {:error, message} ->
1094 |> put_status(:forbidden)
1095 |> json(%{error: message})
1099 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1100 with %User{} = blocked <- User.get_cached_by_id(id),
1101 {:ok, blocker} <- User.unblock(blocker, blocked),
1102 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1104 |> put_view(AccountView)
1105 |> render("relationship.json", %{user: blocker, target: blocked})
1107 {:error, message} ->
1109 |> put_status(:forbidden)
1110 |> json(%{error: message})
1114 def blocks(%{assigns: %{user: user}} = conn, _) do
1115 with blocked_accounts <- User.blocked_users(user) do
1116 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1121 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1122 json(conn, info.domain_blocks || [])
1125 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1126 User.block_domain(blocker, domain)
1130 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1131 User.unblock_domain(blocker, domain)
1135 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1136 with %User{} = subscription_target <- User.get_cached_by_id(id),
1137 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1139 |> put_view(AccountView)
1140 |> render("relationship.json", %{user: user, target: subscription_target})
1142 {:error, message} ->
1144 |> put_status(:forbidden)
1145 |> json(%{error: message})
1149 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1150 with %User{} = subscription_target <- User.get_cached_by_id(id),
1151 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1153 |> put_view(AccountView)
1154 |> render("relationship.json", %{user: user, target: subscription_target})
1156 {:error, message} ->
1158 |> put_status(:forbidden)
1159 |> json(%{error: message})
1163 def favourites(%{assigns: %{user: user}} = conn, params) do
1166 |> Map.put("type", "Create")
1167 |> Map.put("favorited_by", user.ap_id)
1168 |> Map.put("blocking_user", user)
1171 ActivityPub.fetch_activities([], params)
1175 |> add_link_headers(activities)
1176 |> put_view(StatusView)
1177 |> render("index.json", %{activities: activities, for: user, as: :activity})
1180 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1181 with %User{} = user <- User.get_by_id(id),
1182 false <- user.info.hide_favorites do
1185 |> Map.put("type", "Create")
1186 |> Map.put("favorited_by", user.ap_id)
1187 |> Map.put("blocking_user", for_user)
1191 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1193 [Pleroma.Constants.as_public()]
1198 |> ActivityPub.fetch_activities(params)
1202 |> add_link_headers(activities)
1203 |> put_view(StatusView)
1204 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1206 nil -> {:error, :not_found}
1207 true -> render_error(conn, :forbidden, "Can't get favorites")
1211 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1212 user = User.get_cached_by_id(user.id)
1215 Bookmark.for_user_query(user.id)
1216 |> Pagination.fetch_paginated(params)
1220 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1223 |> add_link_headers(bookmarks)
1224 |> put_view(StatusView)
1225 |> render("index.json", %{activities: activities, for: user, as: :activity})
1228 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1229 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1230 res = ListView.render("lists.json", lists: lists)
1234 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1235 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1238 |> Map.put("type", "Create")
1239 |> Map.put("blocking_user", user)
1240 |> Map.put("user", user)
1241 |> Map.put("muting_user", user)
1243 # we must filter the following list for the user to avoid leaking statuses the user
1244 # does not actually have permission to see (for more info, peruse security issue #270).
1247 |> Enum.filter(fn x -> x in user.following end)
1248 |> ActivityPub.fetch_activities_bounded(following, params)
1252 |> put_view(StatusView)
1253 |> render("index.json", %{activities: activities, for: user, as: :activity})
1255 _e -> render_error(conn, :forbidden, "Error.")
1259 def index(%{assigns: %{user: user}} = conn, _params) do
1260 token = get_session(conn, :oauth_token)
1263 mastodon_emoji = mastodonized_emoji()
1265 limit = Config.get([:instance, :limit])
1268 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1273 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1274 access_token: token,
1276 domain: Pleroma.Web.Endpoint.host(),
1279 unfollow_modal: false,
1282 auto_play_gif: false,
1283 display_sensitive_media: false,
1284 reduce_motion: false,
1285 max_toot_chars: limit,
1286 mascot: User.get_mascot(user)["url"]
1288 poll_limits: Config.get([:instance, :poll_limits]),
1290 delete_others_notice: present?(user.info.is_moderator),
1291 admin: present?(user.info.is_admin)
1295 default_privacy: user.info.default_scope,
1296 default_sensitive: false,
1297 allow_content_types: Config.get([:instance, :allowed_post_formats])
1299 media_attachments: %{
1300 accept_content_types: [
1316 user.info.settings ||
1346 push_subscription: nil,
1348 custom_emojis: mastodon_emoji,
1354 |> put_layout(false)
1355 |> put_view(MastodonView)
1356 |> render("index.html", %{initial_state: initial_state})
1359 |> put_session(:return_to, conn.request_path)
1360 |> redirect(to: "/web/login")
1364 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1365 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1367 with changeset <- Changeset.change(user),
1368 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1369 {:ok, _user} <- User.update_and_set_cache(changeset) do
1374 |> put_status(:internal_server_error)
1375 |> json(%{error: inspect(e)})
1379 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1380 redirect(conn, to: local_mastodon_root_path(conn))
1383 @doc "Local Mastodon FE login init action"
1384 def login(conn, %{"code" => auth_token}) do
1385 with {:ok, app} <- get_or_make_app(),
1386 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1387 {:ok, token} <- Token.exchange_token(app, auth) do
1389 |> put_session(:oauth_token, token.token)
1390 |> redirect(to: local_mastodon_root_path(conn))
1394 @doc "Local Mastodon FE callback action"
1395 def login(conn, _) do
1396 with {:ok, app} <- get_or_make_app() do
1401 response_type: "code",
1402 client_id: app.client_id,
1404 scope: Enum.join(app.scopes, " ")
1407 redirect(conn, to: path)
1411 defp local_mastodon_root_path(conn) do
1412 case get_session(conn, :return_to) do
1414 mastodon_api_path(conn, :index, ["getting-started"])
1417 delete_session(conn, :return_to)
1422 defp get_or_make_app do
1423 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1424 scopes = ["read", "write", "follow", "push"]
1426 with %App{} = app <- Repo.get_by(App, find_attrs) do
1428 if app.scopes == scopes do
1432 |> Changeset.change(%{scopes: scopes})
1440 App.register_changeset(
1442 Map.put(find_attrs, :scopes, scopes)
1449 def logout(conn, _) do
1452 |> redirect(to: "/")
1455 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1456 Logger.debug("Unimplemented, returning unmodified relationship")
1458 with %User{} = target <- User.get_cached_by_id(id) do
1460 |> put_view(AccountView)
1461 |> render("relationship.json", %{user: user, target: target})
1465 def empty_array(conn, _) do
1466 Logger.debug("Unimplemented, returning an empty array")
1470 def empty_object(conn, _) do
1471 Logger.debug("Unimplemented, returning an empty object")
1475 def get_filters(%{assigns: %{user: user}} = conn, _) do
1476 filters = Filter.get_filters(user)
1477 res = FilterView.render("filters.json", filters: filters)
1482 %{assigns: %{user: user}} = conn,
1483 %{"phrase" => phrase, "context" => context} = params
1489 hide: Map.get(params, "irreversible", false),
1490 whole_word: Map.get(params, "boolean", true)
1494 {:ok, response} = Filter.create(query)
1495 res = FilterView.render("filter.json", filter: response)
1499 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1500 filter = Filter.get(filter_id, user)
1501 res = FilterView.render("filter.json", filter: filter)
1506 %{assigns: %{user: user}} = conn,
1507 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1511 filter_id: filter_id,
1514 hide: Map.get(params, "irreversible", nil),
1515 whole_word: Map.get(params, "boolean", true)
1519 {:ok, response} = Filter.update(query)
1520 res = FilterView.render("filter.json", filter: response)
1524 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1527 filter_id: filter_id
1530 {:ok, _} = Filter.delete(query)
1534 def suggestions(%{assigns: %{user: user}} = conn, _) do
1535 suggestions = Config.get(:suggestions)
1537 if Keyword.get(suggestions, :enabled, false) do
1538 api = Keyword.get(suggestions, :third_party_engine, "")
1539 timeout = Keyword.get(suggestions, :timeout, 5000)
1540 limit = Keyword.get(suggestions, :limit, 23)
1542 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1544 user = user.nickname
1548 |> String.replace("{{host}}", host)
1549 |> String.replace("{{user}}", user)
1551 with {:ok, %{status: 200, body: body}} <-
1552 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1553 {:ok, data} <- Jason.decode(body) do
1556 |> Enum.slice(0, limit)
1559 |> Map.put("id", fetch_suggestion_id(x))
1560 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1561 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1567 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1574 defp fetch_suggestion_id(attrs) do
1575 case User.get_or_fetch(attrs["acct"]) do
1576 {:ok, %User{id: id}} -> id
1581 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1582 with %Activity{} = activity <- Activity.get_by_id(status_id),
1583 true <- Visibility.visible_for_user?(activity, user) do
1587 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1597 def reports(%{assigns: %{user: user}} = conn, params) do
1598 case CommonAPI.report(user, params) do
1601 |> put_view(ReportView)
1602 |> try_render("report.json", %{activity: activity})
1606 |> put_status(:bad_request)
1607 |> json(%{error: err})
1611 def account_register(
1612 %{assigns: %{app: app}} = conn,
1613 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1621 "captcha_answer_data",
1625 |> Map.put("nickname", nickname)
1626 |> Map.put("fullname", params["fullname"] || nickname)
1627 |> Map.put("bio", params["bio"] || "")
1628 |> Map.put("confirm", params["password"])
1630 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1631 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1633 token_type: "Bearer",
1634 access_token: token.token,
1636 created_at: Token.Utils.format_created_at(token)
1641 |> put_status(:bad_request)
1646 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1647 render_error(conn, :bad_request, "Missing parameters")
1650 def account_register(conn, _) do
1651 render_error(conn, :forbidden, "Invalid credentials")
1654 def conversations(%{assigns: %{user: user}} = conn, params) do
1655 participations = Participation.for_user_with_last_activity_id(user, params)
1658 Enum.map(participations, fn participation ->
1659 ConversationView.render("participation.json", %{participation: participation, for: user})
1663 |> add_link_headers(participations)
1664 |> json(conversations)
1667 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1668 with %Participation{} = participation <-
1669 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1670 {:ok, participation} <- Participation.mark_as_read(participation) do
1671 participation_view =
1672 ConversationView.render("participation.json", %{participation: participation, for: user})
1675 |> json(participation_view)
1679 def password_reset(conn, params) do
1680 nickname_or_email = params["email"] || params["nickname"]
1682 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1684 |> put_status(:no_content)
1687 {:error, "unknown user"} ->
1688 send_resp(conn, :not_found, "")
1691 send_resp(conn, :bad_request, "")
1695 def account_confirmation_resend(conn, params) do
1696 nickname_or_email = params["email"] || params["nickname"]
1698 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1699 {:ok, _} <- User.try_send_confirmation_email(user) do
1701 |> json_response(:no_content, "")
1705 def try_render(conn, target, params)
1706 when is_binary(target) do
1707 case render(conn, target, params) do
1708 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1713 def try_render(conn, _, _) do
1714 render_error(conn, :not_implemented, "Can't display this activity")
1717 defp present?(nil), do: false
1718 defp present?(false), do: false
1719 defp present?(_), do: true