federation/in: drop remote part from received emoji reactions
The remote part is included in federated emoji names by e.g.
Iceshrimp.NET ever since remote emoji support was added in
4d21aa1670
and as of writing it still continues to do so.
It adds no value for us though; we add the remote part automatically
based on the URL and it makes it more difficult to correctly coalesce
the original reaction (from a user for whom the moji was local)
and the subsequent reactions with the identical emoji from users of
other instances. Additionally the remote part can cause issues when
later used with our REST API.
For non-reactions this is unproblematic and thus
there’s no need to change anything there.
Use a migration to fix up existing activities.
This will cause some (further) desync from the inlined reactions
array, but will be fixable with the resync mix task and avoids
issues when running the resync without first fixing existing activities.
This commit is contained in:
parent
4765b79b49
commit
89801abad5
3 changed files with 214 additions and 0 deletions
|
|
@ -57,6 +57,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
|> fix_emoji_qualification()
|
||||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_activity_addressing()
|
||||
|> prune_tags()
|
||||
|> drop_remote_indicator()
|
||||
|
||||
data =
|
||||
if Map.has_key?(data, "tag") do
|
||||
|
|
@ -133,4 +135,54 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
|> validate_emoji()
|
||||
|> maybe_validate_tag_presence()
|
||||
end
|
||||
|
||||
# All tags but the single emoji tag corresponding to the used custom emoji (if any)
|
||||
# are ignored anyway. Having a known single-element array makes further processing easier.
|
||||
# Also ensures the Emoji tag uses a pre-stripped name
|
||||
defp prune_tags(%{"content" => emoji, "tag" => tags} = data) do
|
||||
clean_emoji = Emoji.stripped_name(emoji)
|
||||
|
||||
pruned_tags =
|
||||
Enum.reduce_while(tags, [], fn
|
||||
%{"type" => "Emoji", "name" => name} = tag, res ->
|
||||
clean_name = Emoji.stripped_name(name)
|
||||
|
||||
if clean_name == clean_emoji do
|
||||
{:halt, [%{tag | "name" => clean_name}]}
|
||||
else
|
||||
{:cont, res}
|
||||
end
|
||||
|
||||
_, res ->
|
||||
{:cont, res}
|
||||
end)
|
||||
|
||||
%{data | "tag" => pruned_tags}
|
||||
end
|
||||
|
||||
defp prune_tags(data), do: data
|
||||
|
||||
# some software, like Iceshrimp.NET, federates emoji reaction with (from its POV) remote emoji
|
||||
# with the source instance added to the name in AP as an @ postfix, similar to how it’s handled
|
||||
# in Akkoma’s REST API.
|
||||
# However, this leads to duplicated remote indicators being presented to our clients an can cause
|
||||
# issues when trying to split the values we receive from REST API. Thus just drop them here.
|
||||
defp drop_remote_indicator(%{"content" => emoji, "tag" => tag} = data) when is_list(tag) do
|
||||
if String.contains?(emoji, "@") do
|
||||
stripped_emoji = Emoji.stripped_name(emoji)
|
||||
[clean_emoji | _] = String.split(stripped_emoji, "@", parts: 2)
|
||||
|
||||
clean_tag =
|
||||
Enum.map(tag, fn
|
||||
%{"name" => ^stripped_emoji} = t -> %{t | "name" => clean_emoji}
|
||||
t -> t
|
||||
end)
|
||||
|
||||
%{data | "content" => ":" <> clean_emoji <> ":", "tag" => clean_tag}
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_remote_indicator(data), do: data
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
defmodule Pleroma.Repo.Migrations.EmojiReactDropRemotePartFromName do
|
||||
use Ecto.Migration
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
defp drop_remote_indicator(%{"content" => emoji, "tag" => _tag} = data) do
|
||||
if String.contains?(emoji, "@") do
|
||||
do_drop_remote_indicator(data)
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp do_drop_remote_indicator(%{"content" => emoji, "tag" => tag} = data) do
|
||||
stripped_emoji = Pleroma.Emoji.stripped_name(emoji)
|
||||
[clean_emoji | _] = String.split(stripped_emoji, "@", parts: 2)
|
||||
|
||||
clean_tag =
|
||||
Enum.map(tag, fn
|
||||
%{"name" => ^stripped_emoji} = t -> %{t | "name" => clean_emoji}
|
||||
t -> t
|
||||
end)
|
||||
|
||||
%{data | "content" => ":" <> clean_emoji <> ":", "tag" => clean_tag}
|
||||
end
|
||||
|
||||
defp prune_and_strip_tags(%{"content" => emoji, "tag" => tags} = data) do
|
||||
clean_emoji = Pleroma.Emoji.stripped_name(emoji)
|
||||
|
||||
pruned_tags =
|
||||
Enum.reduce_while(tags, [], fn
|
||||
%{"type" => "Emoji", "name" => name} = tag, res ->
|
||||
clean_name = Pleroma.Emoji.stripped_name(name)
|
||||
|
||||
if clean_name == clean_emoji do
|
||||
{:halt, [%{tag | "name" => clean_name}]}
|
||||
else
|
||||
{:cont, res}
|
||||
end
|
||||
|
||||
_, res ->
|
||||
{:cont, res}
|
||||
end)
|
||||
|
||||
%{data | "tag" => pruned_tags}
|
||||
end
|
||||
|
||||
def up() do
|
||||
Pleroma.Activity
|
||||
|> where(
|
||||
[a],
|
||||
fragment("?->>'type' = 'EmojiReact'", a.data) and
|
||||
fragment("jsonb_typeof(?->'content') = 'string'", a.data) and
|
||||
fragment("jsonb_typeof(?->'tag') = 'array'", a.data)
|
||||
)
|
||||
|> Pleroma.Repo.chunk_stream(600, :batches, timeout: :infinity)
|
||||
|> Stream.each(fn chunk ->
|
||||
Enum.reduce(chunk, {[], []}, fn %{id: id, data: data}, {ids, newdat} ->
|
||||
new_data =
|
||||
data
|
||||
|> prune_and_strip_tags()
|
||||
|> drop_remote_indicator()
|
||||
|
||||
if new_data == data do
|
||||
{ids, newdat}
|
||||
else
|
||||
# not sure why we get a string back from the db here and need to explicit convert it back
|
||||
{[FlakeId.from_string(id) | ids], [new_data | newdat]}
|
||||
end
|
||||
end)
|
||||
|> then(fn
|
||||
{[], []} ->
|
||||
IO.puts("Nothing in current batch")
|
||||
:ok
|
||||
|
||||
{ids, newdat} ->
|
||||
{upcnt, _} =
|
||||
Pleroma.Activity
|
||||
|> join(
|
||||
:inner,
|
||||
[a],
|
||||
news in fragment(
|
||||
"SELECT * FROM unnest(?::uuid[], ?::jsonb[]) AS news(id, new_data)",
|
||||
^ids,
|
||||
^newdat
|
||||
),
|
||||
on: a.id == news.id
|
||||
)
|
||||
|> update([_a, news], set: [data: news.new_data])
|
||||
|> Pleroma.Repo.update_all([], timeout: :infinity)
|
||||
|
||||
IO.puts("Fixed #{upcnt} reacts in current batch")
|
||||
end)
|
||||
end)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
def down() do
|
||||
# not reversible, but also shouldn’t cause any problems
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
|
@ -212,6 +212,66 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
|
|||
assert {:error, _} = Transmogrifier.handle_incoming(data)
|
||||
end
|
||||
|
||||
test "it strips colons from emoji object in tag" do
|
||||
shortcode = ":blobcatgoogly:"
|
||||
imgurl = "https://example.org/emoji/a.png"
|
||||
{data, _, _} = prepare_react(shortcode, imgurl)
|
||||
coloned_tag = Enum.map(data["tag"], fn tag -> %{tag | "name" => shortcode} end)
|
||||
data = %{data | "tag" => coloned_tag}
|
||||
|
||||
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||
|
||||
assert data["type"] == "EmojiReact"
|
||||
assert data["content"] == shortcode
|
||||
[%{"name" => tag_name}] = data["tag"]
|
||||
assert ":" <> tag_name <> ":" == shortcode
|
||||
end
|
||||
|
||||
test "it prunes tag to only the relevant emoji object" do
|
||||
shortcode = "blobcatgoogly"
|
||||
imgurl = "https://example.org/emoji/a.png"
|
||||
{data, _, _} = prepare_react(shortcode, imgurl)
|
||||
|
||||
ext_tag = [
|
||||
%{
|
||||
"type" => "Hashtag",
|
||||
"name" => "#cat",
|
||||
"href" => "https://example.org/hastags/cat"
|
||||
},
|
||||
emoji_object("evilcat", "https://example.org/evilcat.avif")
|
||||
| data["tag"]
|
||||
]
|
||||
|
||||
data = %{data | "tag" => ext_tag}
|
||||
refute match?([%{"type" => "Emoji"}], data["tag"])
|
||||
|
||||
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||
|
||||
assert match?([%{"type" => "Emoji", "name" => ^shortcode}], data["tag"])
|
||||
end
|
||||
|
||||
test "it strips pre-existing remote indicators" do
|
||||
shortcode = "blobcatgoogly@fedi.example.org"
|
||||
imgurl = "https://example.org/emoji/a.png"
|
||||
{data, _, _} = prepare_react(shortcode, imgurl)
|
||||
[clean_shortcode, _] = String.split(shortcode, "@", parts: 2)
|
||||
|
||||
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||
|
||||
assert data["content"] == ":" <> clean_shortcode <> ":"
|
||||
assert match?([%{"name" => ^clean_shortcode}], data["tag"])
|
||||
end
|
||||
|
||||
defp prepare_react(shortcode, imgurl, emoji_id \\ nil) do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user, local: false)
|
||||
{:ok, activity} = CommonAPI.post(user, %{status: "hello"})
|
||||
|
||||
emoji = emoji_object(shortcode, emoji_id, imgurl)
|
||||
data = react_with_custom(activity.data["object"], other_user.ap_id, emoji)
|
||||
{data, activity, other_user}
|
||||
end
|
||||
|
||||
defp emoji_object(shortcode, id \\ nil, url \\ "https://example.org/emoji.png") do
|
||||
%{
|
||||
"type" => "Emoji",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue