add custom emoji reaction support

This commit is contained in:
FloatingGhost 2022-06-08 02:42:44 +01:00
parent f19c93cdaa
commit ad0b8c095c
11 changed files with 76 additions and 46 deletions

View file

@ -115,7 +115,7 @@ defp update_emojis(emojis) do
|> String.split("\n") |> String.split("\n")
|> Enum.filter(fn line -> |> Enum.filter(fn line ->
line != "" and not String.starts_with?(line, "#") and line != "" and not String.starts_with?(line, "#") and
String.contains?(line, "fully-qualified") String.contains?(line, "qualified")
end) end)
|> Enum.map(fn line -> |> Enum.map(fn line ->
line line

View file

@ -144,6 +144,7 @@ def validate(%{"type" => type} = object, meta)
def validate(%{"type" => type} = object, meta) def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
ChatMessage Answer] do ChatMessage Answer] do
IO.inspect(object)
validator = validator =
case type do case type do
"Accept" -> AcceptRejectValidator "Accept" -> AcceptRejectValidator

View file

@ -65,4 +65,10 @@ defmacro status_object_fields do
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

View file

@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false @primary_key false
@emoji_regex ~r/:[A-Za-z_-]+:/
embedded_schema do embedded_schema do
quote do quote do
@ -19,6 +20,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()
end end
end end
@ -43,7 +45,8 @@ def cast_data(data) do
def changeset(struct, data) do def changeset(struct, data) do
struct struct
|> cast(data, __schema__(:fields)) |> cast(data, __schema__(:fields) -- [:tag])
|> cast_embed(:tag)
end end
defp fix(data) do defp fix(data) do
@ -52,6 +55,12 @@ defp fix(data) do
|> CommonFixes.fix_actor() |> CommonFixes.fix_actor()
|> CommonFixes.fix_activity_addressing() |> CommonFixes.fix_activity_addressing()
data = if Map.has_key?(data, "tag") do
data
else
Map.put(data, "tag", [])
end
with %Object{} = object <- Object.normalize(data["object"]) do with %Object{} = object <- Object.normalize(data["object"]) do
data data
|> CommonFixes.fix_activity_context(object) |> CommonFixes.fix_activity_context(object)
@ -63,12 +72,12 @@ defp fix(data) do
defp validate_emoji(cng) do defp validate_emoji(cng) do
content = get_field(cng, :content) content = get_field(cng, :content)
IO.inspect(Pleroma.Emoji.is_unicode_emoji?(content))
if Pleroma.Emoji.is_unicode_emoji?(content) do if Pleroma.Emoji.is_unicode_emoji?(content) || Regex.match?(@emoji_regex, content) do
cng cng
else else
cng cng
|> add_error(:content, "must be a single character emoji") |> add_error(:content, "is not a valid emoji")
end end
end end

View file

@ -77,7 +77,8 @@ defp maybe_federate(%Activity{} = activity, meta) do
{:ok, :not_federated} {:ok, :not_federated}
end end
else else
_e -> {:error, :badarg} _e ->
{:error, :badarg}
end end
end end
end end

View file

@ -269,7 +269,6 @@ def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, met
def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
reacted_object = Object.get_by_ap_id(object.data["object"]) reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object) Utils.add_emoji_reaction_to_object(object, reacted_object)
Notification.create_notifications(object) Notification.create_notifications(object)
{:ok, object, meta} {:ok, object, meta}

View file

@ -415,31 +415,31 @@ def handle_incoming(
end end
end end
@misskey_reactions %{
"like" => "👍",
"love" => "❤️",
"laugh" => "😆",
"hmm" => "🤔",
"surprise" => "😮",
"congrats" => "🎉",
"angry" => "💢",
"confused" => "😥",
"rip" => "😇",
"pudding" => "🍮",
"star" => ""
}
@doc "Rewrite misskey likes into EmojiReacts" @doc "Rewrite misskey likes into EmojiReacts"
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Like", "type" => "Like",
"_misskey_reaction" => reaction "_misskey_reaction" => reaction,
"tag" => _
} = data, } = data,
options options
) do ) do
data data
|> Map.put("type", "EmojiReact") |> Map.put("type", "EmojiReact")
|> Map.put("content", @misskey_reactions[reaction] || reaction) |> Map.put("content", reaction)
|> handle_incoming(options)
end
def handle_incoming(
%{
"type" => "Like",
"_misskey_reaction" => reaction,
} = data,
options
) do
data
|> Map.put("type", "EmojiReact")
|> Map.put("content", reaction)
|> handle_incoming(options) |> handle_incoming(options)
end end
@ -472,11 +472,11 @@ def handle_incoming(
def handle_incoming(%{"type" => type} = data, _options) def handle_incoming(%{"type" => type} = data, _options)
when type in ~w{Like EmojiReact Announce Add Remove} do when type in ~w{Like EmojiReact Announce Add Remove} do
with :ok <- ObjectValidator.fetch_actor_and_object(data), with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <- {:ok, activity, _meta} <- Pipeline.common_pipeline(data, local: false) do
Pipeline.common_pipeline(data, local: false) do
{:ok, activity} {:ok, activity}
else else
e -> {:error, e} e ->
{:error, e}
end end
end end

View file

@ -344,21 +344,22 @@ def update_element_in_object(property, element, object, count \\ nil) do
{:ok, Object.t()} | {:error, Ecto.Changeset.t()} {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_emoji_reaction_to_object( def add_emoji_reaction_to_object(
%Activity{data: %{"content" => emoji, "actor" => actor}}, %Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
object object
) do ) do
IO.inspect(emoji)
reactions = get_cached_emoji_reactions(object) reactions = get_cached_emoji_reactions(object)
emoji = stripped_emoji_name(emoji)
new_reactions = new_reactions =
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do case Enum.find_index(reactions, fn [candidate, _, _] -> emoji == candidate end) do
nil -> nil ->
reactions ++ [[emoji, [actor]]] reactions ++ [[emoji, [actor], emoji_url(emoji, activity)]]
index -> index ->
List.update_at( List.update_at(
reactions, reactions,
index, index,
fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
) )
end end
@ -367,8 +368,22 @@ 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 stripped_emoji_name(name) do
name
|> String.replace_leading(":", "")
|> String.replace_trailing(":", "")
end
defp emoji_url(name,
%Activity{
data: %{"tag" => [
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
]}
}), do: url
defp 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)
end end
def remove_emoji_reaction_from_object( def remove_emoji_reaction_from_object(
@ -376,9 +391,8 @@ def remove_emoji_reaction_from_object(
object object
) do ) do
reactions = get_cached_emoji_reactions(object) reactions = get_cached_emoji_reactions(object)
new_reactions = new_reactions =
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do case Enum.find_index(reactions, fn [candidate, _, _] -> emoji == candidate end) do
nil -> nil ->
reactions reactions
@ -386,9 +400,9 @@ def remove_emoji_reaction_from_object(
List.update_at( List.update_at(
reactions, reactions,
index, index,
fn [emoji, users] -> [emoji, List.delete(users, actor)] end fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
) )
|> Enum.reject(fn [_, users] -> Enum.empty?(users) end) |> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
end end
count = emoji_count(new_reactions) count = emoji_count(new_reactions)

View file

@ -304,7 +304,6 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
_e -> _e ->
nil nil
end end
emoji_reactions = emoji_reactions =
object.data object.data
|> Map.get("reactions", []) |> Map.get("reactions", [])
@ -312,8 +311,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
opts[:for], opts[:for],
Map.get(opts, :with_muted, false) Map.get(opts, :with_muted, false)
) )
|> Stream.map(fn {emoji, users} -> |> Stream.map(fn {emoji, users, url} ->
build_emoji_map(emoji, users, opts[:for]) build_emoji_map(emoji, users, url, opts[:for])
end) end)
|> Enum.to_list() |> Enum.to_list()
@ -569,10 +568,11 @@ defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_
end end
end end
defp build_emoji_map(emoji, users, current_user) do defp build_emoji_map(emoji, users, url, current_user) do
%{ %{
name: emoji, name: emoji,
count: length(users), count: length(users),
url: url,
me: !!(current_user && current_user.ap_id in users) me: !!(current_user && current_user.ap_id in users)
} }
end end

View file

@ -50,17 +50,17 @@ def filter_allowed_users(reactions, user, with_muted) do
if not with_muted, do: User.cached_muted_users_ap_ids(user), else: [] if not with_muted, do: User.cached_muted_users_ap_ids(user), else: []
end end
filter_emoji = fn emoji, users -> filter_emoji = fn emoji, users, url ->
case Enum.reject(users, &(&1 in exclude_ap_ids)) do case Enum.reject(users, &(&1 in exclude_ap_ids)) do
[] -> nil [] -> nil
users -> {emoji, users} users -> {emoji, users, url}
end end
end end
reactions reactions
|> Stream.map(fn |> Stream.map(fn
[emoji, users] when is_list(users) -> filter_emoji.(emoji, users) [emoji, users, url] when is_list(users) -> filter_emoji.(emoji, users, url)
{emoji, users} when is_list(users) -> filter_emoji.(emoji, users) {emoji, users, url} when is_list(users) -> filter_emoji.(emoji, users, url)
_ -> nil _ -> nil
end) end)
|> Stream.reject(&is_nil/1) |> Stream.reject(&is_nil/1)

View file

@ -11,13 +11,13 @@ def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do
render_many(emoji_reactions, __MODULE__, "show.json", opts) render_many(emoji_reactions, __MODULE__, "show.json", opts)
end end
def render("show.json", %{emoji_reaction: {emoji, user_ap_ids}, user: user}) do def render("show.json", %{emoji_reaction: {emoji, user_ap_ids, url}, user: user}) do
users = fetch_users(user_ap_ids) users = fetch_users(user_ap_ids)
%{ %{
name: emoji, name: emoji,
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: url,
me: !!(user && user.ap_id in user_ap_ids) me: !!(user && user.ap_id in user_ap_ids)
} }
end end