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.OAuthScopesPlug
23 alias Pleroma.Plugs.RateLimiter
25 alias Pleroma.ScheduledActivity
29 alias Pleroma.Web.ActivityPub.ActivityPub
30 alias Pleroma.Web.ActivityPub.Visibility
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.MastodonAPI.AccountView
33 alias Pleroma.Web.MastodonAPI.AppView
34 alias Pleroma.Web.MastodonAPI.ConversationView
35 alias Pleroma.Web.MastodonAPI.FilterView
36 alias Pleroma.Web.MastodonAPI.ListView
37 alias Pleroma.Web.MastodonAPI.MastodonAPI
38 alias Pleroma.Web.MastodonAPI.MastodonView
39 alias Pleroma.Web.MastodonAPI.NotificationView
40 alias Pleroma.Web.MastodonAPI.ReportView
41 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
42 alias Pleroma.Web.MastodonAPI.StatusView
43 alias Pleroma.Web.MediaProxy
44 alias Pleroma.Web.OAuth.App
45 alias Pleroma.Web.OAuth.Authorization
46 alias Pleroma.Web.OAuth.Scopes
47 alias Pleroma.Web.OAuth.Token
48 alias Pleroma.Web.TwitterAPI.TwitterAPI
50 alias Pleroma.Web.ControllerHelper
54 require Pleroma.Constants
58 %{scopes: ["follow", "read:blocks"]} when action in [:blocks, :domain_blocks]
63 %{scopes: ["follow", "write:blocks"]}
64 when action in [:block, :unblock, :block_domain, :unblock_domain]
67 plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :follow_requests)
71 %{scopes: ["follow", "write:follows"]}
77 :authorize_follow_request,
78 :reject_follow_request
82 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
83 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
87 %{scopes: ["write:mutes"]}
88 when action in [:mute_conversation, :unmute_conversation]
91 @rate_limited_relations_actions ~w(follow unfollow)a
93 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
94 post_status delete_status)a
98 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
99 when action in ~w(reblog_status unreblog_status)a
104 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
105 when action in ~w(fav_status unfav_status)a
110 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
113 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
114 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
115 plug(RateLimiter, :app_account_creation when action == :account_register)
116 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
117 plug(RateLimiter, :password_reset when action == :password_reset)
118 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
120 @local_mastodon_name "Mastodon-Local"
122 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
124 def create_app(conn, params) do
125 scopes = Scopes.fetch_scopes(params, ["read"])
129 |> Map.drop(["scope", "scopes"])
130 |> Map.put("scopes", scopes)
132 with cs <- App.register_changeset(%App{}, app_attrs),
133 false <- cs.changes[:client_name] == @local_mastodon_name,
134 {:ok, app} <- Repo.insert(cs) do
137 |> render("show.json", %{app: app})
146 value_function \\ fn x -> {:ok, x} end
148 if Map.has_key?(params, params_field) do
149 case value_function.(params[params_field]) do
150 {:ok, new_value} -> Map.put(map, map_field, new_value)
158 def update_credentials(%{assigns: %{user: user}} = conn, params) do
163 |> add_if_present(params, "display_name", :name)
164 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
165 |> add_if_present(params, "avatar", :avatar, fn value ->
166 with %Plug.Upload{} <- value,
167 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
174 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
178 |> Map.get(:emoji, [])
179 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
190 :skip_thread_containment
192 |> Enum.reduce(%{}, fn key, acc ->
193 add_if_present(acc, params, to_string(key), key, fn value ->
194 {:ok, ControllerHelper.truthy_param?(value)}
197 |> add_if_present(params, "default_scope", :default_scope)
198 |> add_if_present(params, "fields", :fields, fn fields ->
199 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
203 |> add_if_present(params, "fields", :raw_fields)
204 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
205 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
207 |> add_if_present(params, "header", :banner, fn value ->
208 with %Plug.Upload{} <- value,
209 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
215 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
216 with %Plug.Upload{} <- value,
217 {:ok, object} <- ActivityPub.upload(value, type: :background) do
223 |> Map.put(:emoji, user_info_emojis)
225 info_cng = User.Info.profile_update(user.info, info_params)
227 with changeset <- User.update_changeset(user, user_params),
228 changeset <- Changeset.put_embed(changeset, :info, info_cng),
229 {:ok, user} <- User.update_and_set_cache(changeset) do
230 if original_user != user do
231 CommonAPI.update(user)
236 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
239 _e -> render_error(conn, :forbidden, "Invalid request")
243 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
244 change = Changeset.change(user, %{avatar: nil})
245 {:ok, user} = User.update_and_set_cache(change)
246 CommonAPI.update(user)
248 json(conn, %{url: nil})
251 def update_avatar(%{assigns: %{user: user}} = conn, params) do
252 {:ok, object} = ActivityPub.upload(params, type: :avatar)
253 change = Changeset.change(user, %{avatar: object.data})
254 {:ok, user} = User.update_and_set_cache(change)
255 CommonAPI.update(user)
256 %{"url" => [%{"href" => href} | _]} = object.data
258 json(conn, %{url: href})
261 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
262 with new_info <- %{"banner" => %{}},
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 CommonAPI.update(user)
268 json(conn, %{url: nil})
272 def update_banner(%{assigns: %{user: user}} = conn, params) do
273 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
274 new_info <- %{"banner" => 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 CommonAPI.update(user)
279 %{"url" => [%{"href" => href} | _]} = object.data
281 json(conn, %{url: href})
285 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
286 with new_info <- %{"background" => %{}},
287 info_cng <- User.Info.profile_update(user.info, new_info),
288 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
289 {:ok, _user} <- User.update_and_set_cache(changeset) do
290 json(conn, %{url: nil})
294 def update_background(%{assigns: %{user: user}} = conn, params) do
295 with {:ok, object} <- ActivityPub.upload(params, type: :background),
296 new_info <- %{"background" => object.data},
297 info_cng <- User.Info.profile_update(user.info, new_info),
298 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
299 {:ok, _user} <- User.update_and_set_cache(changeset) do
300 %{"url" => [%{"href" => href} | _]} = object.data
302 json(conn, %{url: href})
306 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
307 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
310 AccountView.render("account.json", %{
313 with_pleroma_settings: true,
314 with_chat_token: chat_token
320 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
321 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
324 |> render("short.json", %{app: app})
328 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
329 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
330 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
331 account = AccountView.render("account.json", %{user: user, for: for_user})
334 _e -> render_error(conn, :not_found, "Can't find user")
338 @mastodon_api_level "2.7.2"
340 def masto_instance(conn, _params) do
341 instance = Config.get(:instance)
345 title: Keyword.get(instance, :name),
346 description: Keyword.get(instance, :description),
347 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
348 email: Keyword.get(instance, :email),
350 streaming_api: Pleroma.Web.Endpoint.websocket_url()
352 stats: Stats.get_stats(),
353 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
355 registrations: Pleroma.Config.get([:instance, :registrations_open]),
356 # Extra (not present in Mastodon):
357 max_toot_chars: Keyword.get(instance, :limit),
358 poll_limits: Keyword.get(instance, :poll_limits)
364 def peers(conn, _params) do
365 json(conn, Stats.get_peers())
368 defp mastodonized_emoji do
369 Pleroma.Emoji.get_all()
370 |> Enum.map(fn {shortcode, relative_url, tags} ->
371 url = to_string(URI.merge(Web.base_url(), relative_url))
374 "shortcode" => shortcode,
376 "visible_in_picker" => true,
379 # Assuming that a comma is authorized in the category name
380 "category" => (tags -- ["Custom"]) |> Enum.join(",")
385 def custom_emojis(conn, _params) do
386 mastodon_emoji = mastodonized_emoji()
387 json(conn, mastodon_emoji)
390 def home_timeline(%{assigns: %{user: user}} = conn, params) do
393 |> Map.put("type", ["Create", "Announce"])
394 |> Map.put("blocking_user", user)
395 |> Map.put("muting_user", user)
396 |> Map.put("user", user)
399 [user.ap_id | user.following]
400 |> ActivityPub.fetch_activities(params)
404 |> add_link_headers(:home_timeline, activities)
405 |> put_view(StatusView)
406 |> render("index.json", %{activities: activities, for: user, as: :activity})
409 def public_timeline(%{assigns: %{user: user}} = conn, params) do
410 local_only = params["local"] in [true, "True", "true", "1"]
414 |> Map.put("type", ["Create", "Announce"])
415 |> Map.put("local_only", local_only)
416 |> Map.put("blocking_user", user)
417 |> Map.put("muting_user", user)
418 |> Map.put("user", user)
419 |> ActivityPub.fetch_public_activities()
423 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
424 |> put_view(StatusView)
425 |> render("index.json", %{activities: activities, for: user, as: :activity})
428 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
429 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
432 |> Map.put("tag", params["tagged"])
434 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
437 |> add_link_headers(:user_statuses, activities, params["id"])
438 |> put_view(StatusView)
439 |> render("index.json", %{
440 activities: activities,
447 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
450 |> Map.put("type", "Create")
451 |> Map.put("blocking_user", user)
452 |> Map.put("user", user)
453 |> Map.put(:visibility, "direct")
457 |> ActivityPub.fetch_activities_query(params)
458 |> Pagination.fetch_paginated(params)
461 |> add_link_headers(:dm_timeline, activities)
462 |> put_view(StatusView)
463 |> render("index.json", %{activities: activities, for: user, as: :activity})
466 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
467 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
468 true <- Visibility.visible_for_user?(activity, user) do
470 |> put_view(StatusView)
471 |> try_render("status.json", %{activity: activity, for: user})
475 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
476 with %Activity{} = activity <- Activity.get_by_id(id),
478 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
479 "blocking_user" => user,
481 "exclude_id" => activity.id
483 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
489 activities: grouped_activities[true] || [],
493 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
498 activities: grouped_activities[false] || [],
502 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
509 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
510 with %Object{} = object <- Object.get_by_id(id),
511 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
512 true <- Visibility.visible_for_user?(activity, user) do
514 |> put_view(StatusView)
515 |> try_render("poll.json", %{object: object, for: user})
517 error when is_nil(error) or error == false ->
518 render_error(conn, :not_found, "Record not found")
522 defp get_cached_vote_or_vote(user, object, choices) do
523 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
526 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
527 case CommonAPI.vote(user, object, choices) do
528 {:error, _message} = res -> {:ignore, res}
529 res -> {:commit, res}
536 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
537 with %Object{} = object <- Object.get_by_id(id),
538 true <- object.data["type"] == "Question",
539 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
540 true <- Visibility.visible_for_user?(activity, user),
541 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
543 |> put_view(StatusView)
544 |> try_render("poll.json", %{object: object, for: user})
547 render_error(conn, :not_found, "Record not found")
550 render_error(conn, :not_found, "Record not found")
554 |> put_status(:unprocessable_entity)
555 |> json(%{error: message})
559 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
560 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
562 |> add_link_headers(:scheduled_statuses, scheduled_activities)
563 |> put_view(ScheduledActivityView)
564 |> render("index.json", %{scheduled_activities: scheduled_activities})
568 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
569 with %ScheduledActivity{} = scheduled_activity <-
570 ScheduledActivity.get(user, scheduled_activity_id) do
572 |> put_view(ScheduledActivityView)
573 |> render("show.json", %{scheduled_activity: scheduled_activity})
575 _ -> {:error, :not_found}
579 def update_scheduled_status(
580 %{assigns: %{user: user}} = conn,
581 %{"id" => scheduled_activity_id} = params
583 with %ScheduledActivity{} = scheduled_activity <-
584 ScheduledActivity.get(user, scheduled_activity_id),
585 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
587 |> put_view(ScheduledActivityView)
588 |> render("show.json", %{scheduled_activity: scheduled_activity})
590 nil -> {:error, :not_found}
595 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
596 with %ScheduledActivity{} = scheduled_activity <-
597 ScheduledActivity.get(user, scheduled_activity_id),
598 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
600 |> put_view(ScheduledActivityView)
601 |> render("show.json", %{scheduled_activity: scheduled_activity})
603 nil -> {:error, :not_found}
608 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
611 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
613 scheduled_at = params["scheduled_at"]
615 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
616 with {:ok, scheduled_activity} <-
617 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
619 |> put_view(ScheduledActivityView)
620 |> render("show.json", %{scheduled_activity: scheduled_activity})
623 params = Map.drop(params, ["scheduled_at"])
625 case CommonAPI.post(user, params) do
628 |> put_status(:unprocessable_entity)
629 |> json(%{error: message})
633 |> put_view(StatusView)
634 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
639 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
640 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
643 _e -> render_error(conn, :forbidden, "Can't delete this post")
647 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
648 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
649 %Activity{} = announce <- Activity.normalize(announce.data) do
651 |> put_view(StatusView)
652 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
656 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
657 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
658 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
665 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
666 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
667 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
669 |> put_view(StatusView)
670 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
674 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
675 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
676 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
678 |> put_view(StatusView)
679 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
683 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
684 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
686 |> put_view(StatusView)
687 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
691 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
692 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
694 |> put_view(StatusView)
695 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
699 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
700 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
701 %User{} = user <- User.get_cached_by_nickname(user.nickname),
702 true <- Visibility.visible_for_user?(activity, user),
703 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
705 |> put_view(StatusView)
706 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
710 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
711 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
712 %User{} = user <- User.get_cached_by_nickname(user.nickname),
713 true <- Visibility.visible_for_user?(activity, user),
714 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
716 |> put_view(StatusView)
717 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
721 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
722 activity = Activity.get_by_id(id)
724 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
726 |> put_view(StatusView)
727 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
731 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
732 activity = Activity.get_by_id(id)
734 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
736 |> put_view(StatusView)
737 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
741 def notifications(%{assigns: %{user: user}} = conn, params) do
742 notifications = MastodonAPI.get_notifications(user, params)
745 |> add_link_headers(:notifications, notifications)
746 |> put_view(NotificationView)
747 |> render("index.json", %{notifications: notifications, for: user})
750 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
751 with {:ok, notification} <- Notification.get(user, id) do
753 |> put_view(NotificationView)
754 |> render("show.json", %{notification: notification, for: user})
758 |> put_status(:forbidden)
759 |> json(%{"error" => reason})
763 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
764 Notification.clear(user)
768 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
769 with {:ok, _notif} <- Notification.dismiss(user, id) do
774 |> put_status(:forbidden)
775 |> json(%{"error" => reason})
779 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
780 Notification.destroy_multiple(user, ids)
784 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
786 q = from(u in User, where: u.id in ^id)
787 targets = Repo.all(q)
790 |> put_view(AccountView)
791 |> render("relationships.json", %{user: user, targets: targets})
794 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
795 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
797 def update_media(%{assigns: %{user: user}} = conn, data) do
798 with %Object{} = object <- Repo.get(Object, data["id"]),
799 true <- Object.authorize_mutation(object, user),
800 true <- is_binary(data["description"]),
801 description <- data["description"] do
802 new_data = %{object.data | "name" => description}
806 |> Object.change(%{data: new_data})
809 attachment_data = Map.put(new_data, "id", object.id)
812 |> put_view(StatusView)
813 |> render("attachment.json", %{attachment: attachment_data})
817 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
818 with {:ok, object} <-
821 actor: User.ap_id(user),
822 description: Map.get(data, "description")
824 attachment_data = Map.put(object.data, "id", object.id)
827 |> put_view(StatusView)
828 |> render("attachment.json", %{attachment: attachment_data})
832 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
833 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
834 %{} = attachment_data <- Map.put(object.data, "id", object.id),
835 %{type: type} = rendered <-
836 StatusView.render("attachment.json", %{attachment: attachment_data}) do
837 # Reject if not an image
838 if type == "image" do
840 # Save to the user's info
841 info_changeset = User.Info.mascot_update(user.info, rendered)
845 |> Changeset.change()
846 |> Changeset.put_embed(:info, info_changeset)
848 {:ok, _user} = User.update_and_set_cache(user_changeset)
853 render_error(conn, :unsupported_media_type, "mascots can only be images")
858 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
859 mascot = User.get_mascot(user)
865 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
866 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
867 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
868 q = from(u in User, where: u.ap_id in ^likes)
872 |> Enum.filter(&(not User.blocks?(user, &1)))
875 |> put_view(AccountView)
876 |> render("accounts.json", %{for: user, users: users, as: :user})
882 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
883 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
884 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
885 q = from(u in User, where: u.ap_id in ^announces)
889 |> Enum.filter(&(not User.blocks?(user, &1)))
892 |> put_view(AccountView)
893 |> render("accounts.json", %{for: user, users: users, as: :user})
899 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
900 local_only = params["local"] in [true, "True", "true", "1"]
903 [params["tag"], params["any"]]
907 |> Enum.map(&String.downcase(&1))
912 |> Enum.map(&String.downcase(&1))
917 |> Enum.map(&String.downcase(&1))
921 |> Map.put("type", "Create")
922 |> Map.put("local_only", local_only)
923 |> Map.put("blocking_user", user)
924 |> Map.put("muting_user", user)
925 |> Map.put("user", user)
926 |> Map.put("tag", tags)
927 |> Map.put("tag_all", tag_all)
928 |> Map.put("tag_reject", tag_reject)
929 |> ActivityPub.fetch_public_activities()
933 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
934 |> put_view(StatusView)
935 |> render("index.json", %{activities: activities, for: user, as: :activity})
938 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
939 with %User{} = user <- User.get_cached_by_id(id),
940 followers <- MastodonAPI.get_followers(user, params) do
943 for_user && user.id == for_user.id -> followers
944 user.info.hide_followers -> []
949 |> add_link_headers(:followers, followers, user)
950 |> put_view(AccountView)
951 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
955 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
956 with %User{} = user <- User.get_cached_by_id(id),
957 followers <- MastodonAPI.get_friends(user, params) do
960 for_user && user.id == for_user.id -> followers
961 user.info.hide_follows -> []
966 |> add_link_headers(:following, followers, user)
967 |> put_view(AccountView)
968 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
972 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
973 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
975 |> put_view(AccountView)
976 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
980 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
981 with %User{} = follower <- User.get_cached_by_id(id),
982 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
984 |> put_view(AccountView)
985 |> render("relationship.json", %{user: followed, target: follower})
989 |> put_status(:forbidden)
990 |> json(%{error: message})
994 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
995 with %User{} = follower <- User.get_cached_by_id(id),
996 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
998 |> put_view(AccountView)
999 |> render("relationship.json", %{user: followed, target: follower})
1001 {:error, message} ->
1003 |> put_status(:forbidden)
1004 |> json(%{error: message})
1008 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1009 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1010 {_, true} <- {:followed, follower.id != followed.id},
1011 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1013 |> put_view(AccountView)
1014 |> render("relationship.json", %{user: follower, target: followed})
1017 {:error, :not_found}
1019 {:error, message} ->
1021 |> put_status(:forbidden)
1022 |> json(%{error: message})
1026 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1027 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1028 {_, true} <- {:followed, follower.id != followed.id},
1029 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1031 |> put_view(AccountView)
1032 |> render("account.json", %{user: followed, for: follower})
1035 {:error, :not_found}
1037 {:error, message} ->
1039 |> put_status(:forbidden)
1040 |> json(%{error: message})
1044 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1045 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1046 {_, true} <- {:followed, follower.id != followed.id},
1047 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1049 |> put_view(AccountView)
1050 |> render("relationship.json", %{user: follower, target: followed})
1053 {:error, :not_found}
1060 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1062 if Map.has_key?(params, "notifications"),
1063 do: params["notifications"] in [true, "True", "true", "1"],
1066 with %User{} = muted <- User.get_cached_by_id(id),
1067 {:ok, muter} <- User.mute(muter, muted, notifications) do
1069 |> put_view(AccountView)
1070 |> render("relationship.json", %{user: muter, target: muted})
1072 {:error, message} ->
1074 |> put_status(:forbidden)
1075 |> json(%{error: message})
1079 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1080 with %User{} = muted <- User.get_cached_by_id(id),
1081 {:ok, muter} <- User.unmute(muter, muted) do
1083 |> put_view(AccountView)
1084 |> render("relationship.json", %{user: muter, target: muted})
1086 {:error, message} ->
1088 |> put_status(:forbidden)
1089 |> json(%{error: message})
1093 def mutes(%{assigns: %{user: user}} = conn, _) do
1094 with muted_accounts <- User.muted_users(user) do
1095 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1100 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1101 with %User{} = blocked <- User.get_cached_by_id(id),
1102 {:ok, blocker} <- User.block(blocker, blocked),
1103 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1105 |> put_view(AccountView)
1106 |> render("relationship.json", %{user: blocker, target: blocked})
1108 {:error, message} ->
1110 |> put_status(:forbidden)
1111 |> json(%{error: message})
1115 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1116 with %User{} = blocked <- User.get_cached_by_id(id),
1117 {:ok, blocker} <- User.unblock(blocker, blocked),
1118 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1120 |> put_view(AccountView)
1121 |> render("relationship.json", %{user: blocker, target: blocked})
1123 {:error, message} ->
1125 |> put_status(:forbidden)
1126 |> json(%{error: message})
1130 def blocks(%{assigns: %{user: user}} = conn, _) do
1131 with blocked_accounts <- User.blocked_users(user) do
1132 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1137 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1138 json(conn, info.domain_blocks || [])
1141 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1142 User.block_domain(blocker, domain)
1146 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1147 User.unblock_domain(blocker, domain)
1151 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1152 with %User{} = subscription_target <- User.get_cached_by_id(id),
1153 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1155 |> put_view(AccountView)
1156 |> render("relationship.json", %{user: user, target: subscription_target})
1158 {:error, message} ->
1160 |> put_status(:forbidden)
1161 |> json(%{error: message})
1165 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1166 with %User{} = subscription_target <- User.get_cached_by_id(id),
1167 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1169 |> put_view(AccountView)
1170 |> render("relationship.json", %{user: user, target: subscription_target})
1172 {:error, message} ->
1174 |> put_status(:forbidden)
1175 |> json(%{error: message})
1179 def favourites(%{assigns: %{user: user}} = conn, params) do
1182 |> Map.put("type", "Create")
1183 |> Map.put("favorited_by", user.ap_id)
1184 |> Map.put("blocking_user", user)
1187 ActivityPub.fetch_activities([], params)
1191 |> add_link_headers(:favourites, activities)
1192 |> put_view(StatusView)
1193 |> render("index.json", %{activities: activities, for: user, as: :activity})
1196 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1197 with %User{} = user <- User.get_by_id(id),
1198 false <- user.info.hide_favorites do
1201 |> Map.put("type", "Create")
1202 |> Map.put("favorited_by", user.ap_id)
1203 |> Map.put("blocking_user", for_user)
1207 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1209 [Pleroma.Constants.as_public()]
1214 |> ActivityPub.fetch_activities(params)
1218 |> add_link_headers(:favourites, activities)
1219 |> put_view(StatusView)
1220 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1222 nil -> {:error, :not_found}
1223 true -> render_error(conn, :forbidden, "Can't get favorites")
1227 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1228 user = User.get_cached_by_id(user.id)
1231 Bookmark.for_user_query(user.id)
1232 |> Pagination.fetch_paginated(params)
1236 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1239 |> add_link_headers(:bookmarks, bookmarks)
1240 |> put_view(StatusView)
1241 |> render("index.json", %{activities: activities, for: user, as: :activity})
1244 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1245 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1246 res = ListView.render("lists.json", lists: lists)
1250 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1251 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1254 |> Map.put("type", "Create")
1255 |> Map.put("blocking_user", user)
1256 |> Map.put("user", user)
1257 |> Map.put("muting_user", user)
1259 # we must filter the following list for the user to avoid leaking statuses the user
1260 # does not actually have permission to see (for more info, peruse security issue #270).
1263 |> Enum.filter(fn x -> x in user.following end)
1264 |> ActivityPub.fetch_activities_bounded(following, params)
1268 |> put_view(StatusView)
1269 |> render("index.json", %{activities: activities, for: user, as: :activity})
1271 _e -> render_error(conn, :forbidden, "Error.")
1275 def index(%{assigns: %{user: user}} = conn, _params) do
1276 token = get_session(conn, :oauth_token)
1279 mastodon_emoji = mastodonized_emoji()
1281 limit = Config.get([:instance, :limit])
1284 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1289 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1290 access_token: token,
1292 domain: Pleroma.Web.Endpoint.host(),
1295 unfollow_modal: false,
1298 auto_play_gif: false,
1299 display_sensitive_media: false,
1300 reduce_motion: false,
1301 max_toot_chars: limit,
1302 mascot: User.get_mascot(user)["url"]
1304 poll_limits: Config.get([:instance, :poll_limits]),
1306 delete_others_notice: present?(user.info.is_moderator),
1307 admin: present?(user.info.is_admin)
1311 default_privacy: user.info.default_scope,
1312 default_sensitive: false,
1313 allow_content_types: Config.get([:instance, :allowed_post_formats])
1315 media_attachments: %{
1316 accept_content_types: [
1332 user.info.settings ||
1362 push_subscription: nil,
1364 custom_emojis: mastodon_emoji,
1370 |> put_layout(false)
1371 |> put_view(MastodonView)
1372 |> render("index.html", %{initial_state: initial_state})
1375 |> put_session(:return_to, conn.request_path)
1376 |> redirect(to: "/web/login")
1380 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1381 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1383 with changeset <- Changeset.change(user),
1384 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1385 {:ok, _user} <- User.update_and_set_cache(changeset) do
1390 |> put_status(:internal_server_error)
1391 |> json(%{error: inspect(e)})
1395 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1396 redirect(conn, to: local_mastodon_root_path(conn))
1399 @doc "Local Mastodon FE login init action"
1400 def login(conn, %{"code" => auth_token}) do
1401 with {:ok, app} <- get_or_make_app(),
1402 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1403 {:ok, token} <- Token.exchange_token(app, auth) do
1405 |> put_session(:oauth_token, token.token)
1406 |> redirect(to: local_mastodon_root_path(conn))
1410 @doc "Local Mastodon FE callback action"
1411 def login(conn, _) do
1412 with {:ok, app} <- get_or_make_app() do
1417 response_type: "code",
1418 client_id: app.client_id,
1420 scope: Enum.join(app.scopes, " ")
1423 redirect(conn, to: path)
1427 defp local_mastodon_root_path(conn) do
1428 case get_session(conn, :return_to) do
1430 mastodon_api_path(conn, :index, ["getting-started"])
1433 delete_session(conn, :return_to)
1438 defp get_or_make_app do
1439 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1440 scopes = ["read", "write", "follow", "push"]
1442 with %App{} = app <- Repo.get_by(App, find_attrs) do
1444 if app.scopes == scopes do
1448 |> Changeset.change(%{scopes: scopes})
1456 App.register_changeset(
1458 Map.put(find_attrs, :scopes, scopes)
1465 def logout(conn, _) do
1468 |> redirect(to: "/")
1471 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1472 Logger.debug("Unimplemented, returning unmodified relationship")
1474 with %User{} = target <- User.get_cached_by_id(id) do
1476 |> put_view(AccountView)
1477 |> render("relationship.json", %{user: user, target: target})
1481 def empty_array(conn, _) do
1482 Logger.debug("Unimplemented, returning an empty array")
1486 def empty_object(conn, _) do
1487 Logger.debug("Unimplemented, returning an empty object")
1491 def get_filters(%{assigns: %{user: user}} = conn, _) do
1492 filters = Filter.get_filters(user)
1493 res = FilterView.render("filters.json", filters: filters)
1498 %{assigns: %{user: user}} = conn,
1499 %{"phrase" => phrase, "context" => context} = params
1505 hide: Map.get(params, "irreversible", false),
1506 whole_word: Map.get(params, "boolean", true)
1510 {:ok, response} = Filter.create(query)
1511 res = FilterView.render("filter.json", filter: response)
1515 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1516 filter = Filter.get(filter_id, user)
1517 res = FilterView.render("filter.json", filter: filter)
1522 %{assigns: %{user: user}} = conn,
1523 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1527 filter_id: filter_id,
1530 hide: Map.get(params, "irreversible", nil),
1531 whole_word: Map.get(params, "boolean", true)
1535 {:ok, response} = Filter.update(query)
1536 res = FilterView.render("filter.json", filter: response)
1540 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1543 filter_id: filter_id
1546 {:ok, _} = Filter.delete(query)
1550 def suggestions(%{assigns: %{user: user}} = conn, _) do
1551 suggestions = Config.get(:suggestions)
1553 if Keyword.get(suggestions, :enabled, false) do
1554 api = Keyword.get(suggestions, :third_party_engine, "")
1555 timeout = Keyword.get(suggestions, :timeout, 5000)
1556 limit = Keyword.get(suggestions, :limit, 23)
1558 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1560 user = user.nickname
1564 |> String.replace("{{host}}", host)
1565 |> String.replace("{{user}}", user)
1567 with {:ok, %{status: 200, body: body}} <-
1568 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1569 {:ok, data} <- Jason.decode(body) do
1572 |> Enum.slice(0, limit)
1575 |> Map.put("id", fetch_suggestion_id(x))
1576 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1577 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1583 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1590 defp fetch_suggestion_id(attrs) do
1591 case User.get_or_fetch(attrs["acct"]) do
1592 {:ok, %User{id: id}} -> id
1597 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1598 with %Activity{} = activity <- Activity.get_by_id(status_id),
1599 true <- Visibility.visible_for_user?(activity, user) do
1603 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1613 def reports(%{assigns: %{user: user}} = conn, params) do
1614 case CommonAPI.report(user, params) do
1617 |> put_view(ReportView)
1618 |> try_render("report.json", %{activity: activity})
1622 |> put_status(:bad_request)
1623 |> json(%{error: err})
1627 def account_register(
1628 %{assigns: %{app: app}} = conn,
1629 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1637 "captcha_answer_data",
1641 |> Map.put("nickname", nickname)
1642 |> Map.put("fullname", params["fullname"] || nickname)
1643 |> Map.put("bio", params["bio"] || "")
1644 |> Map.put("confirm", params["password"])
1646 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1647 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1649 token_type: "Bearer",
1650 access_token: token.token,
1652 created_at: Token.Utils.format_created_at(token)
1657 |> put_status(:bad_request)
1662 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1663 render_error(conn, :bad_request, "Missing parameters")
1666 def account_register(conn, _) do
1667 render_error(conn, :forbidden, "Invalid credentials")
1670 def conversations(%{assigns: %{user: user}} = conn, params) do
1671 participations = Participation.for_user_with_last_activity_id(user, params)
1674 Enum.map(participations, fn participation ->
1675 ConversationView.render("participation.json", %{participation: participation, for: user})
1679 |> add_link_headers(:conversations, participations)
1680 |> json(conversations)
1683 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1684 with %Participation{} = participation <-
1685 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1686 {:ok, participation} <- Participation.mark_as_read(participation) do
1687 participation_view =
1688 ConversationView.render("participation.json", %{participation: participation, for: user})
1691 |> json(participation_view)
1695 def password_reset(conn, params) do
1696 nickname_or_email = params["email"] || params["nickname"]
1698 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1700 |> put_status(:no_content)
1703 {:error, "unknown user"} ->
1704 send_resp(conn, :not_found, "")
1707 send_resp(conn, :bad_request, "")
1711 def account_confirmation_resend(conn, params) do
1712 nickname_or_email = params["email"] || params["nickname"]
1714 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1715 {:ok, _} <- User.try_send_confirmation_email(user) do
1717 |> json_response(:no_content, "")
1721 def try_render(conn, target, params)
1722 when is_binary(target) do
1723 case render(conn, target, params) do
1724 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1729 def try_render(conn, _, _) do
1730 render_error(conn, :not_implemented, "Can't display this activity")
1733 defp present?(nil), do: false
1734 defp present?(false), do: false
1735 defp present?(_), do: true