aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/web/pleroma_api/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/web/pleroma_api/controllers')
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex159
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/app_controller.ex23
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/backup_controller.ex28
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex188
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex93
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex154
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex230
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex94
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/instances_controller.ex21
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex42
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/notification_controller.ex38
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/report_controller.ex46
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex52
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/settings_controller.ex79
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex133
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex61
16 files changed, 1441 insertions, 0 deletions
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
new file mode 100644
index 0000000..591391b
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -0,0 +1,159 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.AccountController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper,
+ only: [
+ json_response: 3,
+ add_link_headers: 2,
+ embed_relationships?: 1,
+ assign_account_by_id: 2
+ ]
+
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.Plugs.RateLimiter
+
+ require Pleroma.Constants
+
+ plug(
+ Majic.Plug,
+ [pool: Pleroma.MajicPool] when action in [:update_avatar, :update_background, :update_banner]
+ )
+
+ plug(
+ OpenApiSpex.Plug.PutApiSpec,
+ [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend
+ )
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(:skip_auth when action == :confirmation_resend)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
+ when action == :endorsements
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:accounts"]} when action == :birthdays
+ )
+
+ plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
+
+ plug(
+ :assign_account_by_id
+ when action in [:favourites, :endorsements, :subscribe, :unsubscribe]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation
+
+ @doc "POST /api/v1/pleroma/accounts/confirmation_resend"
+ def confirmation_resend(conn, params) do
+ nickname_or_email = params[:email] || params[:nickname]
+
+ with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
+ {:ok, _} <- User.maybe_send_confirmation_email(user) do
+ json_response(conn, :no_content, "")
+ end
+ end
+
+ @doc "GET /api/v1/pleroma/accounts/:id/favourites"
+ def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do
+ render_error(conn, :forbidden, "Can't get favorites")
+ end
+
+ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ params =
+ params
+ |> Map.put(:type, "Create")
+ |> Map.put(:favorited_by, user.ap_id)
+ |> Map.put(:blocking_user, for_user)
+
+ recipients =
+ if for_user do
+ [Pleroma.Constants.as_public()] ++ [for_user.ap_id | User.following(for_user)]
+ else
+ [Pleroma.Constants.as_public()]
+ end
+
+ activities =
+ recipients
+ |> ActivityPub.fetch_activities(params)
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> put_view(StatusView)
+ |> render("index.json",
+ activities: activities,
+ for: for_user,
+ as: :activity
+ )
+ end
+
+ @doc "GET /api/v1/pleroma/accounts/:id/endorsements"
+ def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ users =
+ user
+ |> User.endorsed_users_relation(_restrict_deactivated = true)
+ |> Pleroma.Repo.all()
+
+ conn
+ |> render("index.json",
+ for: for_user,
+ users: users,
+ as: :user,
+ embed_relationships: embed_relationships?(params)
+ )
+ end
+
+ @doc "POST /api/v1/pleroma/accounts/:id/subscribe"
+ def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
+ with {:ok, _subscription} <- User.subscribe(user, subscription_target) do
+ render(conn, "relationship.json", user: user, target: subscription_target)
+ else
+ {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ end
+ end
+
+ @doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
+ def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
+ with {:ok, _subscription} <- User.unsubscribe(user, subscription_target) do
+ render(conn, "relationship.json", user: user, target: subscription_target)
+ else
+ {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ end
+ end
+
+ @doc "GET /api/v1/pleroma/birthdays"
+ def birthdays(%{assigns: %{user: %User{} = user}} = conn, %{day: day, month: month} = _params) do
+ birthdays =
+ User.get_friends_birthdays_query(user, day, month)
+ |> Pleroma.Repo.all()
+
+ conn
+ |> render("index.json",
+ for: user,
+ users: birthdays,
+ as: :user
+ )
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/app_controller.ex b/lib/pleroma/web/pleroma_api/controllers/app_controller.ex
new file mode 100644
index 0000000..3e84f75
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/app_controller.ex
@@ -0,0 +1,23 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.AppController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(OAuthScopesPlug, %{scopes: ["follow", "read"]} when action in [:index])
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAppOperation
+
+ @doc "GET /api/v1/pleroma/apps"
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ with apps <- App.get_user_apps(user) do
+ render(conn, "index.json", %{apps: apps})
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
new file mode 100644
index 0000000..b9daed2
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex
@@ -0,0 +1,28 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.BackupController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User.Backup
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(OAuthScopesPlug, %{scopes: ["read:backups"]} when action in [:index, :create])
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBackupOperation
+
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ backups = Backup.list(user)
+ render(conn, "index.json", backups: backups)
+ end
+
+ def create(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, _} <- Backup.create(user) do
+ backups = Backup.list(user)
+ render(conn, "index.json", backups: backups)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
new file mode 100644
index 0000000..3d7b6a4
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -0,0 +1,188 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.PleromaAPI.ChatController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Object
+ alias Pleroma.Pagination
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ import Ecto.Query
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:chats"]}
+ when action in [
+ :post_chat_message,
+ :create,
+ :mark_as_read,
+ :mark_message_as_read,
+ :delete_message
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:chats"]} when action in [:messages, :index, :index2, :show]
+ )
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
+
+ def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
+ message_id: message_id,
+ id: chat_id
+ }) do
+ with %MessageReference{} = cm_ref <-
+ MessageReference.get_by_id(message_id),
+ ^chat_id <- to_string(cm_ref.chat_id),
+ %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
+ {:ok, _} <- remove_or_delete(cm_ref, user) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ else
+ _e ->
+ {:error, :could_not_delete}
+ end
+ end
+
+ defp remove_or_delete(
+ %{object: %{data: %{"actor" => actor, "id" => id}}},
+ %{ap_id: actor} = user
+ ) do
+ with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ CommonAPI.delete(activity.id, user)
+ end
+ end
+
+ defp remove_or_delete(cm_ref, _), do: MessageReference.delete(cm_ref)
+
+ def post_chat_message(
+ %{body_params: params, assigns: %{user: user}} = conn,
+ %{id: id}
+ ) do
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
+ %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
+ {:ok, activity} <-
+ CommonAPI.post_chat_message(user, recipient, params[:content],
+ media_id: params[:media_id],
+ idempotency_key: idempotency_key(conn)
+ ),
+ message <- Object.normalize(activity, fetch: false),
+ cm_ref <- MessageReference.for_chat_and_object(chat, message) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ else
+ {:reject, message} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
+
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: message})
+ end
+ end
+
+ def mark_message_as_read(
+ %{assigns: %{user: %{id: user_id}}} = conn,
+ %{id: chat_id, message_id: message_id}
+ ) do
+ with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id),
+ ^chat_id <- to_string(cm_ref.chat_id),
+ %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
+ {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ end
+ end
+
+ def mark_as_read(
+ %{body_params: %{last_read_id: last_read_id}, assigns: %{user: user}} = conn,
+ %{id: id}
+ ) do
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
+ {_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do
+ render(conn, "show.json", chat: chat)
+ end
+ end
+
+ def messages(%{assigns: %{user: user}} = conn, %{id: id} = params) do
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
+ chat_message_refs =
+ chat
+ |> MessageReference.for_chat_query()
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> add_link_headers(chat_message_refs)
+ |> put_view(MessageReferenceView)
+ |> render("index.json", chat_message_references: chat_message_refs)
+ end
+ end
+
+ def index(%{assigns: %{user: user}} = conn, params) do
+ chats =
+ index_query(user, params)
+ |> Repo.all()
+
+ render(conn, "index.json", chats: chats)
+ end
+
+ def index2(%{assigns: %{user: user}} = conn, params) do
+ chats =
+ index_query(user, params)
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> add_link_headers(chats)
+ |> render("index.json", chats: chats)
+ end
+
+ defp index_query(%{id: user_id} = user, params) do
+ exclude_users =
+ User.cached_blocked_users_ap_ids(user) ++
+ if params[:with_muted], do: [], else: User.cached_muted_users_ap_ids(user)
+
+ user_id
+ |> Chat.for_user_query()
+ |> where([c], c.recipient not in ^exclude_users)
+ end
+
+ def create(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %User{ap_id: recipient} <- User.get_cached_by_id(id),
+ {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
+ render(conn, "show.json", chat: chat)
+ end
+ end
+
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
+ render(conn, "show.json", chat: chat)
+ end
+ end
+
+ defp idempotency_key(conn) do
+ case get_req_header(conn, "idempotency-key") do
+ [key] -> key
+ _ -> nil
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
new file mode 100644
index 0000000..37990db
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
@@ -0,0 +1,93 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ConversationController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:show, :statuses])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:conversations"]} when action in [:update, :mark_as_read]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaConversationOperation
+
+ def show(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: participation_id}) do
+ with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id) do
+ render(conn, "participation.json", participation: participation, for: user)
+ else
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def statuses(
+ %{assigns: %{user: %{id: user_id} = user}} = conn,
+ %{id: participation_id} = params
+ ) do
+ with %Participation{user_id: ^user_id} = participation <-
+ Participation.get(participation_id, preload: [:conversation]) do
+ params =
+ params
+ |> Map.put(:blocking_user, user)
+ |> Map.put(:muting_user, user)
+ |> Map.put(:user, user)
+
+ activities =
+ participation.conversation.ap_id
+ |> ActivityPub.fetch_activities_for_context_query(params)
+ |> Pleroma.Pagination.fetch_paginated(Map.put(params, :total, false))
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> put_view(StatusView)
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ else
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def update(
+ %{assigns: %{user: %{id: user_id} = user}} = conn,
+ %{id: participation_id, recipients: recipients}
+ ) do
+ with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id),
+ {:ok, participation} <- Participation.set_recipients(participation, recipients) do
+ render(conn, "participation.json", participation: participation, for: user)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+
+ _error ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{"error" => "Unknown conversation id"})
+ end
+ end
+
+ def mark_as_read(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
+ conn
+ |> add_link_headers(participations)
+ |> render("participations.json", participations: participations, for: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
new file mode 100644
index 0000000..f854cf9
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
@@ -0,0 +1,154 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Emoji.Pack
+ alias Pleroma.Web.ApiSpec
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["admin:write"]}
+ when action in [
+ :create,
+ :update,
+ :delete
+ ]
+ )
+
+ defdelegate open_api_operation(action), to: ApiSpec.PleromaEmojiFileOperation
+
+ def create(%{body_params: params} = conn, %{name: pack_name}) do
+ filename = params[:filename] || get_filename(params[:file])
+ shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename))
+
+ with {:ok, pack} <- Pack.load_pack(pack_name),
+ {:ok, file} <- get_file(params[:file]),
+ {:ok, pack} <- Pack.add_file(pack, shortcode, filename, file) do
+ json(conn, pack.files)
+ else
+ {:error, :already_exists} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: "pack name, shortcode or filename cannot be empty"})
+
+ {:error, _} = error ->
+ handle_error(conn, error, %{
+ pack_name: pack_name,
+ message: "Unexpected error occurred while adding file to pack."
+ })
+ end
+ end
+
+ def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack_name}) do
+ new_shortcode = params[:new_shortcode]
+ new_filename = params[:new_filename]
+ force = params[:force]
+
+ with {:ok, pack} <- Pack.load_pack(pack_name),
+ {:ok, pack} <- Pack.update_file(pack, shortcode, new_shortcode, new_filename, force) do
+ json(conn, pack.files)
+ else
+ {:error, :already_exists} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{
+ error:
+ "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
+ })
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: "new_shortcode or new_filename cannot be empty"})
+
+ {:error, _} = error ->
+ handle_error(conn, error, %{
+ pack_name: pack_name,
+ code: shortcode,
+ message: "Unexpected error occurred while updating."
+ })
+ end
+ end
+
+ def delete(conn, %{name: pack_name, shortcode: shortcode}) do
+ with {:ok, pack} <- Pack.load_pack(pack_name),
+ {:ok, pack} <- Pack.delete_file(pack, shortcode) do
+ json(conn, pack.files)
+ else
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: "pack name or shortcode cannot be empty"})
+
+ {:error, _} = error ->
+ handle_error(conn, error, %{
+ pack_name: pack_name,
+ code: shortcode,
+ message: "Unexpected error occurred while deleting emoji file."
+ })
+ end
+ end
+
+ defp handle_error(conn, {:error, :doesnt_exist}, %{code: emoji_code}) do
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{emoji_code}\" does not exist"})
+ end
+
+ defp handle_error(conn, {:error, :enoent}, %{pack_name: pack_name}) do
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "pack \"#{pack_name}\" is not found"})
+ end
+
+ defp handle_error(conn, {:error, error}, opts) do
+ message =
+ [
+ Map.get(opts, :message, "Unexpected error occurred."),
+ Pleroma.Utils.posix_error_message(error)
+ ]
+ |> Enum.join(" ")
+ |> String.trim()
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: message})
+ end
+
+ defp get_filename(%Plug.Upload{filename: filename}), do: filename
+ defp get_filename(url) when is_binary(url), do: Path.basename(url)
+
+ def get_file(%Plug.Upload{} = file), do: {:ok, file}
+
+ def get_file(url) when is_binary(url) do
+ with {:ok, %Tesla.Env{body: body, status: code, headers: headers}}
+ when code in 200..299 <- Pleroma.HTTP.get(url) do
+ path = Plug.Upload.random_file!("emoji")
+
+ content_type =
+ case List.keyfind(headers, "content-type", 0) do
+ {"content-type", value} -> value
+ nil -> nil
+ end
+
+ File.write(path, body)
+
+ {:ok,
+ %Plug.Upload{
+ filename: Path.basename(url),
+ path: path,
+ content_type: content_type
+ }}
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
new file mode 100644
index 0000000..420fea1
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -0,0 +1,230 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Emoji.Pack
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["admin:write"]}
+ when action in [
+ :import_from_filesystem,
+ :remote,
+ :download,
+ :create,
+ :update,
+ :delete
+ ]
+ )
+
+ plug(:skip_auth when action in [:index, :archive, :show])
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
+
+ def remote(conn, params) do
+ with {:ok, packs} <-
+ Pack.list_remote(url: params.url, page_size: params.page_size, page: params.page) do
+ json(conn, packs)
+ else
+ {:error, :not_shareable} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
+ end
+ end
+
+ def index(conn, params) do
+ emoji_path =
+ [:instance, :static_dir]
+ |> Pleroma.Config.get!()
+ |> Path.join("emoji")
+
+ with {:ok, packs, count} <- Pack.list_local(page: params.page, page_size: params.page_size) do
+ json(conn, %{packs: packs, count: count})
+ else
+ {:error, :create_dir, e} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
+
+ {:error, :ls, e} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{
+ error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}"
+ })
+ end
+ end
+
+ def show(conn, %{name: name, page: page, page_size: page_size}) do
+ name = String.trim(name)
+
+ with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do
+ json(conn, pack)
+ else
+ {:error, :enoent} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
+
+ {:error, error} ->
+ error_message =
+ add_posix_error(
+ "Failed to get the contents of the `#{name}` pack.",
+ error
+ )
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: error_message})
+ end
+ end
+
+ def archive(conn, %{name: name}) do
+ with {:ok, archive} <- Pack.get_archive(name) do
+ send_download(conn, {:binary, archive}, filename: "#{name}.zip")
+ else
+ {:error, :cant_download} ->
+ conn
+ |> put_status(:forbidden)
+ |> json(%{
+ error:
+ "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
+ })
+
+ {:error, :enoent} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
+ end
+ end
+
+ def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
+ with {:ok, _pack} <- Pack.download(name, url, params[:as]) do
+ json(conn, "ok")
+ else
+ {:error, :not_shareable} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
+
+ {:error, :invalid_checksum} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
+
+ {:error, error} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: error})
+ end
+ end
+
+ def create(conn, %{name: name}) do
+ name = String.trim(name)
+
+ with {:ok, _pack} <- Pack.create(name) do
+ json(conn, "ok")
+ else
+ {:error, :eexist} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{error: "A pack named \"#{name}\" already exists"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
+
+ {:error, error} ->
+ error_message =
+ add_posix_error(
+ "Unexpected error occurred while creating pack.",
+ error
+ )
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: error_message})
+ end
+ end
+
+ def delete(conn, %{name: name}) do
+ name = String.trim(name)
+
+ with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
+ json(conn, "ok")
+ else
+ {:ok, []} ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "Pack #{name} does not exist"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name cannot be empty"})
+
+ {:error, error, _} ->
+ error_message = add_posix_error("Couldn't delete the `#{name}` pack", error)
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: error_message})
+ end
+ end
+
+ def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
+ with {:ok, pack} <- Pack.update_metadata(name, metadata) do
+ json(conn, pack.pack)
+ else
+ {:error, :incomplete} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "The fallback archive does not have all files specified in pack.json"})
+
+ {:error, error} ->
+ error_message =
+ add_posix_error(
+ "Unexpected error occurred while updating pack metadata.",
+ error
+ )
+
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: error_message})
+ end
+ end
+
+ def import_from_filesystem(conn, _params) do
+ with {:ok, names} <- Pack.import_from_filesystem() do
+ json(conn, names)
+ else
+ {:error, :no_read_write} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Error: emoji pack directory must be writable"})
+
+ {:error, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Error accessing emoji pack directory"})
+ end
+ end
+
+ defp add_posix_error(msg, error) do
+ [msg, Pleroma.Utils.posix_error_message(error)]
+ |> Enum.join(" ")
+ |> String.trim()
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
new file mode 100644
index 0000000..78fd0b2
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
@@ -0,0 +1,94 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Object
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+ when action == :index
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
+ with true <- Pleroma.Config.get([:instance, :show_reactions]),
+ %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
+ %Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
+ Object.normalize(activity, fetch: false) do
+ reactions =
+ reactions
+ |> filter(params)
+ |> filter_allowed_users(user, Map.get(params, :with_muted, false))
+
+ render(conn, "index.json", emoji_reactions: reactions, user: user)
+ else
+ _e -> json(conn, [])
+ end
+ end
+
+ def filter_allowed_users(reactions, user, with_muted) do
+ exclude_ap_ids =
+ if is_nil(user) do
+ []
+ else
+ User.cached_blocked_users_ap_ids(user) ++
+ if not with_muted, do: User.cached_muted_users_ap_ids(user), else: []
+ end
+
+ filter_emoji = fn emoji, users ->
+ case Enum.reject(users, &(&1 in exclude_ap_ids)) do
+ [] -> nil
+ users -> {emoji, users}
+ end
+ end
+
+ reactions
+ |> Stream.map(fn
+ [emoji, users] when is_list(users) -> filter_emoji.(emoji, users)
+ {emoji, users} when is_list(users) -> filter_emoji.(emoji, users)
+ _ -> nil
+ end)
+ |> Stream.reject(&is_nil/1)
+ end
+
+ defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
+ Enum.filter(reactions, fn [e, _] -> e == emoji end)
+ end
+
+ defp filter(reactions, _), do: reactions
+
+ def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+ with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
+ activity = Activity.get_by_id(activity_id)
+
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+ with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
+ activity = Activity.get_by_id(activity_id)
+
+ conn
+ |> put_view(StatusView)
+ |> render("show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex
new file mode 100644
index 0000000..6257e31
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex
@@ -0,0 +1,21 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.InstancesController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Instances
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaInstancesOperation
+
+ def show(conn, _params) do
+ unreachable =
+ Instances.get_consistently_unreachable()
+ |> Map.new(fn {host, date} -> {host, to_string(date)} end)
+
+ json(conn, %{"unreachable" => unreachable})
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
new file mode 100644
index 0000000..66e9d84
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.MascotController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update])
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
+ plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation
+
+ @doc "GET /api/v1/pleroma/mascot"
+ def show(%{assigns: %{user: user}} = conn, _params) do
+ json(conn, User.get_mascot(user))
+ end
+
+ @doc "PUT /api/v1/pleroma/mascot"
+ def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do
+ with {:content_type, "image" <> _} <- {:content_type, file.content_type},
+ {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)) do
+ attachment = render_attachment(object)
+ {:ok, _user} = User.mascot_update(user, attachment)
+
+ json(conn, attachment)
+ else
+ {:content_type, _} ->
+ render_error(conn, :unsupported_media_type, "mascots can only be images")
+ end
+ end
+
+ defp render_attachment(object) do
+ attachment_data = Map.put(object.data, "id", object.id)
+ Pleroma.Web.MastodonAPI.StatusView.render("attachment.json", %{attachment: attachment_data})
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
new file mode 100644
index 0000000..87ea81c
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -0,0 +1,38 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.NotificationController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Notification
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ Pleroma.Web.Plugs.OAuthScopesPlug,
+ %{scopes: ["write:notifications"]} when action == :mark_as_read
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation
+
+ def mark_as_read(%{assigns: %{user: user}, body_params: %{id: notification_id}} = conn, _) do
+ with {:ok, notification} <- Notification.read_one(user, notification_id) do
+ render(conn, "show.json", notification: notification, for: user)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+ end
+ end
+
+ def mark_as_read(%{assigns: %{user: user}, body_params: %{max_id: max_id}} = conn, _) do
+ notifications =
+ user
+ |> Notification.set_read_up_to(max_id)
+ |> Enum.take(80)
+
+ render(conn, "index.json", notifications: notifications, for: user)
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/report_controller.ex b/lib/pleroma/web/pleroma_api/controllers/report_controller.ex
new file mode 100644
index 0000000..1f0a82c
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/report_controller.ex
@@ -0,0 +1,46 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ReportController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.AdminAPI.Report
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read:reports"]})
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaReportOperation
+
+ @doc "GET /api/v0/pleroma/reports"
+ def index(%{assigns: %{user: user}, body_params: params} = conn, _) do
+ params =
+ params
+ |> Map.put(:actor_id, user.ap_id)
+
+ reports = Utils.get_reports(params, Map.get(params, :page, 1), Map.get(params, :size, 20))
+
+ render(conn, "index.json", %{reports: reports, for: user})
+ end
+
+ @doc "GET /api/v0/pleroma/reports/:id"
+ def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ with %Activity{} = report <- Activity.get_report(id),
+ true <- report.actor == user.ap_id,
+ %{} = report_info <- Report.extract_report_info(report) do
+ render(conn, "show.json", Map.put(report_info, :for, user))
+ else
+ false ->
+ {:error, :not_found}
+
+ nil ->
+ {:error, :not_found}
+
+ e ->
+ {:error, inspect(e)}
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
new file mode 100644
index 0000000..bf6dc50
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -0,0 +1,52 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index
+ )
+
+ plug(OAuthScopesPlug, %{scopes: ["write"]} when action == :create)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation
+
+ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
+ with {:ok, activity} <- CommonAPI.listen(user, params) do
+ render(conn, "show.json", activity: activity, for: user)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+ end
+ end
+
+ def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do
+ params = Map.put(params, :type, ["Listen"])
+
+ activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)
+
+ conn
+ |> add_link_headers(activities)
+ |> render("index.json", %{
+ activities: activities,
+ for: reading_user,
+ as: :activity
+ })
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex b/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex
new file mode 100644
index 0000000..1136575
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/settings_controller.ex
@@ -0,0 +1,79 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.SettingsController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:accounts"]} when action in [:update]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:accounts"]} when action in [:show]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaSettingsOperation
+
+ @doc "GET /api/v1/pleroma/settings/:app"
+ def show(%{assigns: %{user: user}} = conn, %{app: app} = _params) do
+ conn
+ |> json(get_settings(user, app))
+ end
+
+ @doc "PATCH /api/v1/pleroma/settings/:app"
+ def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{app: app} = _params) do
+ settings =
+ get_settings(user, app)
+ |> merge_recursively(body_params)
+
+ with changeset <-
+ Pleroma.User.update_changeset(
+ user,
+ %{pleroma_settings_store: %{app => settings}}
+ ),
+ {:ok, _} <- Pleroma.Repo.update(changeset) do
+ conn
+ |> json(settings)
+ end
+ end
+
+ defp merge_recursively(old, %{} = new) do
+ old = ensure_object(old)
+
+ Enum.reduce(
+ new,
+ old,
+ fn
+ {k, nil}, acc ->
+ Map.drop(acc, [k])
+
+ {k, %{} = new_child}, acc ->
+ Map.put(acc, k, merge_recursively(acc[k], new_child))
+
+ {k, v}, acc ->
+ Map.put(acc, k, v)
+ end
+ )
+ end
+
+ defp get_settings(user, app) do
+ user.pleroma_settings_store
+ |> Map.get(app, %{})
+ |> ensure_object()
+ end
+
+ defp ensure_object(%{} = object) do
+ object
+ end
+
+ defp ensure_object(_) do
+ %{}
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
new file mode 100644
index 0000000..e69847b
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex
@@ -0,0 +1,133 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.TwoFactorAuthenticationController do
+ @moduledoc "The module represents actions to manage MFA"
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+
+ alias Pleroma.MFA
+ alias Pleroma.MFA.TOTP
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(OAuthScopesPlug, %{scopes: ["read:security"]} when action in [:settings])
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:security"]} when action in [:setup, :confirm, :disable, :backup_codes]
+ )
+
+ @doc """
+ Gets user multi factor authentication settings
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa
+
+ """
+ def settings(%{assigns: %{user: user}} = conn, _params) do
+ json(conn, %{settings: MFA.mfa_settings(user)})
+ end
+
+ @doc """
+ Prepare setup mfa method
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa/setup/[:method]
+
+ """
+ def setup(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = _params) do
+ with {:ok, user} <- MFA.setup_totp(user),
+ %{secret: secret} = _ <- user.multi_factor_authentication_settings.totp do
+ provisioning_uri = TOTP.provisioning_uri(secret, "#{user.email}")
+
+ json(conn, %{provisioning_uri: provisioning_uri, key: secret})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def setup(conn, _params) do
+ json_response(conn, :bad_request, %{error: "undefined method"})
+ end
+
+ @doc """
+ Confirms setup and enable mfa method
+
+ ## Endpoint
+ POST /api/pleroma/accounts/mfa/confirm/:method
+
+ - params:
+ `code` - confirmation code
+ `password` - current password
+ """
+ def confirm(
+ %{assigns: %{user: user}} = conn,
+ %{"method" => "totp", "password" => _, "code" => _} = params
+ ) do
+ with {:ok, _user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.confirm_totp(user, params) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def confirm(conn, _) do
+ json_response(conn, :bad_request, %{error: "undefined mfa method"})
+ end
+
+ @doc """
+ Disable mfa method and disable mfa if need.
+ """
+ def disable(%{assigns: %{user: user}} = conn, %{"method" => "totp"} = params) do
+ with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.disable_totp(user) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def disable(%{assigns: %{user: user}} = conn, %{"method" => "mfa"} = params) do
+ with {:ok, user} <- Utils.confirm_current_password(user, params["password"]),
+ {:ok, _user} <- MFA.disable(user) do
+ json(conn, %{})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+
+ def disable(conn, _) do
+ json_response(conn, :bad_request, %{error: "undefined mfa method"})
+ end
+
+ @doc """
+ Generates backup codes.
+
+ ## Endpoint
+ GET /api/pleroma/accounts/mfa/backup_codes
+
+ ## Response
+ ### Success
+ `{codes: [codes]}`
+
+ ### Error
+ `{error: [error_message]}`
+
+ """
+ def backup_codes(%{assigns: %{user: user}} = conn, _params) do
+ with {:ok, codes} <- MFA.generate_backup_codes(user) do
+ json(conn, %{codes: codes})
+ else
+ {:error, message} ->
+ json_response(conn, :unprocessable_entity, %{error: message})
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
new file mode 100644
index 0000000..90428a5
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
@@ -0,0 +1,61 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.UserImportController do
+ use Pleroma.Web, :controller
+
+ require Logger
+
+ alias Pleroma.User
+ alias Pleroma.Web.ApiSpec
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} when action == :follow)
+ plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks)
+ plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action == :mutes)
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ defdelegate open_api_operation(action), to: ApiSpec.UserImportOperation
+
+ def follow(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
+ follow(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ end
+
+ def follow(%{assigns: %{user: follower}, body_params: %{list: list}} = conn, _) do
+ identifiers =
+ list
+ |> String.split("\n")
+ |> Enum.map(&(&1 |> String.split(",") |> List.first()))
+ |> List.delete("Account address")
+ |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))
+ |> Enum.reject(&(&1 == ""))
+
+ User.Import.follow_import(follower, identifiers)
+ json(conn, "job started")
+ end
+
+ def blocks(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
+ blocks(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ end
+
+ def blocks(%{assigns: %{user: blocker}, body_params: %{list: list}} = conn, _) do
+ User.Import.blocks_import(blocker, prepare_user_identifiers(list))
+ json(conn, "job started")
+ end
+
+ def mutes(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
+ mutes(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ end
+
+ def mutes(%{assigns: %{user: user}, body_params: %{list: list}} = conn, _) do
+ User.Import.mutes_import(user, prepare_user_identifiers(list))
+ json(conn, "job started")
+ end
+
+ defp prepare_user_identifiers(list) do
+ list
+ |> String.split()
+ |> Enum.map(&String.trim_leading(&1, "@"))
+ end
+end