1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
20 alias Pleroma.Pagination
21 alias Pleroma.Plugs.RateLimiter
23 alias Pleroma.ScheduledActivity
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Visibility
29 alias Pleroma.Web.CommonAPI
30 alias Pleroma.Web.MastodonAPI.AccountView
31 alias Pleroma.Web.MastodonAPI.AppView
32 alias Pleroma.Web.MastodonAPI.ConversationView
33 alias Pleroma.Web.MastodonAPI.FilterView
34 alias Pleroma.Web.MastodonAPI.ListView
35 alias Pleroma.Web.MastodonAPI.MastodonAPI
36 alias Pleroma.Web.MastodonAPI.MastodonView
37 alias Pleroma.Web.MastodonAPI.ReportView
38 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
39 alias Pleroma.Web.MastodonAPI.StatusView
40 alias Pleroma.Web.MediaProxy
41 alias Pleroma.Web.OAuth.App
42 alias Pleroma.Web.OAuth.Authorization
43 alias Pleroma.Web.OAuth.Scopes
44 alias Pleroma.Web.OAuth.Token
45 alias Pleroma.Web.TwitterAPI.TwitterAPI
47 alias Pleroma.Web.ControllerHelper
51 require Pleroma.Constants
53 @rate_limited_relations_actions ~w(follow unfollow)a
55 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
56 post_status delete_status)a
60 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
61 when action in ~w(reblog_status unreblog_status)a
66 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
67 when action in ~w(fav_status unfav_status)a
72 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
75 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
76 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
77 plug(RateLimiter, :app_account_creation when action == :account_register)
78 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
79 plug(RateLimiter, :password_reset when action == :password_reset)
80 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
82 @local_mastodon_name "Mastodon-Local"
84 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
86 def create_app(conn, params) do
87 scopes = Scopes.fetch_scopes(params, ["read"])
91 |> Map.drop(["scope", "scopes"])
92 |> Map.put("scopes", scopes)
94 with cs <- App.register_changeset(%App{}, app_attrs),
95 false <- cs.changes[:client_name] == @local_mastodon_name,
96 {:ok, app} <- Repo.insert(cs) do
99 |> render("show.json", %{app: app})
108 value_function \\ fn x -> {:ok, x} end
110 if Map.has_key?(params, params_field) do
111 case value_function.(params[params_field]) do
112 {:ok, new_value} -> Map.put(map, map_field, new_value)
120 def update_credentials(%{assigns: %{user: user}} = conn, params) do
125 |> add_if_present(params, "display_name", :name)
126 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
127 |> add_if_present(params, "avatar", :avatar, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
136 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
140 |> Map.get(:emoji, [])
141 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
148 :hide_followers_count,
154 :skip_thread_containment,
157 |> Enum.reduce(%{}, fn key, acc ->
158 add_if_present(acc, params, to_string(key), key, fn value ->
159 {:ok, ControllerHelper.truthy_param?(value)}
162 |> add_if_present(params, "default_scope", :default_scope)
163 |> add_if_present(params, "fields", :fields, fn fields ->
164 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
168 |> add_if_present(params, "fields", :raw_fields)
169 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
170 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
172 |> add_if_present(params, "header", :banner, fn value ->
173 with %Plug.Upload{} <- value,
174 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
180 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
181 with %Plug.Upload{} <- value,
182 {:ok, object} <- ActivityPub.upload(value, type: :background) do
188 |> Map.put(:emoji, user_info_emojis)
192 |> User.update_changeset(user_params)
193 |> User.change_info(&User.Info.profile_update(&1, info_params))
195 with {:ok, user} <- User.update_and_set_cache(changeset) do
196 if original_user != user, do: CommonAPI.update(user)
200 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
203 _e -> render_error(conn, :forbidden, "Invalid request")
207 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
208 change = Changeset.change(user, %{avatar: nil})
209 {:ok, user} = User.update_and_set_cache(change)
210 CommonAPI.update(user)
212 json(conn, %{url: nil})
215 def update_avatar(%{assigns: %{user: user}} = conn, params) do
216 {:ok, object} = ActivityPub.upload(params, type: :avatar)
217 change = Changeset.change(user, %{avatar: object.data})
218 {:ok, user} = User.update_and_set_cache(change)
219 CommonAPI.update(user)
220 %{"url" => [%{"href" => href} | _]} = object.data
222 json(conn, %{url: href})
225 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
226 new_info = %{"banner" => %{}}
228 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
229 CommonAPI.update(user)
230 json(conn, %{url: nil})
234 def update_banner(%{assigns: %{user: user}} = conn, params) do
235 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
236 new_info <- %{"banner" => object.data},
237 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
238 CommonAPI.update(user)
239 %{"url" => [%{"href" => href} | _]} = object.data
241 json(conn, %{url: href})
245 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
246 new_info = %{"background" => %{}}
248 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
249 json(conn, %{url: nil})
253 def update_background(%{assigns: %{user: user}} = conn, params) do
254 with {:ok, object} <- ActivityPub.upload(params, type: :background),
255 new_info <- %{"background" => object.data},
256 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
257 %{"url" => [%{"href" => href} | _]} = object.data
259 json(conn, %{url: href})
263 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
264 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
267 AccountView.render("account.json", %{
270 with_pleroma_settings: true,
271 with_chat_token: chat_token
277 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
278 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
281 |> render("short.json", %{app: app})
285 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
286 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
287 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
288 account = AccountView.render("account.json", %{user: user, for: for_user})
291 _e -> render_error(conn, :not_found, "Can't find user")
295 @mastodon_api_level "2.7.2"
297 def masto_instance(conn, _params) do
298 instance = Config.get(:instance)
302 title: Keyword.get(instance, :name),
303 description: Keyword.get(instance, :description),
304 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
305 email: Keyword.get(instance, :email),
307 streaming_api: Pleroma.Web.Endpoint.websocket_url()
309 stats: Stats.get_stats(),
310 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
312 registrations: Pleroma.Config.get([:instance, :registrations_open]),
313 # Extra (not present in Mastodon):
314 max_toot_chars: Keyword.get(instance, :limit),
315 poll_limits: Keyword.get(instance, :poll_limits)
321 def peers(conn, _params) do
322 json(conn, Stats.get_peers())
325 defp mastodonized_emoji do
326 Pleroma.Emoji.get_all()
327 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
328 url = to_string(URI.merge(Web.base_url(), relative_url))
331 "shortcode" => shortcode,
333 "visible_in_picker" => true,
336 # Assuming that a comma is authorized in the category name
337 "category" => (tags -- ["Custom"]) |> Enum.join(",")
342 def custom_emojis(conn, _params) do
343 mastodon_emoji = mastodonized_emoji()
344 json(conn, mastodon_emoji)
347 def home_timeline(%{assigns: %{user: user}} = conn, params) do
350 |> Map.put("type", ["Create", "Announce"])
351 |> Map.put("blocking_user", user)
352 |> Map.put("muting_user", user)
353 |> Map.put("user", user)
356 [user.ap_id | user.following]
357 |> ActivityPub.fetch_activities(params)
361 |> add_link_headers(activities)
362 |> put_view(StatusView)
363 |> render("index.json", %{activities: activities, for: user, as: :activity})
366 def public_timeline(%{assigns: %{user: user}} = conn, params) do
367 local_only = params["local"] in [true, "True", "true", "1"]
371 |> Map.put("type", ["Create", "Announce"])
372 |> Map.put("local_only", local_only)
373 |> Map.put("blocking_user", user)
374 |> Map.put("muting_user", user)
375 |> ActivityPub.fetch_public_activities()
379 |> add_link_headers(activities, %{"local" => local_only})
380 |> put_view(StatusView)
381 |> render("index.json", %{activities: activities, for: user, as: :activity})
384 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
385 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
388 |> Map.put("tag", params["tagged"])
390 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
393 |> add_link_headers(activities)
394 |> put_view(StatusView)
395 |> render("index.json", %{
396 activities: activities,
403 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
406 |> Map.put("type", "Create")
407 |> Map.put("blocking_user", user)
408 |> Map.put("user", user)
409 |> Map.put(:visibility, "direct")
413 |> ActivityPub.fetch_activities_query(params)
414 |> Pagination.fetch_paginated(params)
417 |> add_link_headers(activities)
418 |> put_view(StatusView)
419 |> render("index.json", %{activities: activities, for: user, as: :activity})
422 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
428 |> Activity.all_by_ids_with_object()
429 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
432 |> put_view(StatusView)
433 |> render("index.json", activities: activities, for: user, as: :activity)
436 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
437 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
438 true <- Visibility.visible_for_user?(activity, user) do
440 |> put_view(StatusView)
441 |> try_render("status.json", %{activity: activity, for: user})
445 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
446 with %Activity{} = activity <- Activity.get_by_id(id),
448 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
449 "blocking_user" => user,
451 "exclude_id" => activity.id
453 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
459 activities: grouped_activities[true] || [],
463 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
468 activities: grouped_activities[false] || [],
472 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
479 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
480 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
481 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
482 true <- Visibility.visible_for_user?(activity, user) do
484 |> put_view(StatusView)
485 |> try_render("poll.json", %{object: object, for: user})
487 error when is_nil(error) or error == false ->
488 render_error(conn, :not_found, "Record not found")
492 defp get_cached_vote_or_vote(user, object, choices) do
493 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
496 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
497 case CommonAPI.vote(user, object, choices) do
498 {:error, _message} = res -> {:ignore, res}
499 res -> {:commit, res}
506 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
507 with %Object{} = object <- Object.get_by_id(id),
508 true <- object.data["type"] == "Question",
509 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
510 true <- Visibility.visible_for_user?(activity, user),
511 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
513 |> put_view(StatusView)
514 |> try_render("poll.json", %{object: object, for: user})
517 render_error(conn, :not_found, "Record not found")
520 render_error(conn, :not_found, "Record not found")
524 |> put_status(:unprocessable_entity)
525 |> json(%{error: message})
529 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
530 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
532 |> add_link_headers(scheduled_activities)
533 |> put_view(ScheduledActivityView)
534 |> render("index.json", %{scheduled_activities: scheduled_activities})
538 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
539 with %ScheduledActivity{} = scheduled_activity <-
540 ScheduledActivity.get(user, scheduled_activity_id) do
542 |> put_view(ScheduledActivityView)
543 |> render("show.json", %{scheduled_activity: scheduled_activity})
545 _ -> {:error, :not_found}
549 def update_scheduled_status(
550 %{assigns: %{user: user}} = conn,
551 %{"id" => scheduled_activity_id} = params
553 with %ScheduledActivity{} = scheduled_activity <-
554 ScheduledActivity.get(user, scheduled_activity_id),
555 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
557 |> put_view(ScheduledActivityView)
558 |> render("show.json", %{scheduled_activity: scheduled_activity})
560 nil -> {:error, :not_found}
565 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
566 with %ScheduledActivity{} = scheduled_activity <-
567 ScheduledActivity.get(user, scheduled_activity_id),
568 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
570 |> put_view(ScheduledActivityView)
571 |> render("show.json", %{scheduled_activity: scheduled_activity})
573 nil -> {:error, :not_found}
579 %{assigns: %{user: user}} = conn,
580 %{"status" => _, "scheduled_at" => scheduled_at} = params
582 if ScheduledActivity.far_enough?(scheduled_at) do
583 with {:ok, scheduled_activity} <-
584 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
586 |> put_view(ScheduledActivityView)
587 |> render("show.json", %{scheduled_activity: scheduled_activity})
590 post_status(conn, Map.drop(params, ["scheduled_at"]))
594 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
595 case CommonAPI.post(user, params) do
598 |> put_view(StatusView)
599 |> try_render("status.json", %{
603 with_direct_conversation_id: true
608 |> put_status(:unprocessable_entity)
609 |> json(%{error: message})
613 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
614 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
617 _e -> render_error(conn, :forbidden, "Can't delete this post")
621 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
622 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
623 %Activity{} = announce <- Activity.normalize(announce.data) do
625 |> put_view(StatusView)
626 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
630 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
631 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
632 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
634 |> put_view(StatusView)
635 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
639 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
640 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
641 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
643 |> put_view(StatusView)
644 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
648 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
649 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
650 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
658 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
660 |> put_view(StatusView)
661 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
665 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
666 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
668 |> put_view(StatusView)
669 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
673 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
674 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
675 %User{} = user <- User.get_cached_by_nickname(user.nickname),
676 true <- Visibility.visible_for_user?(activity, user),
677 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
679 |> put_view(StatusView)
680 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
684 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
685 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
686 %User{} = user <- User.get_cached_by_nickname(user.nickname),
687 true <- Visibility.visible_for_user?(activity, user),
688 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
695 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
696 activity = Activity.get_by_id(id)
698 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
700 |> put_view(StatusView)
701 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
705 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
706 activity = Activity.get_by_id(id)
708 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
710 |> put_view(StatusView)
711 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
715 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
717 q = from(u in User, where: u.id in ^id)
718 targets = Repo.all(q)
721 |> put_view(AccountView)
722 |> render("relationships.json", %{user: user, targets: targets})
725 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
726 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
728 def update_media(%{assigns: %{user: user}} = conn, data) do
729 with %Object{} = object <- Repo.get(Object, data["id"]),
730 true <- Object.authorize_mutation(object, user),
731 true <- is_binary(data["description"]),
732 description <- data["description"] do
733 new_data = %{object.data | "name" => description}
737 |> Object.change(%{data: new_data})
740 attachment_data = Map.put(new_data, "id", object.id)
743 |> put_view(StatusView)
744 |> render("attachment.json", %{attachment: attachment_data})
748 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
749 with {:ok, object} <-
752 actor: User.ap_id(user),
753 description: Map.get(data, "description")
755 attachment_data = Map.put(object.data, "id", object.id)
758 |> put_view(StatusView)
759 |> render("attachment.json", %{attachment: attachment_data})
763 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
764 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
765 %{} = attachment_data <- Map.put(object.data, "id", object.id),
766 # Reject if not an image
767 %{type: "image"} = rendered <-
768 StatusView.render("attachment.json", %{attachment: attachment_data}) do
770 # Save to the user's info
771 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
775 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
779 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
780 mascot = User.get_mascot(user)
786 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
787 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
788 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
789 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
790 q = from(u in User, where: u.ap_id in ^likes)
794 |> Enum.filter(&(not User.blocks?(user, &1)))
797 |> put_view(AccountView)
798 |> render("accounts.json", %{for: user, users: users, as: :user})
800 {:visible, false} -> {:error, :not_found}
805 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
806 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
807 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
808 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
809 q = from(u in User, where: u.ap_id in ^announces)
813 |> Enum.filter(&(not User.blocks?(user, &1)))
816 |> put_view(AccountView)
817 |> render("accounts.json", %{for: user, users: users, as: :user})
819 {:visible, false} -> {:error, :not_found}
824 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
825 local_only = params["local"] in [true, "True", "true", "1"]
828 [params["tag"], params["any"]]
832 |> Enum.map(&String.downcase(&1))
837 |> Enum.map(&String.downcase(&1))
842 |> Enum.map(&String.downcase(&1))
846 |> Map.put("type", "Create")
847 |> Map.put("local_only", local_only)
848 |> Map.put("blocking_user", user)
849 |> Map.put("muting_user", user)
850 |> Map.put("user", user)
851 |> Map.put("tag", tags)
852 |> Map.put("tag_all", tag_all)
853 |> Map.put("tag_reject", tag_reject)
854 |> ActivityPub.fetch_public_activities()
858 |> add_link_headers(activities, %{"local" => local_only})
859 |> put_view(StatusView)
860 |> render("index.json", %{activities: activities, for: user, as: :activity})
863 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
864 with %User{} = user <- User.get_cached_by_id(id),
865 followers <- MastodonAPI.get_followers(user, params) do
868 for_user && user.id == for_user.id -> followers
869 user.info.hide_followers -> []
874 |> add_link_headers(followers)
875 |> put_view(AccountView)
876 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
880 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
881 with %User{} = user <- User.get_cached_by_id(id),
882 followers <- MastodonAPI.get_friends(user, params) do
885 for_user && user.id == for_user.id -> followers
886 user.info.hide_follows -> []
891 |> add_link_headers(followers)
892 |> put_view(AccountView)
893 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
897 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
898 follow_requests = User.get_follow_requests(followed)
901 |> put_view(AccountView)
902 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
905 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
906 with %User{} = follower <- User.get_cached_by_id(id),
907 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
909 |> put_view(AccountView)
910 |> render("relationship.json", %{user: followed, target: follower})
914 |> put_status(:forbidden)
915 |> json(%{error: message})
919 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
920 with %User{} = follower <- User.get_cached_by_id(id),
921 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
923 |> put_view(AccountView)
924 |> render("relationship.json", %{user: followed, target: follower})
928 |> put_status(:forbidden)
929 |> json(%{error: message})
933 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
934 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
935 {_, true} <- {:followed, follower.id != followed.id},
936 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: follower, target: followed})
946 |> put_status(:forbidden)
947 |> json(%{error: message})
951 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
952 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
953 {_, true} <- {:followed, follower.id != followed.id},
954 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
956 |> put_view(AccountView)
957 |> render("account.json", %{user: followed, for: follower})
964 |> put_status(:forbidden)
965 |> json(%{error: message})
969 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
970 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
971 {_, true} <- {:followed, follower.id != followed.id},
972 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
974 |> put_view(AccountView)
975 |> render("relationship.json", %{user: follower, target: followed})
985 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
987 if Map.has_key?(params, "notifications"),
988 do: params["notifications"] in [true, "True", "true", "1"],
991 with %User{} = muted <- User.get_cached_by_id(id),
992 {:ok, muter} <- User.mute(muter, muted, notifications) do
994 |> put_view(AccountView)
995 |> render("relationship.json", %{user: muter, target: muted})
999 |> put_status(:forbidden)
1000 |> json(%{error: message})
1004 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1005 with %User{} = muted <- User.get_cached_by_id(id),
1006 {:ok, muter} <- User.unmute(muter, muted) do
1008 |> put_view(AccountView)
1009 |> render("relationship.json", %{user: muter, target: muted})
1011 {:error, message} ->
1013 |> put_status(:forbidden)
1014 |> json(%{error: message})
1018 def mutes(%{assigns: %{user: user}} = conn, _) do
1019 with muted_accounts <- User.muted_users(user) do
1020 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1025 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1026 with %User{} = blocked <- User.get_cached_by_id(id),
1027 {:ok, blocker} <- User.block(blocker, blocked),
1028 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1030 |> put_view(AccountView)
1031 |> render("relationship.json", %{user: blocker, target: blocked})
1033 {:error, message} ->
1035 |> put_status(:forbidden)
1036 |> json(%{error: message})
1040 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1041 with %User{} = blocked <- User.get_cached_by_id(id),
1042 {:ok, blocker} <- User.unblock(blocker, blocked),
1043 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1045 |> put_view(AccountView)
1046 |> render("relationship.json", %{user: blocker, target: blocked})
1048 {:error, message} ->
1050 |> put_status(:forbidden)
1051 |> json(%{error: message})
1055 def blocks(%{assigns: %{user: user}} = conn, _) do
1056 with blocked_accounts <- User.blocked_users(user) do
1057 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1062 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1063 json(conn, info.domain_blocks || [])
1066 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1067 User.block_domain(blocker, domain)
1071 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1072 User.unblock_domain(blocker, domain)
1076 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1077 with %User{} = subscription_target <- User.get_cached_by_id(id),
1078 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1080 |> put_view(AccountView)
1081 |> render("relationship.json", %{user: user, target: subscription_target})
1083 {:error, message} ->
1085 |> put_status(:forbidden)
1086 |> json(%{error: message})
1090 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1091 with %User{} = subscription_target <- User.get_cached_by_id(id),
1092 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1094 |> put_view(AccountView)
1095 |> render("relationship.json", %{user: user, target: subscription_target})
1097 {:error, message} ->
1099 |> put_status(:forbidden)
1100 |> json(%{error: message})
1104 def favourites(%{assigns: %{user: user}} = conn, params) do
1107 |> Map.put("type", "Create")
1108 |> Map.put("favorited_by", user.ap_id)
1109 |> Map.put("blocking_user", user)
1112 ActivityPub.fetch_activities([], params)
1116 |> add_link_headers(activities)
1117 |> put_view(StatusView)
1118 |> render("index.json", %{activities: activities, for: user, as: :activity})
1121 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1122 with %User{} = user <- User.get_by_id(id),
1123 false <- user.info.hide_favorites do
1126 |> Map.put("type", "Create")
1127 |> Map.put("favorited_by", user.ap_id)
1128 |> Map.put("blocking_user", for_user)
1132 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1134 [Pleroma.Constants.as_public()]
1139 |> ActivityPub.fetch_activities(params)
1143 |> add_link_headers(activities)
1144 |> put_view(StatusView)
1145 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1147 nil -> {:error, :not_found}
1148 true -> render_error(conn, :forbidden, "Can't get favorites")
1152 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1153 user = User.get_cached_by_id(user.id)
1156 Bookmark.for_user_query(user.id)
1157 |> Pagination.fetch_paginated(params)
1161 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1164 |> add_link_headers(bookmarks)
1165 |> put_view(StatusView)
1166 |> render("index.json", %{activities: activities, for: user, as: :activity})
1169 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1170 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1171 res = ListView.render("lists.json", lists: lists)
1175 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1176 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1179 |> Map.put("type", "Create")
1180 |> Map.put("blocking_user", user)
1181 |> Map.put("user", user)
1182 |> Map.put("muting_user", user)
1184 # we must filter the following list for the user to avoid leaking statuses the user
1185 # does not actually have permission to see (for more info, peruse security issue #270).
1188 |> Enum.filter(fn x -> x in user.following end)
1189 |> ActivityPub.fetch_activities_bounded(following, params)
1193 |> put_view(StatusView)
1194 |> render("index.json", %{activities: activities, for: user, as: :activity})
1196 _e -> render_error(conn, :forbidden, "Error.")
1200 def index(%{assigns: %{user: user}} = conn, _params) do
1201 token = get_session(conn, :oauth_token)
1204 mastodon_emoji = mastodonized_emoji()
1206 limit = Config.get([:instance, :limit])
1209 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1214 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1215 access_token: token,
1217 domain: Pleroma.Web.Endpoint.host(),
1220 unfollow_modal: false,
1223 auto_play_gif: false,
1224 display_sensitive_media: false,
1225 reduce_motion: false,
1226 max_toot_chars: limit,
1227 mascot: User.get_mascot(user)["url"]
1229 poll_limits: Config.get([:instance, :poll_limits]),
1231 delete_others_notice: present?(user.info.is_moderator),
1232 admin: present?(user.info.is_admin)
1236 default_privacy: user.info.default_scope,
1237 default_sensitive: false,
1238 allow_content_types: Config.get([:instance, :allowed_post_formats])
1240 media_attachments: %{
1241 accept_content_types: [
1257 user.info.settings ||
1287 push_subscription: nil,
1289 custom_emojis: mastodon_emoji,
1295 |> put_layout(false)
1296 |> put_view(MastodonView)
1297 |> render("index.html", %{initial_state: initial_state})
1300 |> put_session(:return_to, conn.request_path)
1301 |> redirect(to: "/web/login")
1305 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1306 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
1311 |> put_status(:internal_server_error)
1312 |> json(%{error: inspect(e)})
1316 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1317 redirect(conn, to: local_mastodon_root_path(conn))
1320 @doc "Local Mastodon FE login init action"
1321 def login(conn, %{"code" => auth_token}) do
1322 with {:ok, app} <- get_or_make_app(),
1323 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1324 {:ok, token} <- Token.exchange_token(app, auth) do
1326 |> put_session(:oauth_token, token.token)
1327 |> redirect(to: local_mastodon_root_path(conn))
1331 @doc "Local Mastodon FE callback action"
1332 def login(conn, _) do
1333 with {:ok, app} <- get_or_make_app() do
1338 response_type: "code",
1339 client_id: app.client_id,
1341 scope: Enum.join(app.scopes, " ")
1344 redirect(conn, to: path)
1348 defp local_mastodon_root_path(conn) do
1349 case get_session(conn, :return_to) do
1351 mastodon_api_path(conn, :index, ["getting-started"])
1354 delete_session(conn, :return_to)
1359 defp get_or_make_app do
1360 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1361 scopes = ["read", "write", "follow", "push"]
1363 with %App{} = app <- Repo.get_by(App, find_attrs) do
1365 if app.scopes == scopes do
1369 |> Changeset.change(%{scopes: scopes})
1377 App.register_changeset(
1379 Map.put(find_attrs, :scopes, scopes)
1386 def logout(conn, _) do
1389 |> redirect(to: "/")
1392 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1393 Logger.debug("Unimplemented, returning unmodified relationship")
1395 with %User{} = target <- User.get_cached_by_id(id) do
1397 |> put_view(AccountView)
1398 |> render("relationship.json", %{user: user, target: target})
1402 def empty_array(conn, _) do
1403 Logger.debug("Unimplemented, returning an empty array")
1407 def empty_object(conn, _) do
1408 Logger.debug("Unimplemented, returning an empty object")
1412 def get_filters(%{assigns: %{user: user}} = conn, _) do
1413 filters = Filter.get_filters(user)
1414 res = FilterView.render("filters.json", filters: filters)
1419 %{assigns: %{user: user}} = conn,
1420 %{"phrase" => phrase, "context" => context} = params
1426 hide: Map.get(params, "irreversible", false),
1427 whole_word: Map.get(params, "boolean", true)
1431 {:ok, response} = Filter.create(query)
1432 res = FilterView.render("filter.json", filter: response)
1436 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1437 filter = Filter.get(filter_id, user)
1438 res = FilterView.render("filter.json", filter: filter)
1443 %{assigns: %{user: user}} = conn,
1444 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1448 filter_id: filter_id,
1451 hide: Map.get(params, "irreversible", nil),
1452 whole_word: Map.get(params, "boolean", true)
1456 {:ok, response} = Filter.update(query)
1457 res = FilterView.render("filter.json", filter: response)
1461 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1464 filter_id: filter_id
1467 {:ok, _} = Filter.delete(query)
1471 def suggestions(%{assigns: %{user: user}} = conn, _) do
1472 suggestions = Config.get(:suggestions)
1474 if Keyword.get(suggestions, :enabled, false) do
1475 api = Keyword.get(suggestions, :third_party_engine, "")
1476 timeout = Keyword.get(suggestions, :timeout, 5000)
1477 limit = Keyword.get(suggestions, :limit, 23)
1479 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1481 user = user.nickname
1485 |> String.replace("{{host}}", host)
1486 |> String.replace("{{user}}", user)
1488 with {:ok, %{status: 200, body: body}} <-
1489 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1490 {:ok, data} <- Jason.decode(body) do
1493 |> Enum.slice(0, limit)
1496 |> Map.put("id", fetch_suggestion_id(x))
1497 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1498 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1504 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1511 defp fetch_suggestion_id(attrs) do
1512 case User.get_or_fetch(attrs["acct"]) do
1513 {:ok, %User{id: id}} -> id
1518 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1519 with %Activity{} = activity <- Activity.get_by_id(status_id),
1520 true <- Visibility.visible_for_user?(activity, user) do
1524 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1534 def reports(%{assigns: %{user: user}} = conn, params) do
1535 case CommonAPI.report(user, params) do
1538 |> put_view(ReportView)
1539 |> try_render("report.json", %{activity: activity})
1543 |> put_status(:bad_request)
1544 |> json(%{error: err})
1548 def account_register(
1549 %{assigns: %{app: app}} = conn,
1550 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1558 "captcha_answer_data",
1562 |> Map.put("nickname", nickname)
1563 |> Map.put("fullname", params["fullname"] || nickname)
1564 |> Map.put("bio", params["bio"] || "")
1565 |> Map.put("confirm", params["password"])
1567 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1568 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1570 token_type: "Bearer",
1571 access_token: token.token,
1573 created_at: Token.Utils.format_created_at(token)
1578 |> put_status(:bad_request)
1583 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1584 render_error(conn, :bad_request, "Missing parameters")
1587 def account_register(conn, _) do
1588 render_error(conn, :forbidden, "Invalid credentials")
1591 def conversations(%{assigns: %{user: user}} = conn, params) do
1592 participations = Participation.for_user_with_last_activity_id(user, params)
1595 Enum.map(participations, fn participation ->
1596 ConversationView.render("participation.json", %{participation: participation, for: user})
1600 |> add_link_headers(participations)
1601 |> json(conversations)
1604 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1605 with %Participation{} = participation <-
1606 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1607 {:ok, participation} <- Participation.mark_as_read(participation) do
1608 participation_view =
1609 ConversationView.render("participation.json", %{participation: participation, for: user})
1612 |> json(participation_view)
1616 def password_reset(conn, params) do
1617 nickname_or_email = params["email"] || params["nickname"]
1619 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1621 |> put_status(:no_content)
1624 {:error, "unknown user"} ->
1625 send_resp(conn, :not_found, "")
1628 send_resp(conn, :bad_request, "")
1632 def account_confirmation_resend(conn, params) do
1633 nickname_or_email = params["email"] || params["nickname"]
1635 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1636 {:ok, _} <- User.try_send_confirmation_email(user) do
1638 |> json_response(:no_content, "")
1642 def try_render(conn, target, params)
1643 when is_binary(target) do
1644 case render(conn, target, params) do
1645 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1650 def try_render(conn, _, _) do
1651 render_error(conn, :not_implemented, "Can't display this activity")
1654 defp present?(nil), do: false
1655 defp present?(false), do: false
1656 defp present?(_), do: true