Allow custom emoji reactions: Add pleroma_custom_emoji_reactions feature, review changes
This commit is contained in:
parent
8d3b29aaba
commit
2c2ea16b50
|
@ -51,14 +51,7 @@ 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 =
|
name = maybe_strip_name(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
|
||||||
|
@ -148,13 +141,15 @@ 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
|
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
|
||||||
name
|
|
||||||
|> String.replace_leading(":", "")
|
|
||||||
|> String.replace_trailing(":", "")
|
|
||||||
end
|
|
||||||
|
|
||||||
def stripped_name(name), do: name
|
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
|
def maybe_quote(name) when is_binary(name) do
|
||||||
if is_unicode_emoji?(name) do
|
if is_unicode_emoji?(name) do
|
||||||
|
@ -173,9 +168,13 @@ def maybe_quote(name), do: name
|
||||||
def emoji_url(%{"type" => "EmojiReact", "content" => _, "tag" => []}), do: nil
|
def emoji_url(%{"type" => "EmojiReact", "content" => _, "tag" => []}), do: nil
|
||||||
|
|
||||||
def emoji_url(%{"type" => "EmojiReact", "content" => emoji, "tag" => tags}) do
|
def emoji_url(%{"type" => "EmojiReact", "content" => emoji, "tag" => tags}) do
|
||||||
|
emoji = maybe_strip_name(emoji)
|
||||||
|
|
||||||
tag =
|
tag =
|
||||||
tags
|
tags
|
||||||
|> Enum.find(fn tag -> tag["type"] == "Emoji" && tag["name"] == stripped_name(emoji) end)
|
|> Enum.find(fn tag ->
|
||||||
|
tag["type"] == "Emoji" && !is_nil(tag["name"]) && tag["name"] == emoji
|
||||||
|
end)
|
||||||
|
|
||||||
if is_nil(tag) do
|
if is_nil(tag) do
|
||||||
nil
|
nil
|
||||||
|
|
|
@ -62,21 +62,22 @@ defp unicode_emoji_react(_object, data, emoji) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_emoji_content(data, emoji, url) do
|
defp add_emoji_content(data, emoji, url) do
|
||||||
|
tag = [
|
||||||
|
%{
|
||||||
|
"id" => url,
|
||||||
|
"type" => "Emoji",
|
||||||
|
"name" => Emoji.maybe_quote(emoji),
|
||||||
|
"icon" => %{
|
||||||
|
"type" => "Image",
|
||||||
|
"url" => url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
data
|
data
|
||||||
|> Map.put("content", Emoji.maybe_quote(emoji))
|
|> Map.put("content", Emoji.maybe_quote(emoji))
|
||||||
|> Map.put("type", "EmojiReact")
|
|> Map.put("type", "EmojiReact")
|
||||||
|> Map.put("tag", [
|
|> Map.put("tag", tag)
|
||||||
%{}
|
|
||||||
|> Map.put("id", url)
|
|
||||||
|> Map.put("type", "Emoji")
|
|
||||||
|> Map.put("name", Emoji.maybe_quote(emoji))
|
|
||||||
|> Map.put(
|
|
||||||
"icon",
|
|
||||||
%{}
|
|
||||||
|> Map.put("type", "Image")
|
|
||||||
|> Map.put("url", url)
|
|
||||||
)
|
|
||||||
])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp remote_custom_emoji_react(
|
defp remote_custom_emoji_react(
|
||||||
|
@ -84,7 +85,7 @@ defp remote_custom_emoji_react(
|
||||||
data,
|
data,
|
||||||
emoji
|
emoji
|
||||||
) do
|
) do
|
||||||
[emoji_code, instance] = String.split(Emoji.stripped_name(emoji), "@")
|
[emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
|
||||||
|
|
||||||
matching_reaction =
|
matching_reaction =
|
||||||
Enum.find(
|
Enum.find(
|
||||||
|
@ -110,8 +111,7 @@ defp remote_custom_emoji_react(_object, _data, _emoji) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp local_custom_emoji_react(data, emoji) do
|
defp local_custom_emoji_react(data, emoji) do
|
||||||
with %{} = emojo <- Emoji.get(emoji) do
|
with %{file: path} = emojo <- Emoji.get(emoji) do
|
||||||
path = emojo |> Map.get(:file)
|
|
||||||
url = "#{Endpoint.url()}#{path}"
|
url = "#{Endpoint.url()}#{path}"
|
||||||
add_emoji_content(data, emojo.code, url)
|
add_emoji_content(data, emojo.code, url)
|
||||||
else
|
else
|
||||||
|
|
|
@ -58,17 +58,10 @@ 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
|
||||||
|
|
|
@ -8,12 +8,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
||||||
alias Pleroma.Emoji
|
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
|
||||||
|
|
||||||
@primary_key false
|
@primary_key false
|
||||||
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
|
|
||||||
|
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
quote do
|
quote do
|
||||||
|
@ -21,7 +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()
|
||||||
tag_fields()
|
embeds_many(:tag, TagValidator)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -57,12 +57,7 @@ defp fix(data) do
|
||||||
|> CommonFixes.fix_actor()
|
|> CommonFixes.fix_actor()
|
||||||
|> CommonFixes.fix_activity_addressing()
|
|> CommonFixes.fix_activity_addressing()
|
||||||
|
|
||||||
data =
|
data = Map.put_new(data, "tag", [])
|
||||||
if Map.has_key?(data, "tag") do
|
|
||||||
data
|
|
||||||
else
|
|
||||||
Map.put(data, "tag", [])
|
|
||||||
end
|
|
||||||
|
|
||||||
case Object.normalize(data["object"]) do
|
case Object.normalize(data["object"]) do
|
||||||
%Object{} = object ->
|
%Object{} = object ->
|
||||||
|
@ -92,13 +87,10 @@ defp fix_emoji_qualification(%{"content" => emoji} = data) do
|
||||||
|
|
||||||
defp fix_emoji_qualification(data), do: data
|
defp fix_emoji_qualification(data), do: data
|
||||||
|
|
||||||
defp matches_shortcode?(nil), do: false
|
|
||||||
defp matches_shortcode?(s), do: Regex.match?(@emoji_regex, s)
|
|
||||||
|
|
||||||
defp validate_emoji(cng) do
|
defp validate_emoji(cng) do
|
||||||
content = get_field(cng, :content)
|
content = get_field(cng, :content)
|
||||||
|
|
||||||
if Emoji.is_unicode_emoji?(content) || matches_shortcode?(content) do
|
if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(content) do
|
||||||
cng
|
cng
|
||||||
else
|
else
|
||||||
cng
|
cng
|
||||||
|
@ -113,7 +105,7 @@ defp maybe_validate_tag_presence(cng) do
|
||||||
cng
|
cng
|
||||||
else
|
else
|
||||||
tag = get_field(cng, :tag)
|
tag = get_field(cng, :tag)
|
||||||
emoji_name = Emoji.stripped_name(content)
|
emoji_name = Emoji.maybe_strip_name(content)
|
||||||
|
|
||||||
case tag do
|
case tag do
|
||||||
[%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] ->
|
[%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] ->
|
||||||
|
|
|
@ -329,8 +329,8 @@ def add_emoji_reaction_to_object(
|
||||||
object
|
object
|
||||||
) do
|
) do
|
||||||
reactions = get_cached_emoji_reactions(object)
|
reactions = get_cached_emoji_reactions(object)
|
||||||
emoji = Pleroma.Emoji.stripped_name(emoji)
|
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||||
url = emoji_url(emoji, activity)
|
url = maybe_emoji_url(emoji, activity)
|
||||||
|
|
||||||
new_reactions =
|
new_reactions =
|
||||||
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
|
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
|
||||||
|
@ -356,7 +356,7 @@ def add_emoji_reaction_to_object(
|
||||||
update_element_in_object("reaction", new_reactions, object, count)
|
update_element_in_object("reaction", new_reactions, object, count)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp emoji_url(
|
defp maybe_emoji_url(
|
||||||
name,
|
name,
|
||||||
%Activity{
|
%Activity{
|
||||||
data: %{
|
data: %{
|
||||||
|
@ -368,7 +368,7 @@ defp emoji_url(
|
||||||
),
|
),
|
||||||
do: url
|
do: url
|
||||||
|
|
||||||
defp emoji_url(_, _), do: nil
|
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)
|
||||||
|
@ -378,9 +378,9 @@ def remove_emoji_reaction_from_object(
|
||||||
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
|
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
|
||||||
object
|
object
|
||||||
) do
|
) do
|
||||||
emoji = Pleroma.Emoji.stripped_name(emoji)
|
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||||
reactions = get_cached_emoji_reactions(object)
|
reactions = get_cached_emoji_reactions(object)
|
||||||
url = emoji_url(emoji, activity)
|
url = maybe_emoji_url(emoji, activity)
|
||||||
|
|
||||||
new_reactions =
|
new_reactions =
|
||||||
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
|
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
|
||||||
|
@ -533,9 +533,9 @@ def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
|
||||||
|
|
||||||
defp custom_emoji_discriminator(query, emoji) do
|
defp custom_emoji_discriminator(query, emoji) do
|
||||||
if String.contains?(emoji, "@") do
|
if String.contains?(emoji, "@") do
|
||||||
stripped = Pleroma.Emoji.stripped_name(emoji)
|
stripped = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||||
[name, domain] = String.split(stripped, "@")
|
[name, domain] = String.split(stripped, "@")
|
||||||
domain_pattern = "%" <> domain <> "%"
|
domain_pattern = "%/" <> domain <> "/%"
|
||||||
emoji_pattern = Pleroma.Emoji.maybe_quote(name)
|
emoji_pattern = Pleroma.Emoji.maybe_quote(name)
|
||||||
|
|
||||||
query
|
query
|
||||||
|
|
|
@ -92,6 +92,7 @@ def features 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"
|
||||||
|
|
|
@ -8,7 +8,6 @@ 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, nil), do: emoji
|
||||||
alias Pleroma.Web.MediaProxy
|
|
||||||
|
|
||||||
def emoji_name(emoji, url) do
|
def emoji_name(emoji, url) do
|
||||||
url = URI.parse(url)
|
url = URI.parse(url)
|
||||||
|
@ -31,7 +30,7 @@ def render("show.json", %{emoji_reaction: {emoji, user_ap_ids, url}, user: user}
|
||||||
name: emoji_name(emoji, url),
|
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: MediaProxy.url(url),
|
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
|
||||||
|
|
|
@ -197,6 +197,45 @@ test "EmojiReact notification" do
|
||||||
test_notifications_rendering([notification], user, [expected])
|
test_notifications_rendering([notification], user, [expected])
|
||||||
end
|
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])
|
||||||
|
end
|
||||||
|
|
||||||
test "Poll notification" do
|
test "Poll notification" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
activity = insert(:question_activity, user: user)
|
activity = insert(:question_activity, user: user)
|
||||||
|
|
Loading…
Reference in New Issue