Fix emoji qualification (#124)
authorfloatingghost <hannah@coffee-and-dreams.uk>
Thu, 28 Jul 2022 12:02:36 +0000 (12:02 +0000)
committerfloatingghost <hannah@coffee-and-dreams.uk>
Thu, 28 Jul 2022 12:02:36 +0000 (12:02 +0000)
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/124

lib/pleroma/emoji.ex
lib/pleroma/emoji/combinations.ex [new file with mode: 0644]
lib/pleroma/reverse_proxy.ex
lib/pleroma/search/elasticsearch/activity_parser.ex
lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
test/pleroma/emoji_test.exs
test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs

index ced2ae83d28f1702cf5d8faa55d9005208cdf789..24eafda4143dc06e071acc1080fc60b5e1e0c17f 100644 (file)
@@ -9,6 +9,7 @@ defmodule Pleroma.Emoji do
   """
   use GenServer
 
+  alias Pleroma.Emoji.Combinations
   alias Pleroma.Emoji.Loader
 
   require Logger
@@ -124,7 +125,7 @@ defmodule Pleroma.Emoji do
     |> String.split("\n")
     |> Enum.filter(fn line ->
       line != "" and not String.starts_with?(line, "#") and
-        String.contains?(line, "qualified")
+        String.contains?(line, "fully-qualified")
     end)
     |> Enum.map(fn line ->
       line
@@ -186,4 +187,17 @@ defmodule Pleroma.Emoji do
   end
 
   def emoji_url(_), do: nil
+
+  emoji_qualification_map =
+    emojis
+    |> Enum.filter(&String.contains?(&1, "\uFE0F"))
+    |> Combinations.variate_emoji_qualification()
+
+  for {qualified, unqualified_list} <- emoji_qualification_map do
+    for unqualified <- unqualified_list do
+      def fully_qualify_emoji(unquote(unqualified)), do: unquote(qualified)
+    end
+  end
+
+  def fully_qualify_emoji(emoji), do: emoji
 end
diff --git a/lib/pleroma/emoji/combinations.ex b/lib/pleroma/emoji/combinations.ex
new file mode 100644 (file)
index 0000000..981c735
--- /dev/null
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.Combinations do
+  # FE0F is the emoji variation sequence. It is used for fully-qualifying
+  # emoji, and that includes emoji combinations.
+  # This code generates combinations per emoji: for each FE0F, all possible
+  # combinations of the character being removed or staying will be generated.
+  # This is made as an attempt to find all partially-qualified and unqualified
+  # versions of a fully-qualified emoji.
+  # I have found *no cases* for which this would be a problem, after browsing
+  # the entire emoji list in emoji-test.txt. This is safe, and, sadly, most
+  # likely sane too.
+
+  defp qualification_combinations(codepoints) do
+    qualification_combinations([[]], codepoints)
+  end
+
+  defp qualification_combinations(acc, []), do: acc
+
+  defp qualification_combinations(acc, ["\uFE0F" | tail]) do
+    acc
+    |> Enum.flat_map(fn x -> [x, x ++ ["\uFE0F"]] end)
+    |> qualification_combinations(tail)
+  end
+
+  defp qualification_combinations(acc, [codepoint | tail]) do
+    acc
+    |> Enum.map(&Kernel.++(&1, [codepoint]))
+    |> qualification_combinations(tail)
+  end
+
+  def variate_emoji_qualification(emoji) when is_binary(emoji) do
+    emoji
+    |> String.codepoints()
+    |> qualification_combinations()
+    |> Enum.map(&List.to_string/1)
+  end
+
+  def variate_emoji_qualification(emoji) when is_list(emoji) do
+    emoji
+    |> Enum.map(fn emoji -> {emoji, variate_emoji_qualification(emoji)} end)
+  end
+end
index 51f9609cb6573f41371dbff425632180e9cb172d..91cf1bba33006f9947421e5799ad869144cf68de 100644 (file)
@@ -114,6 +114,7 @@ defmodule Pleroma.ReverseProxy do
     else
       {:ok, true} ->
         conn
+        |> put_private(:proxied_url, url)
         |> error_or_redirect(500, "Request failed", opts)
         |> halt()
 
index 9c39e516a54d76835efc32f7c956e87e1b34edac..f2fa394fa501492d7d3d727c4ad09ecad8a80105 100644 (file)
@@ -37,6 +37,13 @@ defmodule Pleroma.Search.Elasticsearch.Parsers.Activity do
   end
 
   def parse(q) do
-    Enum.map(q, &to_es/1)
+    [
+      %{
+        exists: %{
+          field: "content"
+        }
+      }
+    ] ++
+      Enum.map(q, &to_es/1)
   end
 end
index f4870f58095033b1eda3499a1925c489f3ca5bd6..306a57a93bf23740a0ada1501c275ac9e6a86963 100644 (file)
@@ -53,6 +53,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
   defp fix(data) do
     data =
       data
+      |> fix_emoji_qualification()
       |> CommonFixes.fix_actor()
       |> CommonFixes.fix_activity_addressing()
 
@@ -77,6 +78,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
   defp matches_shortcode?(nil), do: false
   defp matches_shortcode?(s), do: Regex.match?(@emoji_regex, s)
 
+  defp fix_emoji_qualification(%{"content" => emoji} = data) do
+    new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji)
+
+    cond do
+      Pleroma.Emoji.is_unicode_emoji?(emoji) ->
+        data
+
+      Pleroma.Emoji.is_unicode_emoji?(new_emoji) ->
+        data |> Map.put("content", new_emoji)
+
+      true ->
+        data
+    end
+  end
+
+  defp fix_emoji_qualification(data), do: data
+
   defp validate_emoji(cng) do
     content = get_field(cng, :content)
 
index 978473b140ac342338ab6d01a701aade276ca2c9..deaab1e8b43ac02e542ffb4a70006933f2c93e5a 100644 (file)
@@ -13,8 +13,8 @@ defmodule Pleroma.EmojiTest do
 
       # Accept fully-qualified and unqualified emoji
       # See http://www.unicode.org/reports/tr51/
-      assert Emoji.is_unicode_emoji?("❤")
-      assert Emoji.is_unicode_emoji?("☂")
+      refute Emoji.is_unicode_emoji?("❤")
+      refute Emoji.is_unicode_emoji?("☂")
 
       assert Emoji.is_unicode_emoji?("🥺")
       assert Emoji.is_unicode_emoji?("🤰")
index 4ab1d29e3f5bad4306af9f140b742338e15f9ac0..d6f9b01447977bd5abb1a133d321160c893c3c00 100644 (file)
@@ -86,6 +86,37 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
            )
   end
 
+  test "it works for incoming unqualified emoji reactions" do
+    user = insert(:user)
+    other_user = insert(:user, local: false)
+    {:ok, activity} = CommonAPI.post(user, %{status: "hello"})
+
+    # woman detective emoji, unqualified
+    unqualified_emoji = [0x1F575, 0x200D, 0x2640] |> List.to_string()
+
+    data =
+      File.read!("test/fixtures/emoji-reaction.json")
+      |> Jason.decode!()
+      |> Map.put("object", activity.data["object"])
+      |> Map.put("actor", other_user.ap_id)
+      |> Map.put("content", unqualified_emoji)
+
+    {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+    assert data["actor"] == other_user.ap_id
+    assert data["type"] == "EmojiReact"
+    assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
+    assert data["object"] == activity.data["object"]
+    # woman detective emoji, fully qualified
+    emoji = [0x1F575, 0xFE0F, 0x200D, 0x2640, 0xFE0F] |> List.to_string()
+    assert data["content"] == emoji
+
+    object = Object.get_by_ap_id(data["object"])
+
+    assert object.data["reaction_count"] == 1
+    assert match?([[^emoji, _, _]], object.data["reactions"])
+  end
+
   test "it reject invalid emoji reactions" do
     user = insert(:user)
     other_user = insert(:user, local: false)