From 2c2ea16b50a7b77b1d3f58722e45720dcf3c51b9 Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Thu, 2 Mar 2023 10:09:13 +0300 Subject: [PATCH] Allow custom emoji reactions: Add pleroma_custom_emoji_reactions feature, review changes --- lib/pleroma/emoji.ex | 29 +++++++------- lib/pleroma/web/activity_pub/builder.ex | 30 +++++++------- .../object_validators/common_fields.ex | 7 ---- .../emoji_react_validator.ex | 18 +++------ lib/pleroma/web/activity_pub/utils.ex | 16 ++++---- .../web/mastodon_api/views/instance_view.ex | 1 + .../pleroma_api/views/emoji_reaction_view.ex | 3 +- .../views/notification_view_test.exs | 39 +++++++++++++++++++ 8 files changed, 83 insertions(+), 60 deletions(-) diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index 749b4f4ca..43a3447c3 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -51,14 +51,7 @@ def reload do @doc "Returns the path of the emoji `name`." @spec get(String.t()) :: String.t() | nil def get(name) do - name = - if String.starts_with?(name, ":") do - name - |> String.replace_leading(":", "") - |> String.replace_trailing(":", "") - else - name - end + name = maybe_strip_name(name) case :ets.lookup(@ets, name) do [{_, path}] -> path @@ -148,13 +141,15 @@ def is_unicode_emoji?(unquote(emoji)), do: true def is_unicode_emoji?(_), do: false - def stripped_name(name) when is_binary(name) do - name - |> String.replace_leading(":", "") - |> String.replace_trailing(":", "") - end + @emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/ - 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 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" => emoji, "tag" => tags}) do + emoji = maybe_strip_name(emoji) + tag = 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 nil diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 9553bde23..8eab3a241 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -62,21 +62,22 @@ defp unicode_emoji_react(_object, data, emoji) do 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", [ - %{} - |> 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) - ) - ]) + |> Map.put("tag", tag) end defp remote_custom_emoji_react( @@ -84,7 +85,7 @@ defp remote_custom_emoji_react( data, emoji ) do - [emoji_code, instance] = String.split(Emoji.stripped_name(emoji), "@") + [emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@") matching_reaction = Enum.find( @@ -110,8 +111,7 @@ defp remote_custom_emoji_react(_object, _data, _emoji) do end defp local_custom_emoji_react(data, emoji) do - with %{} = emojo <- Emoji.get(emoji) do - path = emojo |> Map.get(:file) + with %{file: path} = emojo <- Emoji.get(emoji) do url = "#{Endpoint.url()}#{path}" add_emoji_content(data, emojo.code, url) else diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index 904fdeced..7b60c139a 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -58,17 +58,10 @@ defmacro status_object_fields do field(:like_count, :integer, default: 0) field(:announcement_count, :integer, default: 0) field(:inReplyTo, ObjectValidators.ObjectID) - field(:quoteUri, ObjectValidators.ObjectID) field(:url, ObjectValidators.Uri) field(:likes, {:array, ObjectValidators.ObjectID}, default: []) field(:announcements, {:array, ObjectValidators.ObjectID}, default: []) end end - - defmacro tag_fields do - quote bind_quoted: binding() do - embeds_many(:tag, TagValidator) - end - end end diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex index 54efe5c42..a0b82b325 100644 --- a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex @@ -8,12 +8,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do alias Pleroma.Emoji alias Pleroma.Object alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes + alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator import Ecto.Changeset import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @primary_key false - @emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/ embedded_schema do quote do @@ -21,7 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields message_fields() activity_fields() - tag_fields() + embeds_many(:tag, TagValidator) end end @@ -57,12 +57,7 @@ defp fix(data) do |> CommonFixes.fix_actor() |> CommonFixes.fix_activity_addressing() - data = - if Map.has_key?(data, "tag") do - data - else - Map.put(data, "tag", []) - end + data = Map.put_new(data, "tag", []) case Object.normalize(data["object"]) do %Object{} = object -> @@ -92,13 +87,10 @@ defp fix_emoji_qualification(%{"content" => emoji} = data) do 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 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 else cng @@ -113,7 +105,7 @@ defp maybe_validate_tag_presence(cng) do cng else tag = get_field(cng, :tag) - emoji_name = Emoji.stripped_name(content) + emoji_name = Emoji.maybe_strip_name(content) case tag do [%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] -> diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 11f1b95c5..db7f79dac 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -329,8 +329,8 @@ def add_emoji_reaction_to_object( object ) do reactions = get_cached_emoji_reactions(object) - emoji = Pleroma.Emoji.stripped_name(emoji) - url = emoji_url(emoji, activity) + emoji = Pleroma.Emoji.maybe_strip_name(emoji) + url = maybe_emoji_url(emoji, activity) new_reactions = 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) end - defp emoji_url( + defp maybe_emoji_url( name, %Activity{ data: %{ @@ -368,7 +368,7 @@ defp emoji_url( ), do: url - defp emoji_url(_, _), do: nil + defp maybe_emoji_url(_, _), do: nil def emoji_count(reactions_list) do 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, object ) do - emoji = Pleroma.Emoji.stripped_name(emoji) + emoji = Pleroma.Emoji.maybe_strip_name(emoji) reactions = get_cached_emoji_reactions(object) - url = emoji_url(emoji, activity) + url = maybe_emoji_url(emoji, activity) new_reactions = 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 if String.contains?(emoji, "@") do - stripped = Pleroma.Emoji.stripped_name(emoji) + stripped = Pleroma.Emoji.maybe_strip_name(emoji) [name, domain] = String.split(stripped, "@") - domain_pattern = "%" <> domain <> "%" + domain_pattern = "%/" <> domain <> "/%" emoji_pattern = Pleroma.Emoji.maybe_quote(name) query diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index abf7f29ab..efd2a0af6 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -92,6 +92,7 @@ def features do "safe_dm_mentions" end, "pleroma_emoji_reactions", + "pleroma_custom_emoji_reactions", "pleroma_chat_messages", if Config.get([:instance, :show_reactions]) do "exposable_reactions" diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex index 67db1a5ad..6df4ab9d0 100644 --- a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex +++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do alias Pleroma.Web.MastodonAPI.AccountView def emoji_name(emoji, nil), do: emoji - alias Pleroma.Web.MediaProxy def emoji_name(emoji, url) do 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), count: length(users), 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) } end diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs index e52985f8d..ddbe4557f 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -197,6 +197,45 @@ test "EmojiReact notification" do 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]) + end + test "Poll notification" do user = insert(:user) activity = insert(:question_activity, user: user)