AP ingest: accept Emoji icon variants

This commit is contained in:
Lain Soykaf 2026-01-17 01:33:21 +04:00
commit 9912a9f603
6 changed files with 246 additions and 8 deletions

View file

@ -59,12 +59,29 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
end
def changeset(struct, %{"type" => "Emoji"} = data) do
data = Map.put(data, "name", String.trim(data["name"], ":"))
data =
data
|> Map.put("name", String.trim(data["name"], ":"))
|> normalize_emoji_icon()
struct
|> cast(data, [:type, :name, :updated, :id])
|> cast_embed(:icon, with: &icon_changeset/2)
|> validate_required([:type, :name, :icon])
case data["icon"] do
%{"url" => url} when is_binary(url) ->
if valid_http_url?(url) do
struct
|> cast(data, [:type, :name, :updated, :id])
|> cast_embed(:icon, with: &icon_changeset/2)
|> validate_required([:type, :name, :icon])
else
struct
|> cast(data, [])
|> Map.put(:action, :ignore)
end
_ ->
struct
|> cast(data, [])
|> Map.put(:action, :ignore)
end
end
def changeset(struct, %{"type" => "Link"} = data) do
@ -101,6 +118,41 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
defp infer_type(data), do: data
defp normalize_emoji_icon(%{"icon" => icon} = data) when is_binary(icon) do
Map.put(data, "icon", %{"type" => "Image", "url" => icon})
end
defp normalize_emoji_icon(%{"icon" => icon} = data) when is_map(icon) do
case extract_icon_url(icon) do
url when is_binary(url) -> Map.put(data, "icon", %{"type" => "Image", "url" => url})
_ -> Map.delete(data, "icon")
end
end
defp normalize_emoji_icon(data), do: data
defp extract_icon_url(%{"url" => url}) when is_binary(url), do: url
defp extract_icon_url(%{"url" => %{"href" => href}}) when is_binary(href), do: href
defp extract_icon_url(%{"url" => [first | _]}) do
cond do
is_binary(first) -> first
is_map(first) -> first["href"]
true -> nil
end
end
defp extract_icon_url(%{"href" => href}) when is_binary(href), do: href
defp extract_icon_url(_), do: nil
defp valid_http_url?(url) when is_binary(url) do
match?({:ok, _}, ObjectValidators.Uri.cast(url))
end
defp valid_http_url?(_), do: false
def icon_changeset(struct, data) do
struct
|> cast(data, [:type, :url])

View file

@ -386,6 +386,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_url(object), do: object
defp emoji_icon_url(icon) when is_binary(icon), do: icon
defp emoji_icon_url(%{"url" => url}) when is_binary(url), do: url
defp emoji_icon_url(%{"url" => %{"href" => href}}) when is_binary(href), do: href
defp emoji_icon_url(%{"url" => [first | _]}) do
cond do
is_binary(first) -> first
is_map(first) -> first["href"] || first["url"] || first["id"]
true -> nil
end
end
defp emoji_icon_url(%{"href" => href}) when is_binary(href), do: href
defp emoji_icon_url(_), do: nil
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji =
tags
@ -393,7 +411,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")
Map.put(mapping, name, data["icon"]["url"])
case emoji_icon_url(data["icon"]) do
url when is_binary(url) -> Map.put(mapping, name, url)
_ -> mapping
end
end)
Map.put(object, "emoji", emoji)
@ -401,9 +422,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}
Map.put(object, "emoji", emoji)
with url when is_binary(url) <- emoji_icon_url(tag["icon"]) do
Map.put(object, "emoji", %{name => url})
else
_ -> object
end
end
def fix_emoji(object), do: object

View file

@ -0,0 +1,32 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/emoji-icon-string",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:00Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-emoji-icon-string",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:00Z",
"content": "emoji icon as string :cow:",
"tag": [
{
"type": "Emoji",
"name": ":cow:",
"icon": "https://fedidev.fun/static/cow.png"
}
]
}
}

View file

@ -0,0 +1,34 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/emoji-icon-url-missing-type",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:01Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-emoji-icon-url-missing-type",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:01Z",
"content": "emoji icon url missing type :cow2:",
"tag": [
{
"type": "Emoji",
"name": ":cow2:",
"icon": {
"url": "https://fedidev.fun/static/cow.png"
}
}
]
}
}

View file

@ -0,0 +1,31 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://fedidev.fun/activity/emoji-missing-icon",
"actor": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:02Z",
"object": {
"type": "Note",
"id": "https://fedidev.fun/objects/note-emoji-missing-icon",
"attributedTo": "https://fedidev.fun/users/alice",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [],
"published": "2025-11-21T14:40:02Z",
"content": "emoji missing icon :cow3:",
"tag": [
{
"type": "Emoji",
"name": ":cow3:"
}
]
}
}

View file

@ -0,0 +1,65 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FedidevFunEmojiTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
defp ensure_remote_actor!(ap_id) when is_binary(ap_id) do
case User.get_cached_by_ap_id(ap_id) do
%User{} ->
:ok
_ ->
insert(:user,
local: false,
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following",
featured_address: ap_id <> "/collections/featured",
inbox: ap_id <> "/inbox"
)
:ok
end
end
defp ingest_fixture!(rel_path) do
data = rel_path |> File.read!() |> Jason.decode!()
ensure_remote_actor!(data["actor"])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"], fetch: false)
{activity, object}
end
test "ingests Emoji tag with string icon" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_string.json")
assert object.data["emoji"]["cow"] == "https://fedidev.fun/static/cow.png"
end
test "ingests Emoji tag with icon missing type" do
{_activity, object} =
ingest_fixture!("test/fixtures/fedidev.fun/emoji_icon_url_missing_type.json")
assert object.data["emoji"]["cow2"] == "https://fedidev.fun/static/cow.png"
end
test "ignores Emoji tag missing icon" do
{_activity, object} = ingest_fixture!("test/fixtures/fedidev.fun/emoji_missing_icon.json")
assert object.data["emoji"] == %{}
end
end