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 defp normalize_fields_attributes(fields) do
123 if Enum.all?(fields, &is_tuple/1) do
124 Enum.map(fields, fn {_, v} -> v end)
130 def update_credentials(%{assigns: %{user: user}} = conn, params) do
135 |> add_if_present(params, "display_name", :name)
136 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
137 |> add_if_present(params, "avatar", :avatar, fn value ->
138 with %Plug.Upload{} <- value,
139 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
146 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
150 |> Map.get(:emoji, [])
151 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
155 if Map.has_key?(params, "fields_attributes") do
156 Map.update!(params, "fields_attributes", fn fields ->
158 |> normalize_fields_attributes()
159 |> Enum.filter(fn %{"name" => n} -> n != "" end)
173 :skip_thread_containment
175 |> Enum.reduce(%{}, fn key, acc ->
176 add_if_present(acc, params, to_string(key), key, fn value ->
177 {:ok, ControllerHelper.truthy_param?(value)}
180 |> add_if_present(params, "default_scope", :default_scope)
181 |> add_if_present(params, "fields_attributes", :fields, fn fields ->
182 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
186 |> add_if_present(params, "fields_attributes", :raw_fields)
187 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
188 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
190 |> add_if_present(params, "header", :banner, fn value ->
191 with %Plug.Upload{} <- value,
192 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
198 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
199 with %Plug.Upload{} <- value,
200 {:ok, object} <- ActivityPub.upload(value, type: :background) do
206 |> Map.put(:emoji, user_info_emojis)
208 info_cng = User.Info.profile_update(user.info, info_params)
210 with changeset <- User.update_changeset(user, user_params),
211 changeset <- Changeset.put_embed(changeset, :info, info_cng),
212 {:ok, user} <- User.update_and_set_cache(changeset) do
213 if original_user != user do
214 CommonAPI.update(user)
219 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
222 _e -> render_error(conn, :forbidden, "Invalid request")
226 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
227 change = Changeset.change(user, %{avatar: nil})
228 {:ok, user} = User.update_and_set_cache(change)
229 CommonAPI.update(user)
231 json(conn, %{url: nil})
234 def update_avatar(%{assigns: %{user: user}} = conn, params) do
235 {:ok, object} = ActivityPub.upload(params, type: :avatar)
236 change = Changeset.change(user, %{avatar: object.data})
237 {:ok, user} = User.update_and_set_cache(change)
238 CommonAPI.update(user)
239 %{"url" => [%{"href" => href} | _]} = object.data
241 json(conn, %{url: href})
244 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
245 with new_info <- %{"banner" => %{}},
246 info_cng <- User.Info.profile_update(user.info, new_info),
247 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
248 {:ok, user} <- User.update_and_set_cache(changeset) do
249 CommonAPI.update(user)
251 json(conn, %{url: nil})
255 def update_banner(%{assigns: %{user: user}} = conn, params) do
256 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
257 new_info <- %{"banner" => object.data},
258 info_cng <- User.Info.profile_update(user.info, new_info),
259 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
260 {:ok, user} <- User.update_and_set_cache(changeset) do
261 CommonAPI.update(user)
262 %{"url" => [%{"href" => href} | _]} = object.data
264 json(conn, %{url: href})
268 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
269 with new_info <- %{"background" => %{}},
270 info_cng <- User.Info.profile_update(user.info, new_info),
271 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
272 {:ok, _user} <- User.update_and_set_cache(changeset) do
273 json(conn, %{url: nil})
277 def update_background(%{assigns: %{user: user}} = conn, params) do
278 with {:ok, object} <- ActivityPub.upload(params, type: :background),
279 new_info <- %{"background" => object.data},
280 info_cng <- User.Info.profile_update(user.info, new_info),
281 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
282 {:ok, _user} <- User.update_and_set_cache(changeset) do
283 %{"url" => [%{"href" => href} | _]} = object.data
285 json(conn, %{url: href})
289 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
290 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
293 AccountView.render("account.json", %{
296 with_pleroma_settings: true,
297 with_chat_token: chat_token
303 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
304 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
307 |> render("short.json", %{app: app})
311 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
312 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
313 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
314 account = AccountView.render("account.json", %{user: user, for: for_user})
317 _e -> render_error(conn, :not_found, "Can't find user")
321 @mastodon_api_level "2.7.2"
323 def masto_instance(conn, _params) do
324 instance = Config.get(:instance)
328 title: Keyword.get(instance, :name),
329 description: Keyword.get(instance, :description),
330 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
331 email: Keyword.get(instance, :email),
333 streaming_api: Pleroma.Web.Endpoint.websocket_url()
335 stats: Stats.get_stats(),
336 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
338 registrations: Pleroma.Config.get([:instance, :registrations_open]),
339 # Extra (not present in Mastodon):
340 max_toot_chars: Keyword.get(instance, :limit),
341 poll_limits: Keyword.get(instance, :poll_limits)
347 def peers(conn, _params) do
348 json(conn, Stats.get_peers())
351 defp mastodonized_emoji do
352 Pleroma.Emoji.get_all()
353 |> Enum.map(fn {shortcode, relative_url, tags} ->
354 url = to_string(URI.merge(Web.base_url(), relative_url))
357 "shortcode" => shortcode,
359 "visible_in_picker" => true,
362 # Assuming that a comma is authorized in the category name
363 "category" => (tags -- ["Custom"]) |> Enum.join(",")
368 def custom_emojis(conn, _params) do
369 mastodon_emoji = mastodonized_emoji()
370 json(conn, mastodon_emoji)
373 def home_timeline(%{assigns: %{user: user}} = conn, params) do
376 |> Map.put("type", ["Create", "Announce"])
377 |> Map.put("blocking_user", user)
378 |> Map.put("muting_user", user)
379 |> Map.put("user", user)
382 [user.ap_id | user.following]
383 |> ActivityPub.fetch_activities(params)
387 |> add_link_headers(:home_timeline, activities)
388 |> put_view(StatusView)
389 |> render("index.json", %{activities: activities, for: user, as: :activity})
392 def public_timeline(%{assigns: %{user: user}} = conn, params) do
393 local_only = params["local"] in [true, "True", "true", "1"]
397 |> Map.put("type", ["Create", "Announce"])
398 |> Map.put("local_only", local_only)
399 |> Map.put("blocking_user", user)
400 |> Map.put("muting_user", user)
401 |> ActivityPub.fetch_public_activities()
405 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
406 |> put_view(StatusView)
407 |> render("index.json", %{activities: activities, for: user, as: :activity})
410 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
411 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
414 |> Map.put("tag", params["tagged"])
416 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
419 |> add_link_headers(:user_statuses, activities, params["id"])
420 |> put_view(StatusView)
421 |> render("index.json", %{
422 activities: activities,
429 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
432 |> Map.put("type", "Create")
433 |> Map.put("blocking_user", user)
434 |> Map.put("user", user)
435 |> Map.put(:visibility, "direct")
439 |> ActivityPub.fetch_activities_query(params)
440 |> Pagination.fetch_paginated(params)
443 |> add_link_headers(:dm_timeline, activities)
444 |> put_view(StatusView)
445 |> render("index.json", %{activities: activities, for: user, as: :activity})
448 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
454 |> Activity.all_by_ids_with_object()
455 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
458 |> put_view(StatusView)
459 |> render("index.json", activities: activities, for: user, as: :activity)
462 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
463 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
464 true <- Visibility.visible_for_user?(activity, user) do
466 |> put_view(StatusView)
467 |> try_render("status.json", %{activity: activity, for: user})
471 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
472 with %Activity{} = activity <- Activity.get_by_id(id),
474 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
475 "blocking_user" => user,
477 "exclude_id" => activity.id
479 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
485 activities: grouped_activities[true] || [],
489 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
494 activities: grouped_activities[false] || [],
498 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
505 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
506 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
507 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
508 true <- Visibility.visible_for_user?(activity, user) do
510 |> put_view(StatusView)
511 |> try_render("poll.json", %{object: object, for: user})
513 error when is_nil(error) or error == false ->
514 render_error(conn, :not_found, "Record not found")
518 defp get_cached_vote_or_vote(user, object, choices) do
519 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
522 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
523 case CommonAPI.vote(user, object, choices) do
524 {:error, _message} = res -> {:ignore, res}
525 res -> {:commit, res}
532 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
533 with %Object{} = object <- Object.get_by_id(id),
534 true <- object.data["type"] == "Question",
535 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
536 true <- Visibility.visible_for_user?(activity, user),
537 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
539 |> put_view(StatusView)
540 |> try_render("poll.json", %{object: object, for: user})
543 render_error(conn, :not_found, "Record not found")
546 render_error(conn, :not_found, "Record not found")
550 |> put_status(:unprocessable_entity)
551 |> json(%{error: message})
555 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
556 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
558 |> add_link_headers(:scheduled_statuses, scheduled_activities)
559 |> put_view(ScheduledActivityView)
560 |> render("index.json", %{scheduled_activities: scheduled_activities})
564 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
565 with %ScheduledActivity{} = scheduled_activity <-
566 ScheduledActivity.get(user, scheduled_activity_id) do
568 |> put_view(ScheduledActivityView)
569 |> render("show.json", %{scheduled_activity: scheduled_activity})
571 _ -> {:error, :not_found}
575 def update_scheduled_status(
576 %{assigns: %{user: user}} = conn,
577 %{"id" => scheduled_activity_id} = params
579 with %ScheduledActivity{} = scheduled_activity <-
580 ScheduledActivity.get(user, scheduled_activity_id),
581 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
583 |> put_view(ScheduledActivityView)
584 |> render("show.json", %{scheduled_activity: scheduled_activity})
586 nil -> {:error, :not_found}
591 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
592 with %ScheduledActivity{} = scheduled_activity <-
593 ScheduledActivity.get(user, scheduled_activity_id),
594 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
596 |> put_view(ScheduledActivityView)
597 |> render("show.json", %{scheduled_activity: scheduled_activity})
599 nil -> {:error, :not_found}
604 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
607 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
609 scheduled_at = params["scheduled_at"]
611 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
612 with {:ok, scheduled_activity} <-
613 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
615 |> put_view(ScheduledActivityView)
616 |> render("show.json", %{scheduled_activity: scheduled_activity})
619 params = Map.drop(params, ["scheduled_at"])
621 case CommonAPI.post(user, params) do
624 |> put_status(:unprocessable_entity)
625 |> json(%{error: message})
629 |> put_view(StatusView)
630 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
635 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
636 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
639 _e -> render_error(conn, :forbidden, "Can't delete this post")
643 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
644 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
645 %Activity{} = announce <- Activity.normalize(announce.data) do
647 |> put_view(StatusView)
648 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
652 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
653 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
654 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
656 |> put_view(StatusView)
657 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
661 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
662 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
663 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
665 |> put_view(StatusView)
666 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
670 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
671 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
672 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
674 |> put_view(StatusView)
675 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
679 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
680 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
682 |> put_view(StatusView)
683 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
687 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
688 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
695 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
697 %User{} = user <- User.get_cached_by_nickname(user.nickname),
698 true <- Visibility.visible_for_user?(activity, user),
699 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
701 |> put_view(StatusView)
702 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
706 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
707 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
708 %User{} = user <- User.get_cached_by_nickname(user.nickname),
709 true <- Visibility.visible_for_user?(activity, user),
710 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
712 |> put_view(StatusView)
713 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
717 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
718 activity = Activity.get_by_id(id)
720 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
722 |> put_view(StatusView)
723 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
727 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
728 activity = Activity.get_by_id(id)
730 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
732 |> put_view(StatusView)
733 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
737 def notifications(%{assigns: %{user: user}} = conn, params) do
738 notifications = MastodonAPI.get_notifications(user, params)
741 |> add_link_headers(:notifications, notifications)
742 |> put_view(NotificationView)
743 |> render("index.json", %{notifications: notifications, for: user})
746 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
747 with {:ok, notification} <- Notification.get(user, id) do
749 |> put_view(NotificationView)
750 |> render("show.json", %{notification: notification, for: user})
754 |> put_status(:forbidden)
755 |> json(%{"error" => reason})
759 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
760 Notification.clear(user)
764 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
765 with {:ok, _notif} <- Notification.dismiss(user, id) do
770 |> put_status(:forbidden)
771 |> json(%{"error" => reason})
775 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
776 Notification.destroy_multiple(user, ids)
780 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
782 q = from(u in User, where: u.id in ^id)
783 targets = Repo.all(q)
786 |> put_view(AccountView)
787 |> render("relationships.json", %{user: user, targets: targets})
790 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
791 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
793 def update_media(%{assigns: %{user: user}} = conn, data) do
794 with %Object{} = object <- Repo.get(Object, data["id"]),
795 true <- Object.authorize_mutation(object, user),
796 true <- is_binary(data["description"]),
797 description <- data["description"] do
798 new_data = %{object.data | "name" => description}
802 |> Object.change(%{data: new_data})
805 attachment_data = Map.put(new_data, "id", object.id)
808 |> put_view(StatusView)
809 |> render("attachment.json", %{attachment: attachment_data})
813 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
814 with {:ok, object} <-
817 actor: User.ap_id(user),
818 description: Map.get(data, "description")
820 attachment_data = Map.put(object.data, "id", object.id)
823 |> put_view(StatusView)
824 |> render("attachment.json", %{attachment: attachment_data})
828 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
829 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
830 %{} = attachment_data <- Map.put(object.data, "id", object.id),
831 %{type: type} = rendered <-
832 StatusView.render("attachment.json", %{attachment: attachment_data}) do
833 # Reject if not an image
834 if type == "image" do
836 # Save to the user's info
837 info_changeset = User.Info.mascot_update(user.info, rendered)
841 |> Changeset.change()
842 |> Changeset.put_embed(:info, info_changeset)
844 {:ok, _user} = User.update_and_set_cache(user_changeset)
849 render_error(conn, :unsupported_media_type, "mascots can only be images")
854 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
855 mascot = User.get_mascot(user)
861 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
862 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
863 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
864 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
865 q = from(u in User, where: u.ap_id in ^likes)
869 |> Enum.filter(&(not User.blocks?(user, &1)))
872 |> put_view(AccountView)
873 |> render("accounts.json", %{for: user, users: users, as: :user})
875 {:visible, false} -> {:error, :not_found}
880 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
881 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
882 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
883 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
884 q = from(u in User, where: u.ap_id in ^announces)
888 |> Enum.filter(&(not User.blocks?(user, &1)))
891 |> put_view(AccountView)
892 |> render("accounts.json", %{for: user, users: users, as: :user})
894 {:visible, false} -> {:error, :not_found}
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 ConversationView.safe_render_many(participations, ConversationView, "participation.json", %{
1680 |> add_link_headers(:conversations, participations)
1681 |> json(conversations)
1684 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1685 with %Participation{} = participation <-
1686 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1687 {:ok, participation} <- Participation.mark_as_read(participation) do
1688 participation_view =
1689 ConversationView.render("participation.json", %{participation: participation, for: user})
1692 |> json(participation_view)
1696 def password_reset(conn, params) do
1697 nickname_or_email = params["email"] || params["nickname"]
1699 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1701 |> put_status(:no_content)
1704 {:error, "unknown user"} ->
1705 send_resp(conn, :not_found, "")
1708 send_resp(conn, :bad_request, "")
1712 def account_confirmation_resend(conn, params) do
1713 nickname_or_email = params["email"] || params["nickname"]
1715 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1716 {:ok, _} <- User.try_send_confirmation_email(user) do
1718 |> json_response(:no_content, "")
1722 def try_render(conn, target, params)
1723 when is_binary(target) do
1724 case render(conn, target, params) do
1725 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1730 def try_render(conn, _, _) do
1731 render_error(conn, :not_implemented, "Can't display this activity")
1734 defp present?(nil), do: false
1735 defp present?(false), do: false
1736 defp present?(_), do: true