summaryrefslogtreecommitdiff
path: root/lib/pleroma/web/pleroma_api
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/web/pleroma_api')
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/account_controller.ex102
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/chat_controller.ex179
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex94
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex643
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex304
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex64
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex5
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/notification_controller.ex36
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex199
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex36
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/two_factor_authentication_controller.ex133
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex50
-rw-r--r--lib/pleroma/web/pleroma_api/views/chat_view.ex44
-rw-r--r--lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex33
-rw-r--r--lib/pleroma/web/pleroma_api/views/scrobble_view.ex37
15 files changed, 1017 insertions, 942 deletions
diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
index dcba67d03..563edded7 100644
--- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex
@@ -8,47 +8,47 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
- alias Ecto.Changeset
+ alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
require Pleroma.Constants
plug(
- OAuthScopesPlug,
- %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
+ OpenApiSpex.Plug.PutApiSpec,
+ [module: Pleroma.Web.ApiSpec] when action == :confirmation_resend
)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
plug(
- OAuthScopesPlug,
- %{scopes: ["write:accounts"]}
- # Note: the following actions are not permission-secured in Mastodon:
- when action in [
- :update_avatar,
- :update_banner,
- :update_background
- ]
+ :skip_plug,
+ [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :confirmation_resend
)
- plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
+ )
- # An extra safety measure for possible actions not guarded by OAuth permissions specification
plug(
- Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
- when action != :confirmation_resend
+ OAuthScopesPlug,
+ %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
)
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
+
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+ 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"]
+ nickname_or_email = params[:email] || params[:nickname]
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, _} <- User.try_send_confirmation_email(user) do
@@ -56,62 +56,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
end
end
- @doc "PATCH /api/v1/pleroma/accounts/update_avatar"
- def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- {:ok, user} =
- user
- |> Changeset.change(%{avatar: nil})
- |> User.update_and_set_cache()
-
- CommonAPI.update(user)
-
- json(conn, %{url: nil})
- end
-
- def update_avatar(%{assigns: %{user: user}} = conn, params) do
- {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
- {:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
- %{"url" => [%{"href" => href} | _]} = data
-
- CommonAPI.update(user)
-
- json(conn, %{url: href})
- end
-
- @doc "PATCH /api/v1/pleroma/accounts/update_banner"
- def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
- with {:ok, user} <- User.update_banner(user, %{}) do
- CommonAPI.update(user)
- json(conn, %{url: nil})
- end
- end
-
- def update_banner(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
- {:ok, user} <- User.update_banner(user, object.data) do
- CommonAPI.update(user)
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
- @doc "PATCH /api/v1/pleroma/accounts/update_background"
- def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- with {:ok, _user} <- User.update_background(user, %{}) do
- json(conn, %{url: nil})
- end
- end
-
- def update_background(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(params, type: :background),
- {:ok, _user} <- User.update_background(user, object.data) do
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- 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")
@@ -120,9 +64,9 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
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)
+ |> Map.put(:type, "Create")
+ |> Map.put(:favorited_by, user.ap_id)
+ |> Map.put(:blocking_user, for_user)
recipients =
if for_user do
@@ -139,7 +83,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
conn
|> add_link_headers(activities)
|> put_view(StatusView)
- |> render("index.json", activities: activities, for: for_user, as: :activity)
+ |> render("index.json",
+ activities: activities,
+ for: for_user,
+ as: :activity
+ )
end
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
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 000000000..1f2e953f7
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -0,0 +1,179 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.PleromaAPI.ChatController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.Object
+ alias Pleroma.Pagination
+ alias Pleroma.Plugs.OAuthScopesPlug
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+ alias Pleroma.Web.PleromaAPI.ChatView
+
+ 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, :show]
+ )
+
+ plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+
+ 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 <- cm_ref.chat_id |> to_string(),
+ %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
+ cm_ref
+ |> MessageReference.delete()
+ end
+
+ def post_chat_message(
+ %{body_params: params, assigns: %{user: %{id: user_id} = user}} = conn,
+ %{
+ id: id
+ }
+ ) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_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]
+ ),
+ message <- Object.normalize(activity, false),
+ cm_ref <- MessageReference.for_chat_and_object(chat, message) do
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("show.json", chat_message_reference: cm_ref)
+ 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 <- cm_ref.chat_id |> to_string(),
+ %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: %{id: user_id}}
+ } = conn,
+ %{id: id}
+ ) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id),
+ {_n, _} <-
+ MessageReference.set_all_seen_for_chat(chat, last_read_id) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+
+ def messages(%{assigns: %{user: %{id: user_id}}} = conn, %{id: id} = params) do
+ with %Chat{} = chat <- Repo.get_by(Chat, id: id, user_id: user_id) do
+ cm_refs =
+ chat
+ |> MessageReference.for_chat_query()
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> put_view(MessageReferenceView)
+ |> render("index.json", chat_message_references: cm_refs)
+ else
+ _ ->
+ conn
+ |> put_status(:not_found)
+ |> json(%{error: "not found"})
+ end
+ end
+
+ def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
+ blocked_ap_ids = User.blocked_users_ap_ids(user)
+
+ chats =
+ from(c in Chat,
+ where: c.user_id == ^user_id,
+ where: c.recipient not in ^blocked_ap_ids,
+ order_by: [desc: c.updated_at],
+ inner_join: u in User,
+ on: u.ap_id == c.recipient
+ )
+ |> Repo.all()
+
+ conn
+ |> put_view(ChatView)
+ |> render("index.json", chats: chats)
+ end
+
+ def create(%{assigns: %{user: user}} = conn, params) do
+ with %User{ap_id: recipient} <- User.get_by_id(params[:id]),
+ {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ end
+ end
+
+ def show(%{assigns: %{user: user}} = conn, params) do
+ with %Chat{} = chat <- Repo.get_by(Chat, user_id: user.id, id: params[:id]) do
+ conn
+ |> put_view(ChatView)
+ |> render("show.json", chat: chat)
+ 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 000000000..3d007f324
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
@@ -0,0 +1,94 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 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.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView)
+ 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_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
deleted file mode 100644
index 03e95e020..000000000
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ /dev/null
@@ -1,643 +0,0 @@
-defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
- use Pleroma.Web, :controller
-
- alias Pleroma.Plugs.OAuthScopesPlug
-
- require Logger
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write"], admin: true}
- when action in [
- :create,
- :delete,
- :download_from,
- :list_from,
- :import_from_fs,
- :update_file,
- :update_metadata
- ]
- )
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
- def emoji_dir_path do
- Path.join(
- Pleroma.Config.get!([:instance, :static_dir]),
- "emoji"
- )
- end
-
- @doc """
- Lists packs from the remote instance.
-
- Since JS cannot ask remote instances for their packs due to CPS, it has to
- be done by the server
- """
- def list_from(conn, %{"instance_address" => address}) do
- address = String.trim(address)
-
- if shareable_packs_available(address) do
- list_resp =
- "#{address}/api/pleroma/emoji/packs" |> Tesla.get!() |> Map.get(:body) |> Jason.decode!()
-
- json(conn, list_resp)
- else
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "The requested instance does not support sharing emoji packs"})
- end
- end
-
- @doc """
- Lists the packs available on the instance as JSON.
-
- The information is public and does not require authentication. The format is
- a map of "pack directory name" to pack.json contents.
- """
- def list_packs(conn, _params) do
- # Create the directory first if it does not exist. This is probably the first request made
- # with the API so it should be sufficient
- with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_dir_path())},
- {:ls, {:ok, results}} <- {:ls, File.ls(emoji_dir_path())} do
- pack_infos =
- results
- |> Enum.filter(&has_pack_json?/1)
- |> Enum.map(&load_pack/1)
- # Check if all the files are in place and can be sent
- |> Enum.map(&validate_pack/1)
- # Transform into a map of pack-name => pack-data
- |> Enum.into(%{})
-
- json(conn, pack_infos)
- else
- {:create_dir, {:error, e}} ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "Failed to create the emoji pack directory at #{emoji_dir_path()}: #{e}"})
-
- {:ls, {:error, e}} ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{
- error:
- "Failed to get the contents of the emoji pack directory at #{emoji_dir_path()}: #{e}"
- })
- end
- end
-
- defp has_pack_json?(file) do
- dir_path = Path.join(emoji_dir_path(), file)
- # Filter to only use the pack.json packs
- File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
- end
-
- defp load_pack(pack_name) do
- pack_path = Path.join(emoji_dir_path(), pack_name)
- pack_file = Path.join(pack_path, "pack.json")
-
- {pack_name, Jason.decode!(File.read!(pack_file))}
- end
-
- defp validate_pack({name, pack}) do
- pack_path = Path.join(emoji_dir_path(), name)
-
- if can_download?(pack, pack_path) do
- archive_for_sha = make_archive(name, pack, pack_path)
- archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
-
- pack =
- pack
- |> put_in(["pack", "can-download"], true)
- |> put_in(["pack", "download-sha256"], archive_sha)
-
- {name, pack}
- else
- {name, put_in(pack, ["pack", "can-download"], false)}
- end
- end
-
- defp can_download?(pack, pack_path) do
- # If the pack is set as shared, check if it can be downloaded
- # That means that when asked, the pack can be packed and sent to the remote
- # Otherwise, they'd have to download it from external-src
- pack["pack"]["share-files"] &&
- Enum.all?(pack["files"], fn {_, path} ->
- File.exists?(Path.join(pack_path, path))
- end)
- end
-
- defp create_archive_and_cache(name, pack, pack_dir, md5) do
- files =
- ['pack.json'] ++
- (pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
-
- {:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
-
- cache_seconds_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
- cache_ms = :timer.seconds(cache_seconds_per_file * Enum.count(files))
-
- Cachex.put!(
- :emoji_packs_cache,
- name,
- # if pack.json MD5 changes, the cache is not valid anymore
- %{pack_json_md5: md5, pack_data: zip_result},
- # Add a minute to cache time for every file in the pack
- ttl: cache_ms
- )
-
- Logger.debug("Created an archive for the '#{name}' emoji pack, \
-keeping it in cache for #{div(cache_ms, 1000)}s")
-
- zip_result
- end
-
- defp make_archive(name, pack, pack_dir) do
- # Having a different pack.json md5 invalidates cache
- pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
-
- case Cachex.get!(:emoji_packs_cache, name) do
- %{pack_file_md5: ^pack_file_md5, pack_data: zip_result} ->
- Logger.debug("Using cache for the '#{name}' shared emoji pack")
- zip_result
-
- _ ->
- create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
- end
- end
-
- @doc """
- An endpoint for other instances (via admin UI) or users (via browser)
- to download packs that the instance shares.
- """
- def download_shared(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
- pack_file = Path.join(pack_dir, "pack.json")
-
- with {_, true} <- {:exists?, File.exists?(pack_file)},
- pack = Jason.decode!(File.read!(pack_file)),
- {_, true} <- {:can_download?, can_download?(pack, pack_dir)} do
- zip_result = make_archive(name, pack, pack_dir)
- send_download(conn, {:binary, zip_result}, filename: "#{name}.zip")
- else
- {:can_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"
- })
-
- {:exists?, _} ->
- conn
- |> put_status(:not_found)
- |> json(%{error: "Pack #{name} does not exist"})
- end
- end
-
- defp shareable_packs_available(address) do
- "#{address}/.well-known/nodeinfo"
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> Map.get("links")
- |> List.last()
- |> Map.get("href")
- # Get the actual nodeinfo address and fetch it
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> get_in(["metadata", "features"])
- |> Enum.member?("shareable_emoji_packs")
- end
-
- @doc """
- An admin endpoint to request downloading a pack named `pack_name` from the instance
- `instance_address`.
-
- If the requested instance's admin chose to share the pack, it will be downloaded
- from that instance, otherwise it will be downloaded from the fallback source, if there is one.
- """
- def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
- address = String.trim(address)
-
- if shareable_packs_available(address) do
- full_pack =
- "#{address}/api/pleroma/emoji/packs/list"
- |> Tesla.get!()
- |> Map.get(:body)
- |> Jason.decode!()
- |> Map.get(name)
-
- pack_info_res =
- case full_pack["pack"] do
- %{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
- {:ok,
- %{
- sha: sha,
- uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
- }}
-
- %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
- {:ok,
- %{
- sha: sha,
- uri: src,
- fallback: true
- }}
-
- _ ->
- {:error,
- "The pack was not set as shared and there is no fallback src to download from"}
- end
-
- with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res,
- %{body: emoji_archive} <- Tesla.get!(uri),
- {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
- local_name = data["as"] || name
- pack_dir = Path.join(emoji_dir_path(), local_name)
- File.mkdir_p!(pack_dir)
-
- files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
- # Fallback cannot contain a pack.json file
- files = if pinfo[:fallback], do: files, else: ['pack.json'] ++ files
-
- {:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
-
- # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
- # in it to depend on itself
- if pinfo[:fallback] do
- pack_file_path = Path.join(pack_dir, "pack.json")
-
- File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
- end
-
- json(conn, "ok")
- else
- {:error, e} ->
- conn |> put_status(:internal_server_error) |> json(%{error: e})
-
- {:checksum, _} ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
- end
- else
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "The requested instance does not support sharing emoji packs"})
- end
- end
-
- @doc """
- Creates an empty pack named `name` which then can be updated via the admin UI.
- """
- def create(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
-
- if not File.exists?(pack_dir) do
- File.mkdir_p!(pack_dir)
-
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- File.write!(
- pack_file_p,
- Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
- )
-
- conn |> json("ok")
- else
- conn
- |> put_status(:conflict)
- |> json(%{error: "A pack named \"#{name}\" already exists"})
- end
- end
-
- @doc """
- Deletes the pack `name` and all it's files.
- """
- def delete(conn, %{"name" => name}) do
- pack_dir = Path.join(emoji_dir_path(), name)
-
- case File.rm_rf(pack_dir) do
- {:ok, _} ->
- conn |> json("ok")
-
- {:error, _, _} ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: "Couldn't delete the pack #{name}"})
- end
- end
-
- @doc """
- An endpoint to update `pack_names`'s metadata.
-
- `new_data` is the new metadata for the pack, that will replace the old metadata.
- """
- def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
- pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"])
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- # The new fallback-src is in the new data and it's not the same as it was in the old data
- should_update_fb_sha =
- not is_nil(new_data["fallback-src"]) and
- new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
-
- with {_, true} <- {:should_update?, should_update_fb_sha},
- %{body: pack_arch} <- Tesla.get!(new_data["fallback-src"]),
- {:ok, flist} <- :zip.unzip(pack_arch, [:memory]),
- {_, true} <- {:has_all_files?, has_all_files?(full_pack, flist)} do
- fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
-
- new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha)
- update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
- else
- {:should_update?, _} ->
- update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
-
- {:has_all_files?, _} ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "The fallback archive does not have all files specified in pack.json"})
- end
- end
-
- # Check if all files from the pack.json are in the archive
- defp has_all_files?(%{"files" => files}, flist) do
- Enum.all?(files, fn {_, from_manifest} ->
- Enum.find(flist, fn {from_archive, _} ->
- to_string(from_archive) == from_manifest
- end)
- end)
- end
-
- defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
- full_pack = Map.put(full_pack, "pack", new_data)
- File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))
-
- # Send new data back with fallback sha filled
- json(conn, new_data)
- end
-
- defp get_filename(%{"filename" => filename}), do: filename
-
- defp get_filename(%{"file" => file}) do
- case file do
- %Plug.Upload{filename: filename} -> filename
- url when is_binary(url) -> Path.basename(url)
- end
- end
-
- defp empty?(str), do: String.trim(str) == ""
-
- defp update_file_and_send(conn, updated_full_pack, pack_file_p) do
- # Write the emoji pack file
- File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
-
- # Return the modified file list
- json(conn, updated_full_pack["files"])
- end
-
- @doc """
- Updates a file in a pack.
-
- Updating can mean three things:
-
- - `add` adds an emoji named `shortcode` to the pack `pack_name`,
- that means that the emoji file needs to be uploaded with the request
- (thus requiring it to be a multipart request) and be named `file`.
- There can also be an optional `filename` that will be the new emoji file name
- (if it's not there, the name will be taken from the uploaded file).
- - `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
- (from the current filename to `new_filename`)
- - `remove` removes the emoji named `shortcode` and it's associated file
- """
-
- # Add
- def update_file(
- conn,
- %{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
- ) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
- filename <- get_filename(params),
- false <- empty?(shortcode),
- false <- empty?(filename) do
- file_path = Path.join(pack_dir, filename)
-
- # If the name contains directories, create them
- if String.contains?(file_path, "/") do
- File.mkdir_p!(Path.dirname(file_path))
- end
-
- case params["file"] do
- %Plug.Upload{path: upload_path} ->
- # Copy the uploaded file from the temporary directory
- File.copy!(upload_path, file_path)
-
- url when is_binary(url) ->
- # Download and write the file
- file_contents = Tesla.get!(url).body
- File.write!(file_path, file_contents)
- end
-
- updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
- update_file_and_send(conn, updated_full_pack, pack_file_p)
- else
- {:has_shortcode, _} ->
- conn
- |> put_status(:conflict)
- |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
-
- true ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "shortcode or filename cannot be empty"})
- end
- end
-
- # Remove
- def update_file(conn, %{
- "pack_name" => pack_name,
- "action" => "remove",
- "shortcode" => shortcode
- }) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- if Map.has_key?(full_pack["files"], shortcode) do
- {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
-
- emoji_file_path = Path.join(pack_dir, emoji_file_path)
-
- # Delete the emoji file
- File.rm!(emoji_file_path)
-
- # If the old directory has no more files, remove it
- if String.contains?(emoji_file_path, "/") do
- dir = Path.dirname(emoji_file_path)
-
- if Enum.empty?(File.ls!(dir)) do
- File.rmdir!(dir)
- end
- end
-
- update_file_and_send(conn, updated_full_pack, pack_file_p)
- else
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
- end
- end
-
- # Update
- def update_file(
- conn,
- %{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
- ) do
- pack_dir = Path.join(emoji_dir_path(), pack_name)
- pack_file_p = Path.join(pack_dir, "pack.json")
-
- full_pack = Jason.decode!(File.read!(pack_file_p))
-
- with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
- %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params,
- false <- empty?(new_shortcode),
- false <- empty?(new_filename) do
- # First, remove the old shortcode, saving the old path
- {old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
- old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
- new_emoji_file_path = Path.join(pack_dir, new_filename)
-
- # If the name contains directories, create them
- if String.contains?(new_emoji_file_path, "/") do
- File.mkdir_p!(Path.dirname(new_emoji_file_path))
- end
-
- # Move/Rename the old filename to a new filename
- # These are probably on the same filesystem, so just rename should work
- :ok = File.rename(old_emoji_file_path, new_emoji_file_path)
-
- # If the old directory has no more files, remove it
- if String.contains?(old_emoji_file_path, "/") do
- dir = Path.dirname(old_emoji_file_path)
-
- if Enum.empty?(File.ls!(dir)) do
- File.rmdir!(dir)
- end
- end
-
- # Then, put in the new shortcode with the new path
- updated_full_pack = put_in(updated_full_pack, ["files", new_shortcode], new_filename)
- update_file_and_send(conn, updated_full_pack, pack_file_p)
- else
- {:has_shortcode, _} ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
-
- true ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "new_shortcode or new_filename cannot be empty"})
-
- _ ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: "new_shortcode or new_file were not specified"})
- end
- end
-
- def update_file(conn, %{"action" => action}) do
- conn
- |> put_status(:bad_request)
- |> json(%{error: "Unknown action: #{action}"})
- end
-
- @doc """
- Imports emoji from the filesystem.
-
- Importing means checking all the directories in the
- `$instance_static/emoji/` for directories which do not have
- `pack.json`. If one has an emoji.txt file, that file will be used
- to create a `pack.json` file with it's contents. If the directory has
- neither, all the files with specific configured extenstions will be
- assumed to be emojis and stored in the new `pack.json` file.
- """
- def import_from_fs(conn, _params) do
- emoji_path = emoji_dir_path()
-
- with {:ok, %{access: :read_write}} <- File.stat(emoji_path),
- {:ok, results} <- File.ls(emoji_path) do
- imported_pack_names =
- results
- |> Enum.filter(fn file ->
- dir_path = Path.join(emoji_path, file)
- # Find the directories that do NOT have pack.json
- File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
- end)
- |> Enum.map(&write_pack_json_contents/1)
-
- json(conn, imported_pack_names)
- else
- {:ok, %{access: _}} ->
- 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 write_pack_json_contents(dir) do
- dir_path = Path.join(emoji_dir_path(), dir)
- emoji_txt_path = Path.join(dir_path, "emoji.txt")
-
- files_for_pack = files_for_pack(emoji_txt_path, dir_path)
- pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
-
- File.write!(Path.join(dir_path, "pack.json"), pack_json_contents)
-
- dir
- end
-
- defp files_for_pack(emoji_txt_path, dir_path) do
- if File.exists?(emoji_txt_path) do
- # There's an emoji.txt file, it's likely from a pack installed by the pack manager.
- # Make a pack.json file from the contents of that emoji.txt fileh
-
- # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
-
- # Create a map of shortcodes to filenames from emoji.txt
- File.read!(emoji_txt_path)
- |> String.split("\n")
- |> Enum.map(&String.trim/1)
- |> Enum.map(fn line ->
- case String.split(line, ~r/,\s*/) do
- # This matches both strings with and without tags
- # and we don't care about tags here
- [name, file | _] -> {name, file}
- _ -> nil
- end
- end)
- |> Enum.filter(fn x -> not is_nil(x) end)
- |> Enum.into(%{})
- else
- # If there's no emoji.txt, assume all files
- # that are of certain extensions from the config are emojis and import them all
- pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
- Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions)
- 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 000000000..657f46324
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -0,0 +1,304 @@
+defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Emoji.Pack
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ Pleroma.Plugs.OAuthScopesPlug,
+ %{scopes: ["write"], admin: true}
+ when action in [
+ :import_from_filesystem,
+ :remote,
+ :download,
+ :create,
+ :update,
+ :delete,
+ :add_file,
+ :update_file,
+ :delete_file
+ ]
+ )
+
+ @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug]
+ plug(:skip_plug, @skip_plugs when action in [:index, :show, :archive])
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
+
+ def remote(conn, %{url: url}) do
+ with {:ok, packs} <- Pack.list_remote(url) 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, :not_found} ->
+ 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"})
+ 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, :not_found} ->
+ 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, e} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: e})
+ 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, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while creating pack."
+ )
+ 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, _, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Couldn't delete the pack #{name}"})
+ 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, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while updating pack metadata."
+ )
+ end
+ end
+
+ def add_file(%{body_params: params} = conn, %{name: name}) do
+ filename = params[:filename] || get_filename(params[:file])
+ shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename))
+
+ with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[: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, :not_found} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack \"#{name}\" is not found"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name, shortcode or filename cannot be empty"})
+
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while adding file to pack."
+ )
+ end
+ end
+
+ def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do
+ new_shortcode = params[:new_shortcode]
+ new_filename = params[:new_filename]
+ force = params[:force]
+
+ with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
+ json(conn, pack.files)
+ else
+ {:error, :doesnt_exist} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
+
+ {: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, :not_found} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack \"#{name}\" is not found"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "new_shortcode or new_filename cannot be empty"})
+
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while updating file in pack."
+ )
+ end
+ end
+
+ def delete_file(conn, %{name: name, shortcode: shortcode}) do
+ with {:ok, pack} <- Pack.delete_file(name, shortcode) do
+ json(conn, pack.files)
+ else
+ {:error, :doesnt_exist} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
+
+ {:error, :not_found} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack \"#{name}\" is not found"})
+
+ {:error, :empty_values} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "pack name or shortcode cannot be empty"})
+
+ {:error, _} ->
+ render_error(
+ conn,
+ :internal_server_error,
+ "Unexpected error occurred while removing file from pack."
+ )
+ 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 get_filename(%Plug.Upload{filename: filename}), do: filename
+ defp get_filename(url) when is_binary(url), do: Path.basename(url)
+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 000000000..7f9254c13
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 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.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ 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) do
+ reactions = filter(reactions, params)
+ render(conn, "index.json", emoji_reactions: reactions, user: user)
+ else
+ _e -> json(conn, [])
+ end
+ 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/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
index d9c1c8636..df6c50ca5 100644
--- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -9,10 +9,11 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation
@doc "GET /api/v1/pleroma/mascot"
def show(%{assigns: %{user: user}} = conn, _params) do
@@ -20,7 +21,7 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
end
@doc "PUT /api/v1/pleroma/mascot"
- def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do
+ def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
# Reject if not an image
%{type: "image"} = attachment <- render_attachment(object) do
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 000000000..3ed8bd294
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 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
+ alias Pleroma.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read)
+ plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView)
+
+ 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/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
deleted file mode 100644
index f0867c2c1..000000000
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ /dev/null
@@ -1,199 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
- use Pleroma.Web, :controller
-
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
-
- alias Pleroma.Activity
- alias Pleroma.Conversation.Participation
- alias Pleroma.Notification
- alias Pleroma.Object
- alias Pleroma.Plugs.OAuthScopesPlug
- alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.AccountView
- alias Pleroma.Web.MastodonAPI.ConversationView
- alias Pleroma.Web.MastodonAPI.NotificationView
- alias Pleroma.Web.MastodonAPI.StatusView
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["read:statuses"]}
- when action in [:conversation, :conversation_statuses]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:statuses"]}
- when action in [:react_with_emoji, :unreact_with_emoji]
- )
-
- plug(
- OAuthScopesPlug,
- %{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations]
- )
-
- plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)
-
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
-
- def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
- %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <-
- Object.normalize(activity) do
- reactions =
- emoji_reactions
- |> Enum.map(fn [emoji, user_ap_ids] ->
- if params["emoji"] && params["emoji"] != emoji do
- nil
- else
- users =
- Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
- |> Enum.filter(fn
- %{deactivated: false} -> true
- _ -> false
- end)
-
- %{
- name: emoji,
- count: length(users),
- accounts: AccountView.render("index.json", %{users: users, for: user, as: :user}),
- me: !!(user && user.ap_id in user_ap_ids)
- }
- end
- end)
- |> Enum.filter(& &1)
-
- conn
- |> json(reactions)
- else
- _e ->
- conn
- |> json([])
- end
- end
-
- def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
- with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
- activity <- Activity.get_by_id(activity_id) do
- conn
- |> put_view(StatusView)
- |> render("show.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{
- "id" => activity_id,
- "emoji" => emoji
- }) do
- with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji),
- activity <- Activity.get_by_id(activity_id) do
- conn
- |> put_view(StatusView)
- |> render("show.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
- with %Participation{} = participation <- Participation.get(participation_id),
- true <- user.id == participation.user_id do
- conn
- |> put_view(ConversationView)
- |> render("participation.json", %{participation: participation, for: user})
- else
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def conversation_statuses(
- %{assigns: %{user: user}} = conn,
- %{"id" => participation_id} = params
- ) do
- with %Participation{} = participation <-
- Participation.get(participation_id, preload: [:conversation]),
- true <- user.id == participation.user_id 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(params)
- |> Enum.reverse()
-
- conn
- |> add_link_headers(activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- else
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def update_conversation(
- %{assigns: %{user: user}} = conn,
- %{"id" => participation_id, "recipients" => recipients}
- ) do
- with %Participation{} = participation <- Participation.get(participation_id),
- true <- user.id == participation.user_id,
- {:ok, participation} <- Participation.set_recipients(participation, recipients) do
- conn
- |> put_view(ConversationView)
- |> render("participation.json", %{participation: participation, for: user})
- else
- {:error, message} ->
- conn
- |> put_status(:bad_request)
- |> json(%{"error" => message})
-
- _error ->
- conn
- |> put_status(404)
- |> json(%{"error" => "Unknown conversation id"})
- end
- end
-
- def read_conversations(%{assigns: %{user: user}} = conn, _params) do
- with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
- conn
- |> add_link_headers(participations)
- |> put_view(ConversationView)
- |> render("participations.json", participations: participations, for: user)
- end
- end
-
- def read_notification(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
- with {:ok, notification} <- Notification.read_one(user, notification_id) do
- conn
- |> put_view(NotificationView)
- |> render("show.json", %{notification: notification, for: user})
- else
- {:error, message} ->
- conn
- |> put_status(:bad_request)
- |> json(%{"error" => message})
- end
- end
-
- def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do
- with notifications <- Notification.set_read_up_to(user, max_id) do
- notifications = Enum.take(notifications, 80)
-
- conn
- |> put_view(NotificationView)
- |> render("index.json", %{notifications: notifications, for: user})
- 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
index 4463ec477..e9a4fba92 100644
--- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
@@ -5,32 +5,27 @@
defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
use Pleroma.Web, :controller
- import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2]
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.StatusView
- plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles)
- plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index
+ )
- def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
- params =
- if !params["length"] do
- params
- else
- params
- |> Map.put("length", fetch_integer_param(params, "length"))
- end
+ 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
- conn
- |> put_view(StatusView)
- |> render("listen.json", %{activity: activity, for: user})
+ render(conn, "show.json", activity: activity, for: user)
else
{:error, message} ->
conn
@@ -39,16 +34,15 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
end
end
- def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
- params = Map.put(params, "type", ["Listen"])
+ 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)
- |> put_view(StatusView)
- |> render("listens.json", %{
+ |> render("index.json", %{
activities: activities,
for: reading_user,
as: :activity
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 000000000..b86791d09
--- /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-2020 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.Plugs.OAuthScopesPlug
+ alias Pleroma.Web.CommonAPI.Utils
+
+ 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/views/chat/message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
new file mode 100644
index 000000000..d4e08b50d
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
@@ -0,0 +1,50 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render(
+ "show.json",
+ %{
+ chat_message_reference: %{
+ id: id,
+ object: %{data: chat_message} = object,
+ chat_id: chat_id,
+ unread: unread
+ }
+ }
+ ) do
+ %{
+ id: id |> to_string(),
+ content: chat_message["content"],
+ chat_id: chat_id |> to_string(),
+ account_id: User.get_cached_by_ap_id(chat_message["actor"]).id,
+ created_at: Utils.to_masto_date(chat_message["published"]),
+ emojis: StatusView.build_emojis(chat_message["emoji"]),
+ attachment:
+ chat_message["attachment"] &&
+ StatusView.render("attachment.json", attachment: chat_message["attachment"]),
+ unread: unread,
+ card:
+ StatusView.render(
+ "card.json",
+ Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
+ )
+ }
+ end
+
+ def render("index.json", opts) do
+ render_many(
+ opts[:chat_message_references],
+ __MODULE__,
+ "show.json",
+ Map.put(opts, :as, :chat_message_reference)
+ )
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat_view.ex b/lib/pleroma/web/pleroma_api/views/chat_view.ex
new file mode 100644
index 000000000..04dc20d51
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/chat_view.ex
@@ -0,0 +1,44 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ChatView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Chat
+ alias Pleroma.Chat.MessageReference
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
+
+ def render("show.json", %{chat: %Chat{} = chat} = opts) do
+ recipient = User.get_cached_by_ap_id(chat.recipient)
+ last_message = opts[:last_message] || MessageReference.last_message_for_chat(chat)
+ account_view_opts = account_view_opts(opts, recipient)
+
+ %{
+ id: chat.id |> to_string(),
+ account: AccountView.render("show.json", account_view_opts),
+ unread: MessageReference.unread_count_for_chat(chat),
+ last_message:
+ last_message &&
+ MessageReferenceView.render("show.json", chat_message_reference: last_message),
+ updated_at: Utils.to_masto_date(chat.updated_at)
+ }
+ end
+
+ def render("index.json", %{chats: chats} = opts) do
+ render_many(chats, __MODULE__, "show.json", Map.delete(opts, :chats))
+ end
+
+ defp account_view_opts(opts, recipient) do
+ account_view_opts = Map.put(opts, :user, recipient)
+
+ if Map.has_key?(account_view_opts, :for) do
+ account_view_opts
+ else
+ Map.put(account_view_opts, :skip_visibility_check, true)
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
new file mode 100644
index 000000000..e0f98b50a
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.Web.MastodonAPI.AccountView
+
+ def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do
+ render_many(emoji_reactions, __MODULE__, "show.json", opts)
+ end
+
+ def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
+ users = fetch_users(user_ap_ids)
+
+ %{
+ name: emoji,
+ count: length(users),
+ accounts: render(AccountView, "index.json", users: users, for: user),
+ me: !!(user && user.ap_id in user_ap_ids)
+ }
+ end
+
+ defp fetch_users(user_ap_ids) do
+ user_ap_ids
+ |> Enum.map(&Pleroma.User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn
+ %{deactivated: false} -> true
+ _ -> false
+ end)
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
new file mode 100644
index 000000000..bbff93abe
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex
@@ -0,0 +1,37 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
+ use Pleroma.Web, :view
+
+ require Pleroma.Constants
+
+ alias Pleroma.Activity
+ alias Pleroma.HTML
+ alias Pleroma.Object
+ alias Pleroma.Web.CommonAPI.Utils
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
+ object = Object.normalize(activity)
+
+ user = StatusView.get_user(activity.data["actor"])
+ created_at = Utils.to_masto_date(activity.data["published"])
+
+ %{
+ id: activity.id,
+ account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ created_at: created_at,
+ title: object.data["title"] |> HTML.strip_tags(),
+ artist: object.data["artist"] |> HTML.strip_tags(),
+ album: object.data["album"] |> HTML.strip_tags(),
+ length: object.data["length"]
+ }
+ end
+
+ def render("index.json", opts) do
+ safe_render_many(opts.activities, __MODULE__, "show.json", opts)
+ end
+end