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.RichMedia
46 alias Pleroma.Web.TwitterAPI.TwitterAPI
48 alias Pleroma.Web.ControllerHelper
52 require Pleroma.Constants
54 @rate_limited_relations_actions ~w(follow unfollow)a
56 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
57 post_status delete_status)a
61 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
62 when action in ~w(reblog_status unreblog_status)a
67 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
68 when action in ~w(fav_status unfav_status)a
73 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
76 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
77 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
78 plug(RateLimiter, :app_account_creation when action == :account_register)
79 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
80 plug(RateLimiter, :password_reset when action == :password_reset)
81 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
83 @local_mastodon_name "Mastodon-Local"
85 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
87 def create_app(conn, params) do
88 scopes = Scopes.fetch_scopes(params, ["read"])
92 |> Map.drop(["scope", "scopes"])
93 |> Map.put("scopes", scopes)
95 with cs <- App.register_changeset(%App{}, app_attrs),
96 false <- cs.changes[:client_name] == @local_mastodon_name,
97 {:ok, app} <- Repo.insert(cs) do
100 |> render("show.json", %{app: app})
109 value_function \\ fn x -> {:ok, x} end
111 if Map.has_key?(params, params_field) do
112 case value_function.(params[params_field]) do
113 {:ok, new_value} -> Map.put(map, map_field, new_value)
121 def update_credentials(%{assigns: %{user: user}} = conn, params) do
126 |> add_if_present(params, "display_name", :name)
127 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
128 |> add_if_present(params, "avatar", :avatar, fn value ->
129 with %Plug.Upload{} <- value,
130 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
137 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
141 |> Map.get(:emoji, [])
142 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
149 :hide_followers_count,
155 :skip_thread_containment,
158 |> Enum.reduce(%{}, fn key, acc ->
159 add_if_present(acc, params, to_string(key), key, fn value ->
160 {:ok, ControllerHelper.truthy_param?(value)}
163 |> add_if_present(params, "default_scope", :default_scope)
164 |> add_if_present(params, "fields", :fields, fn fields ->
165 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
169 |> add_if_present(params, "fields", :raw_fields)
170 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
171 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
173 |> add_if_present(params, "header", :banner, fn value ->
174 with %Plug.Upload{} <- value,
175 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
181 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
182 with %Plug.Upload{} <- value,
183 {:ok, object} <- ActivityPub.upload(value, type: :background) do
189 |> Map.put(:emoji, user_info_emojis)
193 |> User.update_changeset(user_params)
194 |> User.change_info(&User.Info.profile_update(&1, info_params))
196 with {:ok, user} <- User.update_and_set_cache(changeset) do
197 if original_user != user, do: CommonAPI.update(user)
201 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
204 _e -> render_error(conn, :forbidden, "Invalid request")
208 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
209 change = Changeset.change(user, %{avatar: nil})
210 {:ok, user} = User.update_and_set_cache(change)
211 CommonAPI.update(user)
213 json(conn, %{url: nil})
216 def update_avatar(%{assigns: %{user: user}} = conn, params) do
217 {:ok, object} = ActivityPub.upload(params, type: :avatar)
218 change = Changeset.change(user, %{avatar: object.data})
219 {:ok, user} = User.update_and_set_cache(change)
220 CommonAPI.update(user)
221 %{"url" => [%{"href" => href} | _]} = object.data
223 json(conn, %{url: href})
226 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
227 new_info = %{"banner" => %{}}
229 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
230 CommonAPI.update(user)
231 json(conn, %{url: nil})
235 def update_banner(%{assigns: %{user: user}} = conn, params) do
236 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
237 new_info <- %{"banner" => object.data},
238 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
239 CommonAPI.update(user)
240 %{"url" => [%{"href" => href} | _]} = object.data
242 json(conn, %{url: href})
246 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
247 new_info = %{"background" => %{}}
249 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
250 json(conn, %{url: nil})
254 def update_background(%{assigns: %{user: user}} = conn, params) do
255 with {:ok, object} <- ActivityPub.upload(params, type: :background),
256 new_info <- %{"background" => object.data},
257 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
258 %{"url" => [%{"href" => href} | _]} = object.data
260 json(conn, %{url: href})
264 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
265 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
268 AccountView.render("account.json", %{
271 with_pleroma_settings: true,
272 with_chat_token: chat_token
278 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
279 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
282 |> render("short.json", %{app: app})
286 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
287 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
288 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
289 account = AccountView.render("account.json", %{user: user, for: for_user})
292 _e -> render_error(conn, :not_found, "Can't find user")
296 @mastodon_api_level "2.7.2"
298 def masto_instance(conn, _params) do
299 instance = Config.get(:instance)
303 title: Keyword.get(instance, :name),
304 description: Keyword.get(instance, :description),
305 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
306 email: Keyword.get(instance, :email),
308 streaming_api: Pleroma.Web.Endpoint.websocket_url()
310 stats: Stats.get_stats(),
311 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
313 registrations: Pleroma.Config.get([:instance, :registrations_open]),
314 # Extra (not present in Mastodon):
315 max_toot_chars: Keyword.get(instance, :limit),
316 poll_limits: Keyword.get(instance, :poll_limits)
322 def peers(conn, _params) do
323 json(conn, Stats.get_peers())
326 defp mastodonized_emoji do
327 Pleroma.Emoji.get_all()
328 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
329 url = to_string(URI.merge(Web.base_url(), relative_url))
332 "shortcode" => shortcode,
334 "visible_in_picker" => true,
337 # Assuming that a comma is authorized in the category name
338 "category" => (tags -- ["Custom"]) |> Enum.join(",")
343 def custom_emojis(conn, _params) do
344 mastodon_emoji = mastodonized_emoji()
345 json(conn, mastodon_emoji)
348 def home_timeline(%{assigns: %{user: user}} = conn, params) do
351 |> Map.put("type", ["Create", "Announce"])
352 |> Map.put("blocking_user", user)
353 |> Map.put("muting_user", user)
354 |> Map.put("user", user)
357 [user.ap_id | user.following]
358 |> ActivityPub.fetch_activities(params)
362 |> add_link_headers(activities)
363 |> put_view(StatusView)
364 |> render("index.json", %{activities: activities, for: user, as: :activity})
367 def public_timeline(%{assigns: %{user: user}} = conn, params) do
368 local_only = params["local"] in [true, "True", "true", "1"]
372 |> Map.put("type", ["Create", "Announce"])
373 |> Map.put("local_only", local_only)
374 |> Map.put("blocking_user", user)
375 |> Map.put("muting_user", user)
376 |> ActivityPub.fetch_public_activities()
380 |> add_link_headers(activities, %{"local" => local_only})
381 |> put_view(StatusView)
382 |> render("index.json", %{activities: activities, for: user, as: :activity})
385 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
386 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
389 |> Map.put("tag", params["tagged"])
391 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
394 |> add_link_headers(activities)
395 |> put_view(StatusView)
396 |> render("index.json", %{
397 activities: activities,
404 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
407 |> Map.put("type", "Create")
408 |> Map.put("blocking_user", user)
409 |> Map.put("user", user)
410 |> Map.put(:visibility, "direct")
414 |> ActivityPub.fetch_activities_query(params)
415 |> Pagination.fetch_paginated(params)
418 |> add_link_headers(activities)
419 |> put_view(StatusView)
420 |> render("index.json", %{activities: activities, for: user, as: :activity})
423 def get_statuses(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
429 |> Activity.all_by_ids_with_object()
430 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
433 |> put_view(StatusView)
434 |> render("index.json", activities: activities, for: user, as: :activity)
437 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
438 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
439 true <- Visibility.visible_for_user?(activity, user) do
441 |> put_view(StatusView)
442 |> try_render("status.json", %{activity: activity, for: user})
446 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
447 with %Activity{} = activity <- Activity.get_by_id(id),
449 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
450 "blocking_user" => user,
452 "exclude_id" => activity.id
454 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
457 StatusView.render("index.json",
459 activities: grouped_activities[true] || [],
463 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
465 StatusView.render("index.json",
467 activities: grouped_activities[false] || [],
471 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
478 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
479 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
480 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
481 true <- Visibility.visible_for_user?(activity, user) do
483 |> put_view(StatusView)
484 |> try_render("poll.json", %{object: object, for: user})
486 error when is_nil(error) or error == false ->
487 render_error(conn, :not_found, "Record not found")
491 defp get_cached_vote_or_vote(user, object, choices) do
492 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
495 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
496 case CommonAPI.vote(user, object, choices) do
497 {:error, _message} = res -> {:ignore, res}
498 res -> {:commit, res}
505 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
506 with %Object{} = object <- Object.get_by_id(id),
507 true <- object.data["type"] == "Question",
508 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
509 true <- Visibility.visible_for_user?(activity, user),
510 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
512 |> put_view(StatusView)
513 |> try_render("poll.json", %{object: object, for: user})
516 render_error(conn, :not_found, "Record not found")
519 render_error(conn, :not_found, "Record not found")
523 |> put_status(:unprocessable_entity)
524 |> json(%{error: message})
528 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
529 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
531 |> add_link_headers(scheduled_activities)
532 |> put_view(ScheduledActivityView)
533 |> render("index.json", %{scheduled_activities: scheduled_activities})
537 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
538 with %ScheduledActivity{} = scheduled_activity <-
539 ScheduledActivity.get(user, scheduled_activity_id) do
541 |> put_view(ScheduledActivityView)
542 |> render("show.json", %{scheduled_activity: scheduled_activity})
544 _ -> {:error, :not_found}
548 def update_scheduled_status(
549 %{assigns: %{user: user}} = conn,
550 %{"id" => scheduled_activity_id} = params
552 with %ScheduledActivity{} = scheduled_activity <-
553 ScheduledActivity.get(user, scheduled_activity_id),
554 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
556 |> put_view(ScheduledActivityView)
557 |> render("show.json", %{scheduled_activity: scheduled_activity})
559 nil -> {:error, :not_found}
564 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
565 with %ScheduledActivity{} = scheduled_activity <-
566 ScheduledActivity.get(user, scheduled_activity_id),
567 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
569 |> put_view(ScheduledActivityView)
570 |> render("show.json", %{scheduled_activity: scheduled_activity})
572 nil -> {:error, :not_found}
577 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
580 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
582 scheduled_at = params["scheduled_at"]
584 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
585 with {:ok, scheduled_activity} <-
586 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
588 |> put_view(ScheduledActivityView)
589 |> render("show.json", %{scheduled_activity: scheduled_activity})
592 params = Map.drop(params, ["scheduled_at"])
594 case CommonAPI.post(user, params) do
597 |> put_status(:unprocessable_entity)
598 |> json(%{error: message})
602 |> put_view(StatusView)
603 |> try_render("status.json", %{
607 with_direct_conversation_id: true
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
716 targets = User.get_all_by_ids(List.wrap(id))
719 |> put_view(AccountView)
720 |> render("relationships.json", %{user: user, targets: targets})
723 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
724 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
727 %{assigns: %{user: user}} = conn,
728 %{"id" => id, "description" => description} = _
730 when is_binary(description) do
731 with %Object{} = object <- Repo.get(Object, id),
732 true <- Object.authorize_mutation(object, user),
733 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
734 attachment_data = Map.put(data, "id", object.id)
737 |> put_view(StatusView)
738 |> render("attachment.json", %{attachment: attachment_data})
742 def update_media(_conn, _data), do: {:error, :bad_request}
744 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
745 with {:ok, object} <-
748 actor: User.ap_id(user),
749 description: Map.get(data, "description")
751 attachment_data = Map.put(object.data, "id", object.id)
754 |> put_view(StatusView)
755 |> render("attachment.json", %{attachment: attachment_data})
759 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
760 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
761 %{} = attachment_data <- Map.put(object.data, "id", object.id),
762 # Reject if not an image
763 %{type: "image"} = rendered <-
764 StatusView.render("attachment.json", %{attachment: attachment_data}) do
766 # Save to the user's info
767 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
771 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
775 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
776 mascot = User.get_mascot(user)
781 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
782 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
783 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
784 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
785 q = from(u in User, where: u.ap_id in ^likes)
789 |> Enum.filter(&(not User.blocks?(user, &1)))
792 |> put_view(AccountView)
793 |> render("accounts.json", %{for: user, users: users, as: :user})
795 {:visible, false} -> {:error, :not_found}
800 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
801 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
802 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
803 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
804 q = from(u in User, where: u.ap_id in ^announces)
808 |> Enum.filter(&(not User.blocks?(user, &1)))
811 |> put_view(AccountView)
812 |> render("accounts.json", %{for: user, users: users, as: :user})
814 {:visible, false} -> {:error, :not_found}
819 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
820 local_only = params["local"] in [true, "True", "true", "1"]
823 [params["tag"], params["any"]]
827 |> Enum.map(&String.downcase(&1))
832 |> Enum.map(&String.downcase(&1))
837 |> Enum.map(&String.downcase(&1))
841 |> Map.put("type", "Create")
842 |> Map.put("local_only", local_only)
843 |> Map.put("blocking_user", user)
844 |> Map.put("muting_user", user)
845 |> Map.put("user", user)
846 |> Map.put("tag", tags)
847 |> Map.put("tag_all", tag_all)
848 |> Map.put("tag_reject", tag_reject)
849 |> ActivityPub.fetch_public_activities()
853 |> add_link_headers(activities, %{"local" => local_only})
854 |> put_view(StatusView)
855 |> render("index.json", %{activities: activities, for: user, as: :activity})
858 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
859 with %User{} = user <- User.get_cached_by_id(id),
860 followers <- MastodonAPI.get_followers(user, params) do
863 for_user && user.id == for_user.id -> followers
864 user.info.hide_followers -> []
869 |> add_link_headers(followers)
870 |> put_view(AccountView)
871 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
875 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
876 with %User{} = user <- User.get_cached_by_id(id),
877 followers <- MastodonAPI.get_friends(user, params) do
880 for_user && user.id == for_user.id -> followers
881 user.info.hide_follows -> []
886 |> add_link_headers(followers)
887 |> put_view(AccountView)
888 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
892 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
893 follow_requests = User.get_follow_requests(followed)
896 |> put_view(AccountView)
897 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
900 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
901 with %User{} = follower <- User.get_cached_by_id(id),
902 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
904 |> put_view(AccountView)
905 |> render("relationship.json", %{user: followed, target: follower})
909 |> put_status(:forbidden)
910 |> json(%{error: message})
914 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
915 with %User{} = follower <- User.get_cached_by_id(id),
916 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
918 |> put_view(AccountView)
919 |> render("relationship.json", %{user: followed, target: follower})
923 |> put_status(:forbidden)
924 |> json(%{error: message})
928 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
929 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
930 {_, true} <- {:followed, follower.id != followed.id},
931 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
933 |> put_view(AccountView)
934 |> render("relationship.json", %{user: follower, target: followed})
941 |> put_status(:forbidden)
942 |> json(%{error: message})
946 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
947 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
948 {_, true} <- {:followed, follower.id != followed.id},
949 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
951 |> put_view(AccountView)
952 |> render("account.json", %{user: followed, for: follower})
959 |> put_status(:forbidden)
960 |> json(%{error: message})
964 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
965 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
966 {_, true} <- {:followed, follower.id != followed.id},
967 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
969 |> put_view(AccountView)
970 |> render("relationship.json", %{user: follower, target: followed})
980 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
982 if Map.has_key?(params, "notifications"),
983 do: params["notifications"] in [true, "True", "true", "1"],
986 with %User{} = muted <- User.get_cached_by_id(id),
987 {:ok, muter} <- User.mute(muter, muted, notifications) do
989 |> put_view(AccountView)
990 |> render("relationship.json", %{user: muter, target: muted})
994 |> put_status(:forbidden)
995 |> json(%{error: message})
999 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1000 with %User{} = muted <- User.get_cached_by_id(id),
1001 {:ok, muter} <- User.unmute(muter, muted) do
1003 |> put_view(AccountView)
1004 |> render("relationship.json", %{user: muter, target: muted})
1006 {:error, message} ->
1008 |> put_status(:forbidden)
1009 |> json(%{error: message})
1013 def mutes(%{assigns: %{user: user}} = conn, _) do
1014 with muted_accounts <- User.muted_users(user) do
1015 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1020 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1021 with %User{} = blocked <- User.get_cached_by_id(id),
1022 {:ok, blocker} <- User.block(blocker, blocked),
1023 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1025 |> put_view(AccountView)
1026 |> render("relationship.json", %{user: blocker, target: blocked})
1028 {:error, message} ->
1030 |> put_status(:forbidden)
1031 |> json(%{error: message})
1035 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1036 with %User{} = blocked <- User.get_cached_by_id(id),
1037 {:ok, blocker} <- User.unblock(blocker, blocked),
1038 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1040 |> put_view(AccountView)
1041 |> render("relationship.json", %{user: blocker, target: blocked})
1043 {:error, message} ->
1045 |> put_status(:forbidden)
1046 |> json(%{error: message})
1050 def blocks(%{assigns: %{user: user}} = conn, _) do
1051 with blocked_accounts <- User.blocked_users(user) do
1052 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1057 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1058 json(conn, info.domain_blocks || [])
1061 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1062 User.block_domain(blocker, domain)
1066 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1067 User.unblock_domain(blocker, domain)
1071 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1072 with %User{} = subscription_target <- User.get_cached_by_id(id),
1073 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1075 |> put_view(AccountView)
1076 |> render("relationship.json", %{user: user, target: subscription_target})
1078 nil -> {:error, :not_found}
1083 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1084 with %User{} = subscription_target <- User.get_cached_by_id(id),
1085 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1087 |> put_view(AccountView)
1088 |> render("relationship.json", %{user: user, target: subscription_target})
1090 nil -> {:error, :not_found}
1095 def favourites(%{assigns: %{user: user}} = conn, params) do
1098 |> Map.put("type", "Create")
1099 |> Map.put("favorited_by", user.ap_id)
1100 |> Map.put("blocking_user", user)
1103 ActivityPub.fetch_activities([], params)
1107 |> add_link_headers(activities)
1108 |> put_view(StatusView)
1109 |> render("index.json", %{activities: activities, for: user, as: :activity})
1112 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1113 with %User{} = user <- User.get_by_id(id),
1114 false <- user.info.hide_favorites do
1117 |> Map.put("type", "Create")
1118 |> Map.put("favorited_by", user.ap_id)
1119 |> Map.put("blocking_user", for_user)
1123 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1125 [Pleroma.Constants.as_public()]
1130 |> ActivityPub.fetch_activities(params)
1134 |> add_link_headers(activities)
1135 |> put_view(StatusView)
1136 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1138 nil -> {:error, :not_found}
1139 true -> render_error(conn, :forbidden, "Can't get favorites")
1143 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1144 user = User.get_cached_by_id(user.id)
1147 Bookmark.for_user_query(user.id)
1148 |> Pagination.fetch_paginated(params)
1152 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1155 |> add_link_headers(bookmarks)
1156 |> put_view(StatusView)
1157 |> render("index.json", %{activities: activities, for: user, as: :activity})
1160 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1161 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1164 |> put_view(ListView)
1165 |> render("index.json", %{lists: lists})
1168 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1169 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1172 |> Map.put("type", "Create")
1173 |> Map.put("blocking_user", user)
1174 |> Map.put("user", user)
1175 |> Map.put("muting_user", user)
1177 # we must filter the following list for the user to avoid leaking statuses the user
1178 # does not actually have permission to see (for more info, peruse security issue #270).
1181 |> Enum.filter(fn x -> x in user.following end)
1182 |> ActivityPub.fetch_activities_bounded(following, params)
1186 |> put_view(StatusView)
1187 |> render("index.json", %{activities: activities, for: user, as: :activity})
1189 _e -> render_error(conn, :forbidden, "Error.")
1193 def index(%{assigns: %{user: user}} = conn, _params) do
1194 token = get_session(conn, :oauth_token)
1197 mastodon_emoji = mastodonized_emoji()
1199 limit = Config.get([:instance, :limit])
1202 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1207 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1208 access_token: token,
1210 domain: Pleroma.Web.Endpoint.host(),
1213 unfollow_modal: false,
1216 auto_play_gif: false,
1217 display_sensitive_media: false,
1218 reduce_motion: false,
1219 max_toot_chars: limit,
1220 mascot: User.get_mascot(user)["url"]
1222 poll_limits: Config.get([:instance, :poll_limits]),
1224 delete_others_notice: present?(user.info.is_moderator),
1225 admin: present?(user.info.is_admin)
1229 default_privacy: user.info.default_scope,
1230 default_sensitive: false,
1231 allow_content_types: Config.get([:instance, :allowed_post_formats])
1233 media_attachments: %{
1234 accept_content_types: [
1250 user.info.settings ||
1280 push_subscription: nil,
1282 custom_emojis: mastodon_emoji,
1288 |> put_layout(false)
1289 |> put_view(MastodonView)
1290 |> render("index.html", %{initial_state: initial_state})
1293 |> put_session(:return_to, conn.request_path)
1294 |> redirect(to: "/web/login")
1298 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1299 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
1304 |> put_status(:internal_server_error)
1305 |> json(%{error: inspect(e)})
1309 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1310 redirect(conn, to: local_mastodon_root_path(conn))
1313 @doc "Local Mastodon FE login init action"
1314 def login(conn, %{"code" => auth_token}) do
1315 with {:ok, app} <- get_or_make_app(),
1316 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
1317 {:ok, token} <- Token.exchange_token(app, auth) do
1319 |> put_session(:oauth_token, token.token)
1320 |> redirect(to: local_mastodon_root_path(conn))
1324 @doc "Local Mastodon FE callback action"
1325 def login(conn, _) do
1326 with {:ok, app} <- get_or_make_app() do
1328 o_auth_path(conn, :authorize,
1329 response_type: "code",
1330 client_id: app.client_id,
1332 scope: Enum.join(app.scopes, " ")
1335 redirect(conn, to: path)
1339 defp local_mastodon_root_path(conn) do
1340 case get_session(conn, :return_to) do
1342 mastodon_api_path(conn, :index, ["getting-started"])
1345 delete_session(conn, :return_to)
1350 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
1351 defp get_or_make_app do
1353 %{client_name: @local_mastodon_name, redirect_uris: "."},
1354 ["read", "write", "follow", "push"]
1358 def logout(conn, _) do
1361 |> redirect(to: "/")
1364 # Stubs for unimplemented mastodon api
1366 def empty_array(conn, _) do
1367 Logger.debug("Unimplemented, returning an empty array")
1371 def get_filters(%{assigns: %{user: user}} = conn, _) do
1372 filters = Filter.get_filters(user)
1373 res = FilterView.render("filters.json", filters: filters)
1378 %{assigns: %{user: user}} = conn,
1379 %{"phrase" => phrase, "context" => context} = params
1385 hide: Map.get(params, "irreversible", false),
1386 whole_word: Map.get(params, "boolean", true)
1390 {:ok, response} = Filter.create(query)
1391 res = FilterView.render("filter.json", filter: response)
1395 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1396 filter = Filter.get(filter_id, user)
1397 res = FilterView.render("filter.json", filter: filter)
1402 %{assigns: %{user: user}} = conn,
1403 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1407 filter_id: filter_id,
1410 hide: Map.get(params, "irreversible", nil),
1411 whole_word: Map.get(params, "boolean", true)
1415 {:ok, response} = Filter.update(query)
1416 res = FilterView.render("filter.json", filter: response)
1420 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1423 filter_id: filter_id
1426 {:ok, _} = Filter.delete(query)
1430 def suggestions(%{assigns: %{user: user}} = conn, _) do
1431 suggestions = Config.get(:suggestions)
1433 if Keyword.get(suggestions, :enabled, false) do
1434 api = Keyword.get(suggestions, :third_party_engine, "")
1435 timeout = Keyword.get(suggestions, :timeout, 5000)
1436 limit = Keyword.get(suggestions, :limit, 23)
1438 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1440 user = user.nickname
1444 |> String.replace("{{host}}", host)
1445 |> String.replace("{{user}}", user)
1447 with {:ok, %{status: 200, body: body}} <-
1448 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1449 {:ok, data} <- Jason.decode(body) do
1452 |> Enum.slice(0, limit)
1455 |> Map.put("id", fetch_suggestion_id(x))
1456 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1457 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1463 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1470 defp fetch_suggestion_id(attrs) do
1471 case User.get_or_fetch(attrs["acct"]) do
1472 {:ok, %User{id: id}} -> id
1477 def status_card(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1478 with %Activity{} = activity <- Activity.get_by_id(id),
1479 true <- Visibility.visible_for_user?(activity, user) do
1480 data = RichMedia.Helpers.fetch_data_for_activity(activity)
1483 |> put_view(StatusView)
1484 |> render("card.json", data)
1486 _e -> {:error, :not_found}
1490 def reports(%{assigns: %{user: user}} = conn, params) do
1491 case CommonAPI.report(user, params) do
1494 |> put_view(ReportView)
1495 |> try_render("report.json", %{activity: activity})
1499 |> put_status(:bad_request)
1500 |> json(%{error: err})
1504 def account_register(
1505 %{assigns: %{app: app}} = conn,
1506 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1514 "captcha_answer_data",
1518 |> Map.put("nickname", nickname)
1519 |> Map.put("fullname", params["fullname"] || nickname)
1520 |> Map.put("bio", params["bio"] || "")
1521 |> Map.put("confirm", params["password"])
1523 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1524 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1526 token_type: "Bearer",
1527 access_token: token.token,
1529 created_at: Token.Utils.format_created_at(token)
1534 |> put_status(:bad_request)
1539 def account_register(%{assigns: %{app: _app}} = conn, _) do
1540 render_error(conn, :bad_request, "Missing parameters")
1543 def account_register(conn, _) do
1544 render_error(conn, :forbidden, "Invalid credentials")
1547 def conversations(%{assigns: %{user: user}} = conn, params) do
1548 participations = Participation.for_user_with_last_activity_id(user, params)
1551 Enum.map(participations, fn participation ->
1552 ConversationView.render("participation.json", %{participation: participation, for: user})
1556 |> add_link_headers(participations)
1557 |> json(conversations)
1560 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1561 with %Participation{} = participation <-
1562 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1563 {:ok, participation} <- Participation.mark_as_read(participation) do
1564 participation_view =
1565 ConversationView.render("participation.json", %{participation: participation, for: user})
1568 |> json(participation_view)
1572 def password_reset(conn, params) do
1573 nickname_or_email = params["email"] || params["nickname"]
1575 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1577 |> put_status(:no_content)
1580 {:error, "unknown user"} ->
1581 send_resp(conn, :not_found, "")
1584 send_resp(conn, :bad_request, "")
1588 def account_confirmation_resend(conn, params) do
1589 nickname_or_email = params["email"] || params["nickname"]
1591 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1592 {:ok, _} <- User.try_send_confirmation_email(user) do
1594 |> json_response(:no_content, "")
1598 defp try_render(conn, target, params)
1599 when is_binary(target) do
1600 case render(conn, target, params) do
1601 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1606 defp try_render(conn, _, _) do
1607 render_error(conn, :not_implemented, "Can't display this activity")
1610 defp present?(nil), do: false
1611 defp present?(false), do: false
1612 defp present?(_), do: true