Compare commits

..

11 Commits

Author SHA1 Message Date
Your New SJW Waifu 02827a1741 Merge remote-tracking branch 'upstream/develop' into neckbeard 2023-03-16 14:25:56 -05:00
tusooa bf9db78426 Merge branch 'docs-otp-support' into 'develop'
docs: Be more explicit about the level of compatibility of OTP releases

See merge request pleroma/pleroma!3849
2023-03-16 08:11:44 +00:00
Haelwenn 353538d16c Merge branch 'pleroma-akkoma-emoji-port' into 'develop'
Custom emoji reactions support

See merge request pleroma/pleroma!3845
2023-03-16 08:00:00 +00:00
Haelwenn c3600b6104 Merge branch 'feat/fields-rel-me-tag' into 'develop'
feat: build rel me tags with profile fields

See merge request pleroma/pleroma!3850
2023-03-16 07:53:27 +00:00
kPherox 83c7415803
fix: append field values to bio before parsing 2023-03-15 23:55:24 +09:00
Alexander Tumin 2c2ea16b50 Allow custom emoji reactions: Add pleroma_custom_emoji_reactions feature, review changes 2023-03-12 11:39:17 +03:00
Haelwenn (lanodan) Monnier 8e072baed0 docs: Be more explicit about the level of compatibility of OTP releases 2023-03-05 08:55:18 +01:00
Alexander Tumin 8d3b29aaba Allow custom emoji reactions: add test for mixed emoji react, fix credo errors 2023-03-02 11:18:16 +03:00
Alexander Tumin 4b85d1c617 Allow custom emoji reactions: Fix tests, mixed custom and unicode reactions 2023-03-02 11:18:16 +03:00
floatingghost 787e30c5fd Allow reacting with remote emoji when they exist on the post (#200)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/200
2023-03-02 11:18:16 +03:00
kPherox d5d7648789
feat: build rel me tags with profile fields 2023-02-18 17:57:41 +09:00
19 changed files with 668 additions and 69 deletions

View File

@ -2,15 +2,16 @@
{! backend/installation/otp_vs_from_source.include !} {! backend/installation/otp_vs_from_source.include !}
This guide covers a installation using an OTP release. To install Pleroma from source, please check out the corresponding guide for your distro. This guide covers a installation using OTP releases as built by the Pleroma project, it is meant as a fallback to distribution packages/recipes which are the preferred installation method.
To install Pleroma from source, please check out the corresponding guide for your distro.
## Pre-requisites ## Pre-requisites
* A machine running Linux with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and `x86_64`, `aarch64` or `armv7l` CPU, you have root access to. If you are not sure if it's compatible see [Detecting flavour section](#detecting-flavour) below * A machine you have root access to running Debian GNU/Linux or compatible (eg. Ubuntu), or Alpine on `x86_64`, `aarch64` or `armv7l` CPU. If you are not sure what you are running see [Detecting flavour section](#detecting-flavour) below
* A (sub)domain pointed to the machine * A (sub)domain pointed to the machine
You will be running commands as root. If you aren't root already, please elevate your privileges by executing `sudo su`/`su`. You will be running commands as root. If you aren't root already, please elevate your privileges by executing `sudo -i`/`su`.
While in theory OTP releases are possbile to install on any compatible machine, for the sake of simplicity this guide focuses only on Debian/Ubuntu and Alpine. Similarly to other binaries, OTP releases tend to be only compatible with the distro they are built on, as such this guide focuses only on Debian/Ubuntu and Alpine.
### Detecting flavour ### Detecting flavour
@ -19,7 +20,7 @@ Paste the following into the shell:
arch="$(uname -m)";if [ "$arch" = "x86_64" ];then arch="amd64";elif [ "$arch" = "armv7l" ];then arch="arm";elif [ "$arch" = "aarch64" ];then arch="arm64";else echo "Unsupported arch: $arch">&2;fi;if getconf GNU_LIBC_VERSION>/dev/null;then libc_postfix="";elif [ "$(ldd 2>&1|head -c 9)" = "musl libc" ];then libc_postfix="-musl";elif [ "$(find /lib/libc.musl*|wc -l)" ];then libc_postfix="-musl";else echo "Unsupported libc">&2;fi;echo "$arch$libc_postfix" arch="$(uname -m)";if [ "$arch" = "x86_64" ];then arch="amd64";elif [ "$arch" = "armv7l" ];then arch="arm";elif [ "$arch" = "aarch64" ];then arch="arm64";else echo "Unsupported arch: $arch">&2;fi;if getconf GNU_LIBC_VERSION>/dev/null;then libc_postfix="";elif [ "$(ldd 2>&1|head -c 9)" = "musl libc" ];then libc_postfix="-musl";elif [ "$(find /lib/libc.musl*|wc -l)" ];then libc_postfix="-musl";else echo "Unsupported libc">&2;fi;echo "$arch$libc_postfix"
``` ```
If your platform is supported the output will contain the flavour string, you will need it later. If not, this just means that we don't build releases for your platform, you can still try installing from source. This should give your flavour string. If not this just means that we don't build releases for your platform, you can still try installing from source.
### Installing the required packages ### Installing the required packages

View File

@ -51,6 +51,8 @@ defmodule Pleroma.Emoji do
@doc "Returns the path of the emoji `name`." @doc "Returns the path of the emoji `name`."
@spec get(String.t()) :: String.t() | nil @spec get(String.t()) :: String.t() | nil
def get(name) do def get(name) do
name = maybe_strip_name(name)
case :ets.lookup(@ets, name) do case :ets.lookup(@ets, name) do
[{_, path}] -> path [{_, path}] -> path
_ -> nil _ -> nil
@ -139,6 +141,57 @@ defmodule Pleroma.Emoji do
def is_unicode_emoji?(_), do: false def is_unicode_emoji?(_), do: false
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
def is_custom_emoji?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s)
def is_custom_emoji?(_), do: false
def maybe_strip_name(name) when is_binary(name), do: String.trim(name, ":")
def maybe_strip_name(name), do: name
def maybe_quote(name) when is_binary(name) do
if is_unicode_emoji?(name) do
name
else
if String.starts_with?(name, ":") do
name
else
":#{name}:"
end
end
end
def maybe_quote(name), do: name
def emoji_url(%{"type" => "EmojiReact", "content" => _, "tag" => []}), do: nil
def emoji_url(%{"type" => "EmojiReact", "content" => emoji, "tag" => tags}) do
emoji = maybe_strip_name(emoji)
tag =
tags
|> Enum.find(fn tag ->
tag["type"] == "Emoji" && !is_nil(tag["name"]) && tag["name"] == emoji
end)
if is_nil(tag) do
nil
else
tag
|> Map.get("icon")
|> Map.get("url")
end
end
def emoji_url(_), do: nil
def emoji_name_with_instance(name, url) do
url = url |> URI.parse() |> Map.get(:host)
"#{name}@#{url}"
end
emoji_qualification_map = emoji_qualification_map =
emojis emojis
|> Enum.filter(&String.contains?(&1, "\uFE0F")) |> Enum.filter(&String.contains?(&1, "\uFE0F"))

View File

@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI.ActivityDraft alias Pleroma.Web.CommonAPI.ActivityDraft
alias Pleroma.Web.Endpoint
require Pleroma.Constants require Pleroma.Constants
@ -54,13 +55,87 @@ defmodule Pleroma.Web.ActivityPub.Builder do
{:ok, data, []} {:ok, data, []}
end end
defp unicode_emoji_react(_object, data, emoji) do
data
|> Map.put("content", emoji)
|> Map.put("type", "EmojiReact")
end
defp add_emoji_content(data, emoji, url) do
tag = [
%{
"id" => url,
"type" => "Emoji",
"name" => Emoji.maybe_quote(emoji),
"icon" => %{
"type" => "Image",
"url" => url
}
}
]
data
|> Map.put("content", Emoji.maybe_quote(emoji))
|> Map.put("type", "EmojiReact")
|> Map.put("tag", tag)
end
defp remote_custom_emoji_react(
%{data: %{"reactions" => existing_reactions}},
data,
emoji
) do
[emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
matching_reaction =
Enum.find(
existing_reactions,
fn [name, _, url] ->
if url != nil do
url = URI.parse(url)
url.host == instance && name == emoji_code
end
end
)
if matching_reaction do
[name, _, url] = matching_reaction
add_emoji_content(data, name, url)
else
{:error, "Could not react"}
end
end
defp remote_custom_emoji_react(_object, _data, _emoji) do
{:error, "Could not react"}
end
defp local_custom_emoji_react(data, emoji) do
with %{file: path} = emojo <- Emoji.get(emoji) do
url = "#{Endpoint.url()}#{path}"
add_emoji_content(data, emojo.code, url)
else
_ -> {:error, "Emoji does not exist"}
end
end
defp custom_emoji_react(object, data, emoji) do
if String.contains?(emoji, "@") do
remote_custom_emoji_react(object, data, emoji)
else
local_custom_emoji_react(data, emoji)
end
end
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()} @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
def emoji_react(actor, object, emoji) do def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do with {:ok, data, meta} <- object_action(actor, object) do
data = data =
data if Emoji.is_unicode_emoji?(emoji) do
|> Map.put("content", emoji) unicode_emoji_react(object, data, emoji)
|> Map.put("type", "EmojiReact") else
custom_emoji_react(object, data, emoji)
end
{:ok, data, meta} {:ok, data, meta}
end end

View File

@ -5,8 +5,10 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Emoji
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
import Ecto.Changeset import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@ -19,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields
message_fields() message_fields()
activity_fields() activity_fields()
embeds_many(:tag, TagValidator)
end end
end end
@ -43,7 +46,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
def changeset(struct, data) do def changeset(struct, data) do
struct struct
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields) -- [:tag])
|> cast_embed(:tag)
end end
defp fix(data) do defp fix(data) do
@ -53,12 +57,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|> CommonFixes.fix_actor() |> CommonFixes.fix_actor()
|> CommonFixes.fix_activity_addressing() |> CommonFixes.fix_activity_addressing()
with %Object{} = object <- Object.normalize(data["object"]) do data = Map.put_new(data, "tag", [])
data
|> CommonFixes.fix_activity_context(object) case Object.normalize(data["object"]) do
|> CommonFixes.fix_object_action_recipients(object) %Object{} = object ->
else data
_ -> data |> CommonFixes.fix_activity_context(object)
|> CommonFixes.fix_object_action_recipients(object)
_ ->
data
end end
end end
@ -82,11 +90,31 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
defp validate_emoji(cng) do defp validate_emoji(cng) do
content = get_field(cng, :content) content = get_field(cng, :content)
if Pleroma.Emoji.is_unicode_emoji?(content) do if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(content) do
cng cng
else else
cng cng
|> add_error(:content, "must be a single character emoji") |> add_error(:content, "is not a valid emoji")
end
end
defp maybe_validate_tag_presence(cng) do
content = get_field(cng, :content)
if Emoji.is_unicode_emoji?(content) do
cng
else
tag = get_field(cng, :tag)
emoji_name = Emoji.maybe_strip_name(content)
case tag do
[%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] ->
cng
_ ->
cng
|> add_error(:tag, "does not contain an Emoji tag")
end
end end
end end
@ -97,5 +125,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|> validate_actor_presence() |> validate_actor_presence()
|> validate_object_presence() |> validate_object_presence()
|> validate_emoji() |> validate_emoji()
|> maybe_validate_tag_presence()
end end
end end

View File

@ -325,21 +325,29 @@ defmodule Pleroma.Web.ActivityPub.Utils do
{:ok, Object.t()} | {:error, Ecto.Changeset.t()} {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_emoji_reaction_to_object( def add_emoji_reaction_to_object(
%Activity{data: %{"content" => emoji, "actor" => actor}}, %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
object object
) do ) do
reactions = get_cached_emoji_reactions(object) reactions = get_cached_emoji_reactions(object)
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
url = maybe_emoji_url(emoji, activity)
new_reactions = new_reactions =
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
if is_nil(candidate_url) do
emoji == candidate
else
url == candidate_url
end
end) do
nil -> nil ->
reactions ++ [[emoji, [actor]]] reactions ++ [[emoji, [actor], url]]
index -> index ->
List.update_at( List.update_at(
reactions, reactions,
index, index,
fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
) )
end end
@ -348,18 +356,40 @@ defmodule Pleroma.Web.ActivityPub.Utils do
update_element_in_object("reaction", new_reactions, object, count) update_element_in_object("reaction", new_reactions, object, count)
end end
defp maybe_emoji_url(
name,
%Activity{
data: %{
"tag" => [
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
]
}
}
),
do: url
defp maybe_emoji_url(_, _), do: nil
def emoji_count(reactions_list) do def emoji_count(reactions_list) do
Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end) Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
end end
def remove_emoji_reaction_from_object( def remove_emoji_reaction_from_object(
%Activity{data: %{"content" => emoji, "actor" => actor}}, %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
object object
) do ) do
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
reactions = get_cached_emoji_reactions(object) reactions = get_cached_emoji_reactions(object)
url = maybe_emoji_url(emoji, activity)
new_reactions = new_reactions =
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
if is_nil(candidate_url) do
emoji == candidate
else
url == candidate_url
end
end) do
nil -> nil ->
reactions reactions
@ -367,9 +397,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do
List.update_at( List.update_at(
reactions, reactions,
index, index,
fn [emoji, users] -> [emoji, List.delete(users, actor)] end fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
) )
|> Enum.reject(fn [_, users] -> Enum.empty?(users) end) |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
end end
count = emoji_count(new_reactions) count = emoji_count(new_reactions)
@ -489,17 +519,37 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
%{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id) %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
emoji = Pleroma.Emoji.maybe_quote(emoji)
"EmojiReact" "EmojiReact"
|> Activity.Queries.by_type() |> Activity.Queries.by_type()
|> where(actor: ^ap_id) |> where(actor: ^ap_id)
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) |> custom_emoji_discriminator(emoji)
|> Activity.Queries.by_object_id(object_ap_id) |> Activity.Queries.by_object_id(object_ap_id)
|> order_by([activity], fragment("? desc nulls last", activity.id)) |> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1) |> limit(1)
|> Repo.one() |> Repo.one()
end end
defp custom_emoji_discriminator(query, emoji) do
if String.contains?(emoji, "@") do
stripped = Pleroma.Emoji.maybe_strip_name(emoji)
[name, domain] = String.split(stripped, "@")
domain_pattern = "%/" <> domain <> "/%"
emoji_pattern = Pleroma.Emoji.maybe_quote(name)
query
|> where([activity], fragment("?->>'content' = ?
AND EXISTS (
SELECT FROM jsonb_array_elements(?->'tag') elem
WHERE elem->>'id' ILIKE ?
)", activity.data, ^emoji_pattern, activity.data, ^domain_pattern))
else
query
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
end
end
#### Announce-related helpers #### Announce-related helpers
@doc """ @doc """

View File

@ -92,6 +92,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
"safe_dm_mentions" "safe_dm_mentions"
end, end,
"pleroma_emoji_reactions", "pleroma_emoji_reactions",
"pleroma_custom_emoji_reactions",
"pleroma_chat_messages", "pleroma_chat_messages",
if Config.get([:instance, :show_reactions]) do if Config.get([:instance, :show_reactions]) do
"exposable_reactions" "exposable_reactions"

View File

@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id
@ -145,7 +146,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
end end
defp put_emoji(response, activity) do defp put_emoji(response, activity) do
Map.put(response, :emoji, activity.data["content"]) response
|> Map.put(:emoji, activity.data["content"])
|> Map.put(:emoji_url, MediaProxy.url(Pleroma.Emoji.emoji_url(activity.data)))
end end
defp put_chat_message(response, activity, reading_user, opts) do defp put_chat_message(response, activity, reading_user, opts) do

View File

@ -340,8 +340,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
opts[:for], opts[:for],
Map.get(opts, :with_muted, false) Map.get(opts, :with_muted, false)
) )
|> Stream.map(fn {emoji, users} -> |> Stream.map(fn {emoji, users, url} ->
build_emoji_map(emoji, users, opts[:for]) build_emoji_map(emoji, users, url, opts[:for])
end) end)
|> Enum.to_list() |> Enum.to_list()
@ -702,11 +702,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end end
end end
defp build_emoji_map(emoji, users, current_user) do defp build_emoji_map(emoji, users, url, current_user) do
%{ %{
name: emoji, name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
count: length(users), count: length(users),
me: !!(current_user && current_user.ap_id in users) url: MediaProxy.url(url),
me: !!(current_user && current_user.ap_id in users),
account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
} }
end end

View File

@ -8,12 +8,20 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do
@impl Provider @impl Provider
def build_tags(%{user: user}) do def build_tags(%{user: user}) do
bio_tree = Floki.parse_fragment!(user.bio) profile_tree =
user.bio
|> append_fields_tag(user.fields)
|> Floki.parse_fragment!()
(Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ (Floki.attribute(profile_tree, "link[rel~=me]", "href") ++
Floki.attribute(bio_tree, "a[rel~=me]", "href")) Floki.attribute(profile_tree, "a[rel~=me]", "href"))
|> Enum.map(fn link -> |> Enum.map(fn link ->
{:link, [rel: "me", href: link], []} {:link, [rel: "me", href: link], []}
end) end)
end end
defp append_fields_tag(bio, fields) do
fields
|> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end)
end
end end

View File

@ -50,29 +50,35 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
if not with_muted, do: User.cached_muted_users_ap_ids(user), else: [] if not with_muted, do: User.cached_muted_users_ap_ids(user), else: []
end end
filter_emoji = fn emoji, users -> filter_emoji = fn emoji, users, url ->
case Enum.reject(users, &(&1 in exclude_ap_ids)) do case Enum.reject(users, &(&1 in exclude_ap_ids)) do
[] -> nil [] -> nil
users -> {emoji, users} users -> {emoji, users, url}
end end
end end
reactions reactions
|> Stream.map(fn |> Stream.map(fn
[emoji, users] when is_list(users) -> filter_emoji.(emoji, users) [emoji, users, url] when is_list(users) -> filter_emoji.(emoji, users, url)
{emoji, users} when is_list(users) -> filter_emoji.(emoji, users) {emoji, users, url} when is_list(users) -> filter_emoji.(emoji, users, url)
{emoji, users} when is_list(users) -> filter_emoji.(emoji, users, nil)
_ -> nil _ -> nil
end) end)
|> Stream.reject(&is_nil/1) |> Stream.reject(&is_nil/1)
end end
defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
Enum.filter(reactions, fn [e, _] -> e == emoji end) Enum.filter(reactions, fn [e, _, _] -> e == emoji end)
end end
defp filter(reactions, _), do: reactions defp filter(reactions, _), do: reactions
def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
emoji =
emoji
|> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote()
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)
@ -83,6 +89,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
end end
def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
emoji =
emoji
|> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote()
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)

View File

@ -7,17 +7,30 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
def emoji_name(emoji, nil), do: emoji
def emoji_name(emoji, url) do
url = URI.parse(url)
if url.host == Pleroma.Web.Endpoint.host() do
emoji
else
"#{emoji}@#{url.host}"
end
end
def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do
render_many(emoji_reactions, __MODULE__, "show.json", opts) render_many(emoji_reactions, __MODULE__, "show.json", opts)
end end
def render("show.json", %{emoji_reaction: {emoji, user_ap_ids}, user: user}) do def render("show.json", %{emoji_reaction: {emoji, user_ap_ids, url}, user: user}) do
users = fetch_users(user_ap_ids) users = fetch_users(user_ap_ids)
%{ %{
name: emoji, name: emoji_name(emoji, url),
count: length(users), count: length(users),
accounts: render(AccountView, "index.json", users: users, for: user), accounts: render(AccountView, "index.json", users: users, for: user),
url: Pleroma.Web.MediaProxy.url(url),
me: !!(user && user.ap_id in user_ap_ids) me: !!(user && user.ap_id in user_ap_ids)
} }
end end

View File

@ -0,0 +1,28 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"Hashtag": "as:Hashtag"
}
],
"type": "Like",
"id": "https://misskey.local.live/likes/917ocsybgp",
"actor": "https://misskey.local.live/users/8x8yep20u2",
"object": "https://pleroma.local.live/objects/89937a53-2692-4631-bb62-770091267391",
"content": ":hanapog:",
"_misskey_reaction": ":hanapog:",
"tag": [
{
"id": "https://misskey.local.live/emojis/hanapog",
"type": "Emoji",
"name": ":hanapog:",
"updated": "2022-06-07T12:00:05.773Z",
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd"
}
}
]
}

View File

@ -38,16 +38,70 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do
assert {:content, {"can't be blank", [validation: :required]}} in cng.errors assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
end end
test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do test "it is valid when custom emoji is used", %{valid_emoji_react: valid_emoji_react} do
without_emoji_content = without_emoji_content =
valid_emoji_react valid_emoji_react
|> Map.put("content", "x") |> Map.put("content", ":hello:")
|> Map.put("tag", [
%{
"type" => "Emoji",
"name" => ":hello:",
"icon" => %{"url" => "http://somewhere", "type" => "Image"}
}
])
{:ok, _, _} = ObjectValidator.validate(without_emoji_content, [])
end
test "it is not valid when custom emoji don't have a matching tag", %{
valid_emoji_react: valid_emoji_react
} do
without_emoji_content =
valid_emoji_react
|> Map.put("content", ":hello:")
|> Map.put("tag", [
%{
"type" => "Emoji",
"name" => ":whoops:",
"icon" => %{"url" => "http://somewhere", "type" => "Image"}
}
])
{:error, cng} = ObjectValidator.validate(without_emoji_content, []) {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
refute cng.valid? refute cng.valid?
assert {:content, {"must be a single character emoji", []}} in cng.errors assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors
end
test "it is not valid when custom emoji have no tags", %{
valid_emoji_react: valid_emoji_react
} do
without_emoji_content =
valid_emoji_react
|> Map.put("content", ":hello:")
|> Map.put("tag", [])
{:error, cng} = ObjectValidator.validate(without_emoji_content, [])
refute cng.valid?
assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors
end
test "it is not valid when custom emoji doesn't match a shortcode format", %{
valid_emoji_react: valid_emoji_react
} do
without_emoji_content =
valid_emoji_react
|> Map.put("content", "hello")
|> Map.put("tag", [])
{:error, cng} = ObjectValidator.validate(without_emoji_content, [])
refute cng.valid?
assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors
end end
end end
end end

View File

@ -453,7 +453,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
object = Object.get_by_ap_id(emoji_react.data["object"]) object = Object.get_by_ap_id(emoji_react.data["object"])
assert object.data["reaction_count"] == 1 assert object.data["reaction_count"] == 1
assert ["👌", [user.ap_id]] in object.data["reactions"] assert ["👌", [user.ap_id], nil] in object.data["reactions"]
end end
test "creates a notification", %{emoji_react: emoji_react, poster: poster} do test "creates a notification", %{emoji_react: emoji_react, poster: poster} do

View File

@ -34,7 +34,56 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
object = Object.get_by_ap_id(data["object"]) object = Object.get_by_ap_id(data["object"])
assert object.data["reaction_count"] == 1 assert object.data["reaction_count"] == 1
assert match?([["👌", _]], object.data["reactions"]) assert match?([["👌", _, nil]], object.data["reactions"])
end
test "it works for incoming custom emoji reactions" do
user = insert(:user)
other_user = insert(:user, local: false)
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
data =
File.read!("test/fixtures/custom-emoji-reaction.json")
|> Jason.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("actor", other_user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == other_user.ap_id
assert data["type"] == "EmojiReact"
assert data["id"] == "https://misskey.local.live/likes/917ocsybgp"
assert data["object"] == activity.data["object"]
assert data["content"] == ":hanapog:"
assert data["tag"] == [
%{
"id" => "https://misskey.local.live/emojis/hanapog",
"type" => "Emoji",
"name" => "hanapog",
"updated" => "2022-06-07T12:00:05.773Z",
"icon" => %{
"type" => "Image",
"url" =>
"https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd"
}
}
]
object = Object.get_by_ap_id(data["object"])
assert object.data["reaction_count"] == 1
assert match?(
[
[
"hanapog",
_,
"https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd"
]
],
object.data["reactions"]
)
end end
test "it works for incoming unqualified emoji reactions" do test "it works for incoming unqualified emoji reactions" do
@ -65,7 +114,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
object = Object.get_by_ap_id(data["object"]) object = Object.get_by_ap_id(data["object"])
assert object.data["reaction_count"] == 1 assert object.data["reaction_count"] == 1
assert match?([[^emoji, _]], object.data["reactions"]) assert match?([[^emoji, _, _]], object.data["reactions"])
end end
test "it reject invalid emoji reactions" do test "it reject invalid emoji reactions" do

View File

@ -190,7 +190,47 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
emoji: "", emoji: "",
account: AccountView.render("show.json", %{user: other_user, for: user}), account: AccountView.render("show.json", %{user: other_user, for: user}),
status: StatusView.render("show.json", %{activity: activity, for: user}), status: StatusView.render("show.json", %{activity: activity, for: user}),
created_at: Utils.to_masto_date(notification.inserted_at) created_at: Utils.to_masto_date(notification.inserted_at),
emoji_url: nil
}
test_notifications_rendering([notification], user, [expected])
end
test "EmojiReact custom emoji notification" do
user = insert(:user)
other_user = insert(:user)
note =
insert(:note,
user: user,
data: %{
"reactions" => [
["👍", [user.ap_id], nil],
["dinosaur", [user.ap_id], "http://localhost:4001/emoji/dino walking.gif"]
]
}
)
activity = insert(:note_activity, note: note, user: user)
{:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "dinosaur")
activity = Repo.get(Activity, activity.id)
[notification] = Notification.for_user(user)
assert notification
expected = %{
id: to_string(notification.id),
pleroma: %{is_seen: false, is_muted: false},
type: "pleroma:emoji_reaction",
emoji: ":dinosaur:",
account: AccountView.render("show.json", %{user: other_user, for: user}),
status: StatusView.render("show.json", %{activity: activity, for: user}),
created_at: Utils.to_masto_date(notification.inserted_at),
emoji_url: "http://localhost:4001/emoji/dino walking.gif"
} }
test_notifications_rendering([notification], user, [expected]) test_notifications_rendering([notification], user, [expected])

View File

@ -35,16 +35,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
{:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"}) {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "") {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, ":dinosaur:")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:")
activity = Repo.get(Activity, activity.id) activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity) status = StatusView.render("show.json", activity: activity)
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:pleroma][:emoji_reactions] == [ assert status[:pleroma][:emoji_reactions] == [
%{name: "", count: 2, me: false}, %{name: "", count: 2, me: false, url: nil, account_ids: [other_user.id, user.id]},
%{name: "🍵", count: 1, me: false} %{
count: 2,
me: false,
name: "dinosaur",
url: "http://localhost:4001/emoji/dino walking.gif",
account_ids: [other_user.id, user.id]
},
%{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
] ]
status = StatusView.render("show.json", activity: activity, for: user) status = StatusView.render("show.json", activity: activity, for: user)
@ -52,8 +62,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:pleroma][:emoji_reactions] == [ assert status[:pleroma][:emoji_reactions] == [
%{name: "", count: 2, me: true}, %{name: "", count: 2, me: true, url: nil, account_ids: [other_user.id, user.id]},
%{name: "🍵", count: 1, me: false} %{
count: 2,
me: true,
name: "dinosaur",
url: "http://localhost:4001/emoji/dino walking.gif",
account_ids: [other_user.id, user.id]
},
%{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
] ]
end end
@ -66,11 +83,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|> Object.update_data(%{"reactions" => %{"" => [user.ap_id], "x" => 1}}) |> Object.update_data(%{"reactions" => %{"" => [user.ap_id], "x" => 1}})
activity = Activity.get_by_id(activity.id) activity = Activity.get_by_id(activity.id)
status = StatusView.render("show.json", activity: activity, for: user) status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [ assert status[:pleroma][:emoji_reactions] == [
%{name: "", count: 1, me: true} %{name: "", count: 1, me: true, url: nil, account_ids: [user.id]}
] ]
end end
@ -90,7 +106,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
status = StatusView.render("show.json", activity: activity) status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [ assert status[:pleroma][:emoji_reactions] == [
%{name: "", count: 1, me: false} %{name: "", count: 1, me: false, url: nil, account_ids: [other_user.id]}
] ]
status = StatusView.render("show.json", activity: activity, for: user) status = StatusView.render("show.json", activity: activity, for: user)
@ -102,19 +118,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
status = StatusView.render("show.json", activity: activity) status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [ assert status[:pleroma][:emoji_reactions] == [
%{name: "", count: 2, me: false} %{
name: "",
count: 2,
me: false,
url: nil,
account_ids: [third_user.id, other_user.id]
}
] ]
status = StatusView.render("show.json", activity: activity, for: user) status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [ assert status[:pleroma][:emoji_reactions] == [
%{name: "", count: 1, me: false} %{name: "", count: 1, me: false, url: nil, account_ids: [third_user.id]}
] ]
status = StatusView.render("show.json", activity: activity, for: other_user) status = StatusView.render("show.json", activity: activity, for: other_user)
assert status[:pleroma][:emoji_reactions] == [ assert status[:pleroma][:emoji_reactions] == [
%{name: "", count: 1, me: true} %{name: "", count: 1, me: true, url: nil, account_ids: [other_user.id]}
] ]
end end

View File

@ -11,11 +11,24 @@ defmodule Pleroma.Web.Metadata.Providers.RelMeTest do
bio = bio =
~s(<a href="https://some-link.com">https://some-link.com</a> <a rel="me" href="https://another-link.com">https://another-link.com</a> <link href="http://some.com"> <link rel="me" href="http://some3.com">) ~s(<a href="https://some-link.com">https://some-link.com</a> <a rel="me" href="https://another-link.com">https://another-link.com</a> <link href="http://some.com"> <link rel="me" href="http://some3.com">)
user = insert(:user, %{bio: bio}) fields = [
%{
"name" => "profile",
"value" => ~S(<a rel="me" href="http://profile.com">http://profile.com</a>)
},
%{
"name" => "like",
"value" => ~S(<a href="http://cofe.io">http://cofe.io</a>)
},
%{"name" => "foo", "value" => "bar"}
]
user = insert(:user, %{bio: bio, fields: fields})
assert RelMe.build_tags(%{user: user}) == [ assert RelMe.build_tags(%{user: user}) == [
{:link, [rel: "me", href: "http://some3.com"], []}, {:link, [rel: "me", href: "http://some3.com"], []},
{:link, [rel: "me", href: "https://another-link.com"], []} {:link, [rel: "me", href: "https://another-link.com"], []},
{:link, [rel: "me", href: "http://profile.com"], []}
] ]
end end
end end

View File

@ -17,23 +17,113 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) note = insert(:note, user: user, data: %{"reactions" => [["👍", [other_user.ap_id], nil]]})
activity = insert(:note_activity, note: note, user: user)
result = result =
conn conn
|> assign(:user, other_user) |> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/") |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/\u26A0")
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
# We return the status, but this our implementation detail.
assert %{"id" => id} = result assert %{"id" => id} = result
assert to_string(activity.id) == id assert to_string(activity.id) == id
assert result["pleroma"]["emoji_reactions"] == [ assert result["pleroma"]["emoji_reactions"] == [
%{"name" => "", "count" => 1, "me" => true} %{
"name" => "👍",
"count" => 1,
"me" => true,
"url" => nil,
"account_ids" => [other_user.id]
},
%{
"name" => "\u26A0\uFE0F",
"count" => 1,
"me" => true,
"url" => nil,
"account_ids" => [other_user.id]
}
] ]
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
ObanHelpers.perform_all()
# Reacting with a custom emoji
result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:dinosaur:")
|> json_response_and_validate_schema(200)
assert %{"id" => id} = result
assert to_string(activity.id) == id
assert result["pleroma"]["emoji_reactions"] == [
%{
"name" => "dinosaur",
"count" => 1,
"me" => true,
"url" => "http://localhost:4001/emoji/dino walking.gif",
"account_ids" => [other_user.id]
}
]
# Reacting with a remote emoji
note =
insert(:note,
user: user,
data: %{
"reactions" => [
["👍", [other_user.ap_id], nil],
["wow", [other_user.ap_id], "https://remote/emoji/wow"]
]
}
)
activity = insert(:note_activity, note: note, user: user)
result =
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
|> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:")
|> json_response(200)
assert result["pleroma"]["emoji_reactions"] == [
%{
"account_ids" => [other_user.id],
"count" => 1,
"me" => false,
"name" => "👍",
"url" => nil
},
%{
"name" => "wow@remote",
"count" => 2,
"me" => true,
"url" => "https://remote/emoji/wow",
"account_ids" => [user.id, other_user.id]
}
]
# Reacting with a remote custom emoji that hasn't been reacted with yet
note =
insert(:note,
user: user
)
activity = insert(:note_activity, note: note, user: user)
assert conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
|> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:")
|> json_response(400)
# Reacting with a non-emoji # Reacting with a non-emoji
assert conn assert conn
|> assign(:user, other_user) |> assign(:user, other_user)
@ -46,8 +136,21 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) note =
insert(:note,
user: user,
data: %{"reactions" => [["wow", [user.ap_id], "https://remote/emoji/wow"]]}
)
activity = insert(:note_activity, note: note, user: user)
ObanHelpers.perform_all()
{:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "") {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "")
{:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:")
{:ok, _reaction_activity} =
CommonAPI.react_with_emoji(activity.id, other_user, ":wow@remote:")
ObanHelpers.perform_all() ObanHelpers.perform_all()
@ -60,11 +163,47 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
assert %{"id" => id} = json_response_and_validate_schema(result, 200) assert %{"id" => id} = json_response_and_validate_schema(result, 200)
assert to_string(activity.id) == id assert to_string(activity.id) == id
# Remove custom emoji
result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:dinosaur:")
assert %{"id" => id} = json_response_and_validate_schema(result, 200)
assert to_string(activity.id) == id
ObanHelpers.perform_all() ObanHelpers.perform_all()
object = Object.get_by_ap_id(activity.data["object"]) object = Object.get_by_ap_id(activity.data["object"])
assert object.data["reaction_count"] == 0 assert object.data["reaction_count"] == 2
# Remove custom remote emoji
result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:")
|> json_response(200)
assert result["pleroma"]["emoji_reactions"] == [
%{
"name" => "wow@remote",
"count" => 1,
"me" => false,
"url" => "https://remote/emoji/wow",
"account_ids" => [user.id]
}
]
# Remove custom remote emoji that hasn't been reacted with yet
assert conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:zoop@remote:")
|> json_response(400)
end end
test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
@ -181,7 +320,15 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "")
assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = assert [
%{
"name" => "🎅",
"count" => 1,
"accounts" => [represented_user],
"me" => false,
"url" => nil
}
] =
conn conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)