Allow custom emoji reactions: Fix tests, mixed custom and unicode reactions

This commit is contained in:
Alexander Tumin 2022-12-18 21:52:19 +03:00
parent 787e30c5fd
commit 4b85d1c617
10 changed files with 240 additions and 20 deletions

View File

@ -51,6 +51,15 @@ def reload 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 =
if String.starts_with?(name, ":") do
name
|> String.replace_leading(":", "")
|> String.replace_trailing(":", "")
else
name
end
case :ets.lookup(@ets, name) do case :ets.lookup(@ets, name) do
[{_, path}] -> path [{_, path}] -> path
_ -> nil _ -> nil
@ -139,6 +148,51 @@ def is_unicode_emoji?(unquote(emoji)), do: true
def is_unicode_emoji?(_), do: false def is_unicode_emoji?(_), do: false
def stripped_name(name) when is_binary(name) do
name
|> String.replace_leading(":", "")
|> String.replace_trailing(":", "")
end
def stripped_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
tag =
tags
|> Enum.find(fn tag -> tag["type"] == "Emoji" && tag["name"] == stripped_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

@ -90,8 +90,10 @@ defp remote_custom_emoji_react(
Enum.find( Enum.find(
existing_reactions, existing_reactions,
fn [name, _, url] -> fn [name, _, url] ->
url = URI.parse(url) if url != nil do
url.host == instance && name == emoji_code url = URI.parse(url)
url.host == instance && name == emoji_code
end
end end
) )

View File

@ -58,10 +58,17 @@ defmacro status_object_fields do
field(:like_count, :integer, default: 0) field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0) field(:announcement_count, :integer, default: 0)
field(:inReplyTo, ObjectValidators.ObjectID) field(:inReplyTo, ObjectValidators.ObjectID)
field(:quoteUri, ObjectValidators.ObjectID)
field(:url, ObjectValidators.Uri) field(:url, ObjectValidators.Uri)
field(:likes, {:array, ObjectValidators.ObjectID}, default: []) field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
end end
end end
defmacro tag_fields do
quote bind_quoted: binding() do
embeds_many(:tag, TagValidator)
end
end
end end

View File

@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MediaProxy
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
@ -145,7 +146,9 @@ defp put_report(response, activity) 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

@ -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 @@ test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emo
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 @@ test "adds the reaction to the object", %{emoji_react: emoji_react, user: user}
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 @@ test "it works for incoming emoji reactions" 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 @@ test "it works for incoming unqualified emoji reactions" 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,8 @@ test "EmojiReact notification" 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]) test_notifications_rendering([notification], user, [expected])

View File

@ -35,16 +35,26 @@ test "has an emoji reaction list" 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 @@ test "has an emoji reaction list" 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 @@ test "works correctly with badly formatted emojis" 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 @@ test "doesn't show reactions from muted and blocked users" 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 @@ test "doesn't show reactions from muted and blocked users" 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