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: 5, add_link_headers: 4, 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))
147 if Map.has_key?(params, "fields_attributes") do
148 Map.update!(params, "fields_attributes", fn fields ->
149 if Enum.all?(fields, &is_tuple/1) do
150 Enum.map(fields, fn {_, v} -> v end)
154 |> Enum.filter(fn %{"name" => n} -> n != "" end)
168 :skip_thread_containment
170 |> Enum.reduce(%{}, fn key, acc ->
171 add_if_present(acc, params, to_string(key), key, fn value ->
172 {:ok, ControllerHelper.truthy_param?(value)}
175 |> add_if_present(params, "default_scope", :default_scope)
176 |> add_if_present(params, "fields_attributes", :fields, fn fields ->
177 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
181 |> add_if_present(params, "fields_attributes", :raw_fields)
182 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
183 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
185 |> add_if_present(params, "header", :banner, fn value ->
186 with %Plug.Upload{} <- value,
187 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
193 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
194 with %Plug.Upload{} <- value,
195 {:ok, object} <- ActivityPub.upload(value, type: :background) do
201 |> Map.put(:emoji, user_info_emojis)
203 info_cng = User.Info.profile_update(user.info, info_params)
205 with changeset <- User.update_changeset(user, user_params),
206 changeset <- Changeset.put_embed(changeset, :info, info_cng),
207 {:ok, user} <- User.update_and_set_cache(changeset) do
208 if original_user != user do
209 CommonAPI.update(user)
214 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
217 _e -> render_error(conn, :forbidden, "Invalid request")
221 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
222 change = Changeset.change(user, %{avatar: nil})
223 {:ok, user} = User.update_and_set_cache(change)
224 CommonAPI.update(user)
226 json(conn, %{url: nil})
229 def update_avatar(%{assigns: %{user: user}} = conn, params) do
230 {:ok, object} = ActivityPub.upload(params, type: :avatar)
231 change = Changeset.change(user, %{avatar: object.data})
232 {:ok, user} = User.update_and_set_cache(change)
233 CommonAPI.update(user)
234 %{"url" => [%{"href" => href} | _]} = object.data
236 json(conn, %{url: href})
239 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
240 with new_info <- %{"banner" => %{}},
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)
246 json(conn, %{url: nil})
250 def update_banner(%{assigns: %{user: user}} = conn, params) do
251 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
252 new_info <- %{"banner" => object.data},
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 CommonAPI.update(user)
257 %{"url" => [%{"href" => href} | _]} = object.data
259 json(conn, %{url: href})
263 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
264 with new_info <- %{"background" => %{}},
265 info_cng <- User.Info.profile_update(user.info, new_info),
266 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
267 {:ok, _user} <- User.update_and_set_cache(changeset) do
268 json(conn, %{url: nil})
272 def update_background(%{assigns: %{user: user}} = conn, params) do
273 with {:ok, object} <- ActivityPub.upload(params, type: :background),
274 new_info <- %{"background" => object.data},
275 info_cng <- User.Info.profile_update(user.info, new_info),
276 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
277 {:ok, _user} <- User.update_and_set_cache(changeset) do
278 %{"url" => [%{"href" => href} | _]} = object.data
280 json(conn, %{url: href})
284 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
285 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
288 AccountView.render("account.json", %{
291 with_pleroma_settings: true,
292 with_chat_token: chat_token
298 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
299 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
302 |> render("short.json", %{app: app})
306 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
307 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
308 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
309 account = AccountView.render("account.json", %{user: user, for: for_user})
312 _e -> render_error(conn, :not_found, "Can't find user")
316 @mastodon_api_level "2.7.2"
318 def masto_instance(conn, _params) do
319 instance = Config.get(:instance)
323 title: Keyword.get(instance, :name),
324 description: Keyword.get(instance, :description),
325 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
326 email: Keyword.get(instance, :email),
328 streaming_api: Pleroma.Web.Endpoint.websocket_url()
330 stats: Stats.get_stats(),
331 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
333 registrations: Pleroma.Config.get([:instance, :registrations_open]),
334 # Extra (not present in Mastodon):
335 max_toot_chars: Keyword.get(instance, :limit),
336 poll_limits: Keyword.get(instance, :poll_limits)
342 def peers(conn, _params) do
343 json(conn, Stats.get_peers())
346 defp mastodonized_emoji do
347 Pleroma.Emoji.get_all()
348 |> Enum.map(fn {shortcode, relative_url, tags} ->
349 url = to_string(URI.merge(Web.base_url(), relative_url))
352 "shortcode" => shortcode,
354 "visible_in_picker" => true,
357 # Assuming that a comma is authorized in the category name
358 "category" => (tags -- ["Custom"]) |> Enum.join(",")
363 def custom_emojis(conn, _params) do
364 mastodon_emoji = mastodonized_emoji()
365 json(conn, mastodon_emoji)
368 def home_timeline(%{assigns: %{user: user}} = conn, params) do
371 |> Map.put("type", ["Create", "Announce"])
372 |> Map.put("blocking_user", user)
373 |> Map.put("muting_user", user)
374 |> Map.put("user", user)
377 [user.ap_id | user.following]
378 |> ActivityPub.fetch_activities(params)
382 |> add_link_headers(:home_timeline, activities)
383 |> put_view(StatusView)
384 |> render("index.json", %{activities: activities, for: user, as: :activity})
387 def public_timeline(%{assigns: %{user: user}} = conn, params) do
388 local_only = params["local"] in [true, "True", "true", "1"]
392 |> Map.put("type", ["Create", "Announce"])
393 |> Map.put("local_only", local_only)
394 |> Map.put("blocking_user", user)
395 |> Map.put("muting_user", user)
396 |> Map.put("user", user)
397 |> ActivityPub.fetch_public_activities()
401 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
402 |> put_view(StatusView)
403 |> render("index.json", %{activities: activities, for: user, as: :activity})
406 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
407 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
410 |> Map.put("tag", params["tagged"])
412 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
415 |> add_link_headers(:user_statuses, activities, params["id"])
416 |> put_view(StatusView)
417 |> render("index.json", %{
418 activities: activities,
425 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
428 |> Map.put("type", "Create")
429 |> Map.put("blocking_user", user)
430 |> Map.put("user", user)
431 |> Map.put(:visibility, "direct")
435 |> ActivityPub.fetch_activities_query(params)
436 |> Pagination.fetch_paginated(params)
439 |> add_link_headers(:dm_timeline, activities)
440 |> put_view(StatusView)
441 |> render("index.json", %{activities: activities, for: user, as: :activity})
444 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
450 |> Activity.all_by_ids_with_object()
451 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
454 |> put_view(StatusView)
455 |> render("index.json", activities: activities, for: user, as: :activity)
458 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
459 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
460 true <- Visibility.visible_for_user?(activity, user) do
462 |> put_view(StatusView)
463 |> try_render("status.json", %{activity: activity, for: user})
467 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
468 with %Activity{} = activity <- Activity.get_by_id(id),
470 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
471 "blocking_user" => user,
473 "exclude_id" => activity.id
475 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
481 activities: grouped_activities[true] || [],
485 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
490 activities: grouped_activities[false] || [],
494 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
501 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
502 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
503 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
504 true <- Visibility.visible_for_user?(activity, user) do
506 |> put_view(StatusView)
507 |> try_render("poll.json", %{object: object, for: user})
509 error when is_nil(error) or error == false ->
510 render_error(conn, :not_found, "Record not found")
514 defp get_cached_vote_or_vote(user, object, choices) do
515 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
518 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
519 case CommonAPI.vote(user, object, choices) do
520 {:error, _message} = res -> {:ignore, res}
521 res -> {:commit, res}
528 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
529 with %Object{} = object <- Object.get_by_id(id),
530 true <- object.data["type"] == "Question",
531 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
532 true <- Visibility.visible_for_user?(activity, user),
533 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
535 |> put_view(StatusView)
536 |> try_render("poll.json", %{object: object, for: user})
539 render_error(conn, :not_found, "Record not found")
542 render_error(conn, :not_found, "Record not found")
546 |> put_status(:unprocessable_entity)
547 |> json(%{error: message})
551 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
552 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
554 |> add_link_headers(:scheduled_statuses, scheduled_activities)
555 |> put_view(ScheduledActivityView)
556 |> render("index.json", %{scheduled_activities: scheduled_activities})
560 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
561 with %ScheduledActivity{} = scheduled_activity <-
562 ScheduledActivity.get(user, scheduled_activity_id) do
564 |> put_view(ScheduledActivityView)
565 |> render("show.json", %{scheduled_activity: scheduled_activity})
567 _ -> {:error, :not_found}
571 def update_scheduled_status(
572 %{assigns: %{user: user}} = conn,
573 %{"id" => scheduled_activity_id} = params
575 with %ScheduledActivity{} = scheduled_activity <-
576 ScheduledActivity.get(user, scheduled_activity_id),
577 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
579 |> put_view(ScheduledActivityView)
580 |> render("show.json", %{scheduled_activity: scheduled_activity})
582 nil -> {:error, :not_found}
587 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
588 with %ScheduledActivity{} = scheduled_activity <-
589 ScheduledActivity.get(user, scheduled_activity_id),
590 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
592 |> put_view(ScheduledActivityView)
593 |> render("show.json", %{scheduled_activity: scheduled_activity})
595 nil -> {:error, :not_found}
600 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
603 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
605 scheduled_at = params["scheduled_at"]
607 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
608 with {:ok, scheduled_activity} <-
609 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
611 |> put_view(ScheduledActivityView)
612 |> render("show.json", %{scheduled_activity: scheduled_activity})
615 params = Map.drop(params, ["scheduled_at"])
617 case CommonAPI.post(user, params) do
620 |> put_status(:unprocessable_entity)
621 |> json(%{error: message})
625 |> put_view(StatusView)
626 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
631 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
632 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
635 _e -> render_error(conn, :forbidden, "Can't delete this post")
639 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
640 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
641 %Activity{} = announce <- Activity.normalize(announce.data) do
643 |> put_view(StatusView)
644 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
648 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
649 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
650 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
658 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(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 unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
667 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
668 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
670 |> put_view(StatusView)
671 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
675 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
676 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
678 |> put_view(StatusView)
679 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
683 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
684 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
686 |> put_view(StatusView)
687 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
691 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
692 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
693 %User{} = user <- User.get_cached_by_nickname(user.nickname),
694 true <- Visibility.visible_for_user?(activity, user),
695 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
697 |> put_view(StatusView)
698 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
702 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
703 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
704 %User{} = user <- User.get_cached_by_nickname(user.nickname),
705 true <- Visibility.visible_for_user?(activity, user),
706 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
708 |> put_view(StatusView)
709 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
713 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
714 activity = Activity.get_by_id(id)
716 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
718 |> put_view(StatusView)
719 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
723 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
724 activity = Activity.get_by_id(id)
726 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
728 |> put_view(StatusView)
729 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
733 def notifications(%{assigns: %{user: user}} = conn, params) do
734 notifications = MastodonAPI.get_notifications(user, params)
737 |> add_link_headers(:notifications, notifications)
738 |> put_view(NotificationView)
739 |> render("index.json", %{notifications: notifications, for: user})
742 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
743 with {:ok, notification} <- Notification.get(user, id) do
745 |> put_view(NotificationView)
746 |> render("show.json", %{notification: notification, for: user})
750 |> put_status(:forbidden)
751 |> json(%{"error" => reason})
755 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
756 Notification.clear(user)
760 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
761 with {:ok, _notif} <- Notification.dismiss(user, id) do
766 |> put_status(:forbidden)
767 |> json(%{"error" => reason})
771 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
772 Notification.destroy_multiple(user, ids)
776 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
778 q = from(u in User, where: u.id in ^id)
779 targets = Repo.all(q)
782 |> put_view(AccountView)
783 |> render("relationships.json", %{user: user, targets: targets})
786 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
787 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
789 def update_media(%{assigns: %{user: user}} = conn, data) do
790 with %Object{} = object <- Repo.get(Object, data["id"]),
791 true <- Object.authorize_mutation(object, user),
792 true <- is_binary(data["description"]),
793 description <- data["description"] do
794 new_data = %{object.data | "name" => description}
798 |> Object.change(%{data: new_data})
801 attachment_data = Map.put(new_data, "id", object.id)
804 |> put_view(StatusView)
805 |> render("attachment.json", %{attachment: attachment_data})
809 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
810 with {:ok, object} <-
813 actor: User.ap_id(user),
814 description: Map.get(data, "description")
816 attachment_data = Map.put(object.data, "id", object.id)
819 |> put_view(StatusView)
820 |> render("attachment.json", %{attachment: attachment_data})
824 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
825 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
826 %{} = attachment_data <- Map.put(object.data, "id", object.id),
827 %{type: type} = rendered <-
828 StatusView.render("attachment.json", %{attachment: attachment_data}) do
829 # Reject if not an image
830 if type == "image" do
832 # Save to the user's info
833 info_changeset = User.Info.mascot_update(user.info, rendered)
837 |> Changeset.change()
838 |> Changeset.put_embed(:info, info_changeset)
840 {:ok, _user} = User.update_and_set_cache(user_changeset)
845 render_error(conn, :unsupported_media_type, "mascots can only be images")
850 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
851 mascot = User.get_mascot(user)
857 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
858 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
859 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
860 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
861 q = from(u in User, where: u.ap_id in ^likes)
865 |> Enum.filter(&(not User.blocks?(user, &1)))
868 |> put_view(AccountView)
869 |> render("accounts.json", %{for: user, users: users, as: :user})
871 {:visible, false} -> {:error, :not_found}
876 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
877 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
878 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
879 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
880 q = from(u in User, where: u.ap_id in ^announces)
884 |> Enum.filter(&(not User.blocks?(user, &1)))
887 |> put_view(AccountView)
888 |> render("accounts.json", %{for: user, users: users, as: :user})
890 {:visible, false} -> {:error, :not_found}
895 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
896 local_only = params["local"] in [true, "True", "true", "1"]
899 [params["tag"], params["any"]]
903 |> Enum.map(&String.downcase(&1))
908 |> Enum.map(&String.downcase(&1))
913 |> Enum.map(&String.downcase(&1))
917 |> Map.put("type", "Create")
918 |> Map.put("local_only", local_only)
919 |> Map.put("blocking_user", user)
920 |> Map.put("muting_user", user)
921 |> Map.put("user", user)
922 |> Map.put("tag", tags)
923 |> Map.put("tag_all", tag_all)
924 |> Map.put("tag_reject", tag_reject)
925 |> ActivityPub.fetch_public_activities()
929 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
930 |> put_view(StatusView)
931 |> render("index.json", %{activities: activities, for: user, as: :activity})
934 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
935 with %User{} = user <- User.get_cached_by_id(id),
936 followers <- MastodonAPI.get_followers(user, params) do
939 for_user && user.id == for_user.id -> followers
940 user.info.hide_followers -> []
945 |> add_link_headers(:followers, followers, user)
946 |> put_view(AccountView)
947 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
951 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
952 with %User{} = user <- User.get_cached_by_id(id),
953 followers <- MastodonAPI.get_friends(user, params) do
956 for_user && user.id == for_user.id -> followers
957 user.info.hide_follows -> []
962 |> add_link_headers(:following, followers, user)
963 |> put_view(AccountView)
964 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
968 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
969 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
971 |> put_view(AccountView)
972 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
976 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
977 with %User{} = follower <- User.get_cached_by_id(id),
978 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
980 |> put_view(AccountView)
981 |> render("relationship.json", %{user: followed, target: follower})
985 |> put_status(:forbidden)
986 |> json(%{error: message})
990 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
991 with %User{} = follower <- User.get_cached_by_id(id),
992 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
994 |> put_view(AccountView)
995 |> render("relationship.json", %{user: followed, target: follower})
999 |> put_status(:forbidden)
1000 |> json(%{error: message})
1004 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1005 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1006 {_, true} <- {:followed, follower.id != followed.id},
1007 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1009 |> put_view(AccountView)
1010 |> render("relationship.json", %{user: follower, target: followed})
1013 {:error, :not_found}
1015 {:error, message} ->
1017 |> put_status(:forbidden)
1018 |> json(%{error: message})
1022 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1023 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1024 {_, true} <- {:followed, follower.id != followed.id},
1025 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1027 |> put_view(AccountView)
1028 |> render("account.json", %{user: followed, for: follower})
1031 {:error, :not_found}
1033 {:error, message} ->
1035 |> put_status(:forbidden)
1036 |> json(%{error: message})
1040 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1041 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1042 {_, true} <- {:followed, follower.id != followed.id},
1043 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1045 |> put_view(AccountView)
1046 |> render("relationship.json", %{user: follower, target: followed})
1049 {:error, :not_found}
1056 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1058 if Map.has_key?(params, "notifications"),
1059 do: params["notifications"] in [true, "True", "true", "1"],
1062 with %User{} = muted <- User.get_cached_by_id(id),
1063 {:ok, muter} <- User.mute(muter, muted, notifications) do
1065 |> put_view(AccountView)
1066 |> render("relationship.json", %{user: muter, target: muted})
1068 {:error, message} ->
1070 |> put_status(:forbidden)
1071 |> json(%{error: message})
1075 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1076 with %User{} = muted <- User.get_cached_by_id(id),
1077 {:ok, muter} <- User.unmute(muter, muted) do
1079 |> put_view(AccountView)
1080 |> render("relationship.json", %{user: muter, target: muted})
1082 {:error, message} ->
1084 |> put_status(:forbidden)
1085 |> json(%{error: message})
1089 def mutes(%{assigns: %{user: user}} = conn, _) do
1090 with muted_accounts <- User.muted_users(user) do
1091 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1096 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1097 with %User{} = blocked <- User.get_cached_by_id(id),
1098 {:ok, blocker} <- User.block(blocker, blocked),
1099 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1101 |> put_view(AccountView)
1102 |> render("relationship.json", %{user: blocker, target: blocked})
1104 {:error, message} ->
1106 |> put_status(:forbidden)
1107 |> json(%{error: message})
1111 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1112 with %User{} = blocked <- User.get_cached_by_id(id),
1113 {:ok, blocker} <- User.unblock(blocker, blocked),
1114 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1116 |> put_view(AccountView)
1117 |> render("relationship.json", %{user: blocker, target: blocked})
1119 {:error, message} ->
1121 |> put_status(:forbidden)
1122 |> json(%{error: message})
1126 def blocks(%{assigns: %{user: user}} = conn, _) do
1127 with blocked_accounts <- User.blocked_users(user) do
1128 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1133 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1134 json(conn, info.domain_blocks || [])
1137 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1138 User.block_domain(blocker, domain)
1142 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1143 User.unblock_domain(blocker, domain)
1147 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1148 with %User{} = subscription_target <- User.get_cached_by_id(id),
1149 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1151 |> put_view(AccountView)
1152 |> render("relationship.json", %{user: user, target: subscription_target})
1154 {:error, message} ->
1156 |> put_status(:forbidden)
1157 |> json(%{error: message})
1161 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1162 with %User{} = subscription_target <- User.get_cached_by_id(id),
1163 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1165 |> put_view(AccountView)
1166 |> render("relationship.json", %{user: user, target: subscription_target})
1168 {:error, message} ->
1170 |> put_status(:forbidden)
1171 |> json(%{error: message})
1175 def favourites(%{assigns: %{user: user}} = conn, params) do
1178 |> Map.put("type", "Create")
1179 |> Map.put("favorited_by", user.ap_id)
1180 |> Map.put("blocking_user", user)
1183 ActivityPub.fetch_activities([], params)
1187 |> add_link_headers(:favourites, activities)
1188 |> put_view(StatusView)
1189 |> render("index.json", %{activities: activities, for: user, as: :activity})
1192 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1193 with %User{} = user <- User.get_by_id(id),
1194 false <- user.info.hide_favorites do
1197 |> Map.put("type", "Create")
1198 |> Map.put("favorited_by", user.ap_id)
1199 |> Map.put("blocking_user", for_user)
1203 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1205 [Pleroma.Constants.as_public()]
1210 |> ActivityPub.fetch_activities(params)
1214 |> add_link_headers(:favourites, activities)
1215 |> put_view(StatusView)
1216 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1218 nil -> {:error, :not_found}
1219 true -> render_error(conn, :forbidden, "Can't get favorites")
1223 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1224 user = User.get_cached_by_id(user.id)
1227 Bookmark.for_user_query(user.id)
1228 |> Pagination.fetch_paginated(params)
1232 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1235 |> add_link_headers(:bookmarks, bookmarks)
1236 |> put_view(StatusView)
1237 |> render("index.json", %{activities: activities, for: user, as: :activity})
1240 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1241 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1242 res = ListView.render("lists.json", lists: lists)
1246 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1247 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1250 |> Map.put("type", "Create")
1251 |> Map.put("blocking_user", user)
1252 |> Map.put("user", user)
1253 |> Map.put("muting_user", user)
1255 # we must filter the following list for the user to avoid leaking statuses the user
1256 # does not actually have permission to see (for more info, peruse security issue #270).
1259 |> Enum.filter(fn x -> x in user.following end)
1260 |> ActivityPub.fetch_activities_bounded(following, params)
1264 |> put_view(StatusView)
1265 |> render("index.json", %{activities: activities, for: user, as: :activity})
1267 _e -> render_error(conn, :forbidden, "Error.")
1271 def index(%{assigns: %{user: user}} = conn, _params) do
1272 token = get_session(conn, :oauth_token)
1275 mastodon_emoji = mastodonized_emoji()
1277 limit = Config.get([:instance, :limit])
1280 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1285 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1286 access_token: token,
1288 domain: Pleroma.Web.Endpoint.host(),
1291 unfollow_modal: false,
1294 auto_play_gif: false,
1295 display_sensitive_media: false,
1296 reduce_motion: false,
1297 max_toot_chars: limit,
1298 mascot: User.get_mascot(user)["url"]
1300 poll_limits: Config.get([:instance, :poll_limits]),
1302 delete_others_notice: present?(user.info.is_moderator),
1303 admin: present?(user.info.is_admin)
1307 default_privacy: user.info.default_scope,
1308 default_sensitive: false,
1309 allow_content_types: Config.get([:instance, :allowed_post_formats])
1311 media_attachments: %{
1312 accept_content_types: [
1328 user.info.settings ||
1358 push_subscription: nil,
1360 custom_emojis: mastodon_emoji,
1366 |> put_layout(false)
1367 |> put_view(MastodonView)
1368 |> render("index.html", %{initial_state: initial_state})
1371 |> put_session(:return_to, conn.request_path)
1372 |> redirect(to: "/web/login")
1376 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1377 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1379 with changeset <- Changeset.change(user),
1380 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1381 {:ok, _user} <- User.update_and_set_cache(changeset) do
1386 |> put_status(:internal_server_error)
1387 |> json(%{error: inspect(e)})
1391 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1392 redirect(conn, to: local_mastodon_root_path(conn))
1395 @doc "Local Mastodon FE login init action"
1396 def login(conn, %{"code" => auth_token}) do
1397 with {:ok, app} <- get_or_make_app(),
1398 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1399 {:ok, token} <- Token.exchange_token(app, auth) do
1401 |> put_session(:oauth_token, token.token)
1402 |> redirect(to: local_mastodon_root_path(conn))
1406 @doc "Local Mastodon FE callback action"
1407 def login(conn, _) do
1408 with {:ok, app} <- get_or_make_app() do
1413 response_type: "code",
1414 client_id: app.client_id,
1416 scope: Enum.join(app.scopes, " ")
1419 redirect(conn, to: path)
1423 defp local_mastodon_root_path(conn) do
1424 case get_session(conn, :return_to) do
1426 mastodon_api_path(conn, :index, ["getting-started"])
1429 delete_session(conn, :return_to)
1434 defp get_or_make_app do
1435 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1436 scopes = ["read", "write", "follow", "push"]
1438 with %App{} = app <- Repo.get_by(App, find_attrs) do
1440 if app.scopes == scopes do
1444 |> Changeset.change(%{scopes: scopes})
1452 App.register_changeset(
1454 Map.put(find_attrs, :scopes, scopes)
1461 def logout(conn, _) do
1464 |> redirect(to: "/")
1467 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1468 Logger.debug("Unimplemented, returning unmodified relationship")
1470 with %User{} = target <- User.get_cached_by_id(id) do
1472 |> put_view(AccountView)
1473 |> render("relationship.json", %{user: user, target: target})
1477 def empty_array(conn, _) do
1478 Logger.debug("Unimplemented, returning an empty array")
1482 def empty_object(conn, _) do
1483 Logger.debug("Unimplemented, returning an empty object")
1487 def get_filters(%{assigns: %{user: user}} = conn, _) do
1488 filters = Filter.get_filters(user)
1489 res = FilterView.render("filters.json", filters: filters)
1494 %{assigns: %{user: user}} = conn,
1495 %{"phrase" => phrase, "context" => context} = params
1501 hide: Map.get(params, "irreversible", false),
1502 whole_word: Map.get(params, "boolean", true)
1506 {:ok, response} = Filter.create(query)
1507 res = FilterView.render("filter.json", filter: response)
1511 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1512 filter = Filter.get(filter_id, user)
1513 res = FilterView.render("filter.json", filter: filter)
1518 %{assigns: %{user: user}} = conn,
1519 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1523 filter_id: filter_id,
1526 hide: Map.get(params, "irreversible", nil),
1527 whole_word: Map.get(params, "boolean", true)
1531 {:ok, response} = Filter.update(query)
1532 res = FilterView.render("filter.json", filter: response)
1536 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1539 filter_id: filter_id
1542 {:ok, _} = Filter.delete(query)
1546 def suggestions(%{assigns: %{user: user}} = conn, _) do
1547 suggestions = Config.get(:suggestions)
1549 if Keyword.get(suggestions, :enabled, false) do
1550 api = Keyword.get(suggestions, :third_party_engine, "")
1551 timeout = Keyword.get(suggestions, :timeout, 5000)
1552 limit = Keyword.get(suggestions, :limit, 23)
1554 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1556 user = user.nickname
1560 |> String.replace("{{host}}", host)
1561 |> String.replace("{{user}}", user)
1563 with {:ok, %{status: 200, body: body}} <-
1564 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1565 {:ok, data} <- Jason.decode(body) do
1568 |> Enum.slice(0, limit)
1571 |> Map.put("id", fetch_suggestion_id(x))
1572 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1573 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1579 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1586 defp fetch_suggestion_id(attrs) do
1587 case User.get_or_fetch(attrs["acct"]) do
1588 {:ok, %User{id: id}} -> id
1593 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1594 with %Activity{} = activity <- Activity.get_by_id(status_id),
1595 true <- Visibility.visible_for_user?(activity, user) do
1599 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1609 def reports(%{assigns: %{user: user}} = conn, params) do
1610 case CommonAPI.report(user, params) do
1613 |> put_view(ReportView)
1614 |> try_render("report.json", %{activity: activity})
1618 |> put_status(:bad_request)
1619 |> json(%{error: err})
1623 def account_register(
1624 %{assigns: %{app: app}} = conn,
1625 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1633 "captcha_answer_data",
1637 |> Map.put("nickname", nickname)
1638 |> Map.put("fullname", params["fullname"] || nickname)
1639 |> Map.put("bio", params["bio"] || "")
1640 |> Map.put("confirm", params["password"])
1642 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1643 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1645 token_type: "Bearer",
1646 access_token: token.token,
1648 created_at: Token.Utils.format_created_at(token)
1653 |> put_status(:bad_request)
1658 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1659 render_error(conn, :bad_request, "Missing parameters")
1662 def account_register(conn, _) do
1663 render_error(conn, :forbidden, "Invalid credentials")
1666 def conversations(%{assigns: %{user: user}} = conn, params) do
1667 participations = Participation.for_user_with_last_activity_id(user, params)
1670 Enum.map(participations, fn participation ->
1671 ConversationView.render("participation.json", %{participation: participation, for: user})
1675 |> add_link_headers(:conversations, participations)
1676 |> json(conversations)
1679 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1680 with %Participation{} = participation <-
1681 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1682 {:ok, participation} <- Participation.mark_as_read(participation) do
1683 participation_view =
1684 ConversationView.render("participation.json", %{participation: participation, for: user})
1687 |> json(participation_view)
1691 def password_reset(conn, params) do
1692 nickname_or_email = params["email"] || params["nickname"]
1694 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1696 |> put_status(:no_content)
1699 {:error, "unknown user"} ->
1700 send_resp(conn, :not_found, "")
1703 send_resp(conn, :bad_request, "")
1707 def account_confirmation_resend(conn, params) do
1708 nickname_or_email = params["email"] || params["nickname"]
1710 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1711 {:ok, _} <- User.try_send_confirmation_email(user) do
1713 |> json_response(:no_content, "")
1717 def try_render(conn, target, params)
1718 when is_binary(target) do
1719 case render(conn, target, params) do
1720 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1725 def try_render(conn, _, _) do
1726 render_error(conn, :not_implemented, "Can't display this activity")
1729 defp present?(nil), do: false
1730 defp present?(false), do: false
1731 defp present?(_), do: true