From d5d7648789847b943caf339732aeeed11cd2728d Mon Sep 17 00:00:00 2001 From: kPherox Date: Wed, 1 Feb 2023 20:24:58 +0000 Subject: [PATCH 1/7] feat: build rel me tags with profile fields --- lib/pleroma/web/metadata/providers/rel_me.ex | 18 +++++++++++++++--- .../web/metadata/providers/rel_me_test.exs | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex index f0bee85c80..e1890a050a 100644 --- a/lib/pleroma/web/metadata/providers/rel_me.ex +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -8,12 +8,24 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do @impl Provider def build_tags(%{user: user}) do - bio_tree = Floki.parse_fragment!(user.bio) + profile_tree = + Floki.parse_fragment!(user.bio) + |> prepend_fields_tag(user.fields) - (Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ - Floki.attribute(bio_tree, "a[rel~=me]", "href")) + (Floki.attribute(profile_tree, "link[rel~=me]", "href") ++ + Floki.attribute(profile_tree, "a[rel~=me]", "href")) |> Enum.map(fn link -> {:link, [rel: "me", href: link], []} end) end + + defp prepend_fields_tag(bio_tree, fields) do + fields + |> Enum.reduce(bio_tree, fn %{"value" => v}, tree -> + case Floki.parse_fragment(v) do + {:ok, [a | _]} -> [a | tree] + _ -> tree + end + end) + end end diff --git a/test/pleroma/web/metadata/providers/rel_me_test.exs b/test/pleroma/web/metadata/providers/rel_me_test.exs index cce4f3607b..b1177edb73 100644 --- a/test/pleroma/web/metadata/providers/rel_me_test.exs +++ b/test/pleroma/web/metadata/providers/rel_me_test.exs @@ -11,10 +11,23 @@ test "it renders all links with rel='me' from user bio" do bio = ~s(https://some-link.com https://another-link.com ) - user = insert(:user, %{bio: bio}) + fields = [ + %{ + "name" => "profile", + "value" => ~S(http://profile.com) + }, + %{ + "name" => "like", + "value" => ~S(http://cofe.io) + }, + %{"name" => "foo", "value" => "bar"} + ] + + user = insert(:user, %{bio: bio, fields: fields}) assert RelMe.build_tags(%{user: user}) == [ {:link, [rel: "me", href: "http://some3.com"], []}, + {:link, [rel: "me", href: "http://profile.com"], []}, {:link, [rel: "me", href: "https://another-link.com"], []} ] end From 787e30c5fd2f54a07db1bad84ef20aa9a4a406ab Mon Sep 17 00:00:00 2001 From: floatingghost Date: Sun, 4 Sep 2022 23:31:41 +0000 Subject: [PATCH 2/7] Allow reacting with remote emoji when they exist on the post (#200) Co-authored-by: FloatingGhost Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/200 --- lib/pleroma/web/activity_pub/builder.ex | 79 +++++++++- .../emoji_react_validator.ex | 55 +++++-- lib/pleroma/web/activity_pub/utils.ex | 70 ++++++-- .../web/mastodon_api/views/status_view.ex | 12 +- .../controllers/emoji_reaction_controller.ex | 21 ++- .../pleroma_api/views/emoji_reaction_view.ex | 18 ++- .../emoji_reaction_controller_test.exs | 149 +++++++++++++++++- 7 files changed, 363 insertions(+), 41 deletions(-) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 532047599f..4681b3dc9d 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI.ActivityDraft + alias Pleroma.Web.Endpoint require Pleroma.Constants @@ -54,13 +55,85 @@ def follow(follower, followed) do {:ok, data, []} end + defp unicode_emoji_react(_object, data, emoji) do + data + |> Map.put("content", emoji) + |> Map.put("type", "EmojiReact") + end + + defp add_emoji_content(data, emoji, url) do + 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) + ) + ]) + end + + defp remote_custom_emoji_react( + %{data: %{"reactions" => existing_reactions}}, + data, + emoji + ) do + [emoji_code, instance] = String.split(Emoji.stripped_name(emoji), "@") + + matching_reaction = + Enum.find( + existing_reactions, + fn [name, _, url] -> + url = URI.parse(url) + url.host == instance && name == emoji_code + end + ) + + if matching_reaction do + [name, _, url] = matching_reaction + add_emoji_content(data, name, url) + else + {:error, "Could not react"} + end + end + + defp remote_custom_emoji_react(_object, _data, _emoji) do + {:error, "Could not react"} + end + + defp local_custom_emoji_react(data, emoji) do + with %{} = emojo <- Emoji.get(emoji) do + path = emojo |> Map.get(:file) + url = "#{Endpoint.url()}#{path}" + add_emoji_content(data, emojo.code, url) + else + _ -> {:error, "Emoji does not exist"} + end + end + + defp custom_emoji_react(object, data, emoji) do + if String.contains?(emoji, "@") do + remote_custom_emoji_react(object, data, emoji) + else + local_custom_emoji_react(data, emoji) + end + end + @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()} def emoji_react(actor, object, emoji) do with {:ok, data, meta} <- object_action(actor, object) do data = - data - |> Map.put("content", emoji) - |> Map.put("type", "EmojiReact") + if Emoji.is_unicode_emoji?(emoji) do + unicode_emoji_react(object, data, emoji) + else + custom_emoji_react(object, data, emoji) + end {:ok, data, meta} 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 0858281e54..54efe5c42d 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 @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do use Ecto.Schema + alias Pleroma.Emoji alias Pleroma.Object alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes @@ -12,6 +13,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations @primary_key false + @emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/ embedded_schema do quote do @@ -19,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields message_fields() activity_fields() + tag_fields() end end @@ -43,7 +46,8 @@ def cast_data(data) do def changeset(struct, data) do struct - |> cast(data, __schema__(:fields)) + |> cast(data, __schema__(:fields) -- [:tag]) + |> cast_embed(:tag) end defp fix(data) do @@ -53,12 +57,21 @@ defp fix(data) do |> CommonFixes.fix_actor() |> CommonFixes.fix_activity_addressing() - with %Object{} = object <- Object.normalize(data["object"]) do - data - |> CommonFixes.fix_activity_context(object) - |> CommonFixes.fix_object_action_recipients(object) - else - _ -> data + data = + if Map.has_key?(data, "tag") do + data + else + Map.put(data, "tag", []) + end + + case Object.normalize(data["object"]) do + %Object{} = object -> + data + |> CommonFixes.fix_activity_context(object) + |> CommonFixes.fix_object_action_recipients(object) + + _ -> + data end end @@ -79,14 +92,37 @@ 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 Pleroma.Emoji.is_unicode_emoji?(content) do + if Emoji.is_unicode_emoji?(content) || matches_shortcode?(content) do cng else cng - |> add_error(:content, "must be a single character emoji") + |> add_error(:content, "is not a valid emoji") + end + end + + defp maybe_validate_tag_presence(cng) do + content = get_field(cng, :content) + + if Emoji.is_unicode_emoji?(content) do + cng + else + tag = get_field(cng, :tag) + emoji_name = Emoji.stripped_name(content) + + case tag do + [%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] -> + cng + + _ -> + cng + |> add_error(:tag, "does not contain an Emoji tag") + end end end @@ -97,5 +133,6 @@ defp validate_data(data_cng) do |> validate_actor_presence() |> validate_object_presence() |> validate_emoji() + |> maybe_validate_tag_presence() end end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index b898d6fe82..11f1b95c54 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -325,21 +325,29 @@ def update_element_in_object(property, element, object, count \\ nil) do {:ok, Object.t()} | {:error, Ecto.Changeset.t()} def add_emoji_reaction_to_object( - %Activity{data: %{"content" => emoji, "actor" => actor}}, + %Activity{data: %{"content" => emoji, "actor" => actor}} = activity, object ) do reactions = get_cached_emoji_reactions(object) + emoji = Pleroma.Emoji.stripped_name(emoji) + url = emoji_url(emoji, activity) new_reactions = - case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do + case Enum.find_index(reactions, fn [candidate, _, candidate_url] -> + if is_nil(candidate_url) do + emoji == candidate + else + url == candidate_url + end + end) do nil -> - reactions ++ [[emoji, [actor]]] + reactions ++ [[emoji, [actor], url]] index -> List.update_at( reactions, index, - fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end + fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end ) end @@ -348,18 +356,40 @@ def add_emoji_reaction_to_object( update_element_in_object("reaction", new_reactions, object, count) 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 - 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 def remove_emoji_reaction_from_object( - %Activity{data: %{"content" => emoji, "actor" => actor}}, + %Activity{data: %{"content" => emoji, "actor" => actor}} = activity, object ) do + emoji = Pleroma.Emoji.stripped_name(emoji) reactions = get_cached_emoji_reactions(object) + url = emoji_url(emoji, activity) new_reactions = - case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do + case Enum.find_index(reactions, fn [candidate, _, candidate_url] -> + if is_nil(candidate_url) do + emoji == candidate + else + url == candidate_url + end + end) do nil -> reactions @@ -367,9 +397,9 @@ def remove_emoji_reaction_from_object( List.update_at( reactions, 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 count = emoji_count(new_reactions) @@ -489,17 +519,37 @@ def fetch_latest_undo(%User{ap_id: ap_id}) do def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do %{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id) + emoji = Pleroma.Emoji.maybe_quote(emoji) "EmojiReact" |> Activity.Queries.by_type() |> where(actor: ^ap_id) - |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) + |> custom_emoji_discriminator(emoji) |> Activity.Queries.by_object_id(object_ap_id) |> order_by([activity], fragment("? desc nulls last", activity.id)) |> limit(1) |> Repo.one() end + defp custom_emoji_discriminator(query, emoji) do + if String.contains?(emoji, "@") do + stripped = Pleroma.Emoji.stripped_name(emoji) + [name, domain] = String.split(stripped, "@") + domain_pattern = "%" <> domain <> "%" + emoji_pattern = Pleroma.Emoji.maybe_quote(name) + + query + |> where([activity], fragment("?->>'content' = ? + AND EXISTS ( + SELECT FROM jsonb_array_elements(?->'tag') elem + WHERE elem->>'id' ILIKE ? + )", activity.data, ^emoji_pattern, activity.data, ^domain_pattern)) + else + query + |> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji)) + end + end + #### Announce-related helpers @doc """ diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 0a8c98b442..95efd9fd0c 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -340,8 +340,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} opts[:for], Map.get(opts, :with_muted, false) ) - |> Stream.map(fn {emoji, users} -> - build_emoji_map(emoji, users, opts[:for]) + |> Stream.map(fn {emoji, users, url} -> + build_emoji_map(emoji, users, url, opts[:for]) end) |> Enum.to_list() @@ -702,11 +702,13 @@ defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_ end end - defp build_emoji_map(emoji, users, current_user) do + defp build_emoji_map(emoji, users, url, current_user) do %{ - name: emoji, + name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url), count: length(users), - me: !!(current_user && current_user.ap_id in users) + url: MediaProxy.url(url), + me: !!(current_user && current_user.ap_id in users), + account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end) } end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex index 78fd0b2197..e095fa04a3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex @@ -50,29 +50,35 @@ def filter_allowed_users(reactions, user, with_muted) do if not with_muted, do: User.cached_muted_users_ap_ids(user), else: [] end - filter_emoji = fn emoji, users -> + filter_emoji = fn emoji, users, url -> case Enum.reject(users, &(&1 in exclude_ap_ids)) do [] -> nil - users -> {emoji, users} + users -> {emoji, users, url} end end reactions |> Stream.map(fn - [emoji, users] when is_list(users) -> filter_emoji.(emoji, users) - {emoji, users} when is_list(users) -> filter_emoji.(emoji, users) + [emoji, users, url] when is_list(users) -> filter_emoji.(emoji, users, url) + {emoji, users, url} when is_list(users) -> filter_emoji.(emoji, users, url) + {emoji, users} when is_list(users) -> filter_emoji.(emoji, users, nil) _ -> nil end) |> Stream.reject(&is_nil/1) end defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do - Enum.filter(reactions, fn [e, _] -> e == emoji end) + Enum.filter(reactions, fn [e, _, _] -> e == emoji end) end defp filter(reactions, _), do: reactions def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do + emoji = + emoji + |> Pleroma.Emoji.fully_qualify_emoji() + |> Pleroma.Emoji.maybe_quote() + with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do activity = Activity.get_by_id(activity_id) @@ -83,6 +89,11 @@ def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) d end def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do + emoji = + emoji + |> Pleroma.Emoji.fully_qualify_emoji() + |> Pleroma.Emoji.maybe_quote() + with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do activity = Activity.get_by_id(activity_id) 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 68ebd8292a..67db1a5ad9 100644 --- a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex +++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex @@ -7,17 +7,31 @@ 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) + + if url.host == Pleroma.Web.Endpoint.host() do + emoji + else + "#{emoji}@#{url.host}" + end + end + def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do render_many(emoji_reactions, __MODULE__, "show.json", opts) 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) %{ - name: emoji, + name: emoji_name(emoji, url), count: length(users), accounts: render(AccountView, "index.json", users: users, for: user), + url: MediaProxy.url(url), me: !!(user && user.ap_id in user_ap_ids) } end diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs index 77c75b560a..6af68cb527 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -17,23 +17,101 @@ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do user = insert(:user) other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + note = insert(:note, user: user, data: %{"reactions" => [["👍", [other_user.ap_id], nil]]}) + activity = insert(:note_activity, note: note, user: user) result = conn |> assign(:user, other_user) |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) - |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕") + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/\u26A0") |> json_response_and_validate_schema(200) - # We return the status, but this our implementation detail. assert %{"id" => id} = result assert to_string(activity.id) == id assert result["pleroma"]["emoji_reactions"] == [ - %{"name" => "☕", "count" => 1, "me" => true} + %{ + "name" => "👍", + "count" => 1, + "me" => true, + "url" => nil, + "account_ids" => [other_user.id] + }, + %{ + "name" => "\u26A0\uFE0F", + "count" => 1, + "me" => true, + "url" => nil, + "account_ids" => [other_user.id] + } ] + {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + + ObanHelpers.perform_all() + + # Reacting with a custom emoji + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:dinosaur:") + |> json_response_and_validate_schema(200) + + assert %{"id" => id} = result + assert to_string(activity.id) == id + + assert result["pleroma"]["emoji_reactions"] == [ + %{ + "name" => "dinosaur", + "count" => 1, + "me" => true, + "url" => "http://localhost:4001/emoji/dino walking.gif", + "account_ids" => [other_user.id] + } + ] + + # Reacting with a remote emoji + note = + insert(:note, + user: user, + data: %{"reactions" => [["wow", [other_user.ap_id], "https://remote/emoji/wow"]]} + ) + + activity = insert(:note_activity, note: note, user: user) + + result = + conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:") + |> json_response(200) + + assert result["pleroma"]["emoji_reactions"] == [ + %{ + "name" => "wow@remote", + "count" => 2, + "me" => true, + "url" => "https://remote/emoji/wow", + "account_ids" => [user.id, other_user.id] + } + ] + + # Reacting with a remote custom emoji that hasn't been reacted with yet + note = + insert(:note, + user: user + ) + + activity = insert(:note_activity, note: note, user: user) + + assert conn + |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"])) + |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:") + |> json_response(400) + # Reacting with a non-emoji assert conn |> assign(:user, other_user) @@ -46,8 +124,21 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do user = insert(:user) other_user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"}) + note = + insert(:note, + user: user, + data: %{"reactions" => [["wow", [user.ap_id], "https://remote/emoji/wow"]]} + ) + + activity = insert(:note_activity, note: note, user: user) + + ObanHelpers.perform_all() + {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:") + + {:ok, _reaction_activity} = + CommonAPI.react_with_emoji(activity.id, other_user, ":wow@remote:") ObanHelpers.perform_all() @@ -60,11 +151,47 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do assert %{"id" => id} = json_response_and_validate_schema(result, 200) assert to_string(activity.id) == id + # Remove custom emoji + + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:dinosaur:") + + assert %{"id" => id} = json_response_and_validate_schema(result, 200) + assert to_string(activity.id) == id + ObanHelpers.perform_all() object = Object.get_by_ap_id(activity.data["object"]) - assert object.data["reaction_count"] == 0 + assert object.data["reaction_count"] == 2 + + # Remove custom remote emoji + result = + conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:wow@remote:") + |> json_response(200) + + assert result["pleroma"]["emoji_reactions"] == [ + %{ + "name" => "wow@remote", + "count" => 1, + "me" => false, + "url" => "https://remote/emoji/wow", + "account_ids" => [user.id] + } + ] + + # Remove custom remote emoji that hasn't been reacted with yet + assert conn + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) + |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/:zoop@remote:") + |> json_response(400) end test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do @@ -181,7 +308,15 @@ test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") - assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = + assert [ + %{ + "name" => "🎅", + "count" => 1, + "accounts" => [represented_user], + "me" => false, + "url" => nil + } + ] = conn |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅") |> json_response_and_validate_schema(200) From 4b85d1c617e0d3d482dffe59e43c69d717d5a13f Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Sun, 18 Dec 2022 21:52:19 +0300 Subject: [PATCH 3/7] Allow custom emoji reactions: Fix tests, mixed custom and unicode reactions --- lib/pleroma/emoji.ex | 54 +++++++++++++++++ lib/pleroma/web/activity_pub/builder.ex | 6 +- .../object_validators/common_fields.ex | 7 +++ .../mastodon_api/views/notification_view.ex | 5 +- test/fixtures/custom-emoji-reaction.json | 28 +++++++++ .../emoji_react_handling_test.exs | 60 ++++++++++++++++++- .../web/activity_pub/side_effects_test.exs | 2 +- .../emoji_react_handling_test.exs | 53 +++++++++++++++- .../views/notification_view_test.exs | 3 +- .../mastodon_api/views/status_view_test.exs | 42 +++++++++---- 10 files changed, 240 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/custom-emoji-reaction.json diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex index dd65d56ae4..749b4f4cae 100644 --- a/lib/pleroma/emoji.ex +++ b/lib/pleroma/emoji.ex @@ -51,6 +51,15 @@ 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 + case :ets.lookup(@ets, name) do [{_, path}] -> path _ -> nil @@ -139,6 +148,51 @@ 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 + + def stripped_name(name), do: name + + def maybe_quote(name) when is_binary(name) do + if is_unicode_emoji?(name) do + name + else + if String.starts_with?(name, ":") do + name + else + ":#{name}:" + end + end + end + + 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 + tag = + tags + |> Enum.find(fn tag -> tag["type"] == "Emoji" && tag["name"] == stripped_name(emoji) end) + + if is_nil(tag) do + nil + else + tag + |> Map.get("icon") + |> Map.get("url") + end + end + + def emoji_url(_), do: nil + + def emoji_name_with_instance(name, url) do + url = url |> URI.parse() |> Map.get(:host) + "#{name}@#{url}" + end + emoji_qualification_map = emojis |> Enum.filter(&String.contains?(&1, "\uFE0F")) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index 4681b3dc9d..9553bde232 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -90,8 +90,10 @@ defp remote_custom_emoji_react( Enum.find( existing_reactions, fn [name, _, url] -> - url = URI.parse(url) - url.host == instance && name == emoji_code + if url != nil do + url = URI.parse(url) + url.host == instance && name == emoji_code + end end ) 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 7b60c139ad..904fdecedc 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -58,10 +58,17 @@ 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/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index b5b5b23764..314246d3e2 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.CommonAPI + alias Pleroma.Web.MediaProxy alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView @@ -145,7 +146,9 @@ defp put_report(response, activity) do end defp put_emoji(response, activity) do - Map.put(response, :emoji, activity.data["content"]) + response + |> Map.put(:emoji, activity.data["content"]) + |> Map.put(:emoji_url, MediaProxy.url(Pleroma.Emoji.emoji_url(activity.data))) end defp put_chat_message(response, activity, reading_user, opts) do diff --git a/test/fixtures/custom-emoji-reaction.json b/test/fixtures/custom-emoji-reaction.json new file mode 100644 index 0000000000..003de0511e --- /dev/null +++ b/test/fixtures/custom-emoji-reaction.json @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Hashtag": "as:Hashtag" + } + ], + "type": "Like", + "id": "https://misskey.local.live/likes/917ocsybgp", + "actor": "https://misskey.local.live/users/8x8yep20u2", + "object": "https://pleroma.local.live/objects/89937a53-2692-4631-bb62-770091267391", + "content": ":hanapog:", + "_misskey_reaction": ":hanapog:", + "tag": [ + { + "id": "https://misskey.local.live/emojis/hanapog", + "type": "Emoji", + "name": ":hanapog:", + "updated": "2022-06-07T12:00:05.773Z", + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd" + } + } + ] +} diff --git a/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs index bbdb09c4cd..9bb291a38a 100644 --- a/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs +++ b/test/pleroma/web/activity_pub/object_validators/emoji_react_handling_test.exs @@ -38,16 +38,70 @@ test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emo assert {:content, {"can't be blank", [validation: :required]}} in cng.errors end - test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do + test "it is valid when custom emoji is used", %{valid_emoji_react: valid_emoji_react} do without_emoji_content = valid_emoji_react - |> Map.put("content", "x") + |> Map.put("content", ":hello:") + |> Map.put("tag", [ + %{ + "type" => "Emoji", + "name" => ":hello:", + "icon" => %{"url" => "http://somewhere", "type" => "Image"} + } + ]) + + {:ok, _, _} = ObjectValidator.validate(without_emoji_content, []) + end + + test "it is not valid when custom emoji don't have a matching tag", %{ + valid_emoji_react: valid_emoji_react + } do + without_emoji_content = + valid_emoji_react + |> Map.put("content", ":hello:") + |> Map.put("tag", [ + %{ + "type" => "Emoji", + "name" => ":whoops:", + "icon" => %{"url" => "http://somewhere", "type" => "Image"} + } + ]) {:error, cng} = ObjectValidator.validate(without_emoji_content, []) refute cng.valid? - assert {:content, {"must be a single character emoji", []}} in cng.errors + assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors + end + + test "it is not valid when custom emoji have no tags", %{ + valid_emoji_react: valid_emoji_react + } do + without_emoji_content = + valid_emoji_react + |> Map.put("content", ":hello:") + |> Map.put("tag", []) + + {:error, cng} = ObjectValidator.validate(without_emoji_content, []) + + refute cng.valid? + + assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors + end + + test "it is not valid when custom emoji doesn't match a shortcode format", %{ + valid_emoji_react: valid_emoji_react + } do + without_emoji_content = + valid_emoji_react + |> Map.put("content", "hello") + |> Map.put("tag", []) + + {:error, cng} = ObjectValidator.validate(without_emoji_content, []) + + refute cng.valid? + + assert {:tag, {"does not contain an Emoji tag", []}} in cng.errors end end end diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index b24831e854..6820e23d07 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -453,7 +453,7 @@ test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} object = Object.get_by_ap_id(emoji_react.data["object"]) assert object.data["reaction_count"] == 1 - assert ["👌", [user.ap_id]] in object.data["reactions"] + assert ["👌", [user.ap_id], nil] in object.data["reactions"] end test "creates a notification", %{emoji_react: emoji_react, poster: poster} do diff --git a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs index 83bf59c6f3..f2e1cefa3e 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/emoji_react_handling_test.exs @@ -34,7 +34,56 @@ test "it works for incoming emoji reactions" do object = Object.get_by_ap_id(data["object"]) assert object.data["reaction_count"] == 1 - assert match?([["👌", _]], object.data["reactions"]) + assert match?([["👌", _, nil]], object.data["reactions"]) + end + + test "it works for incoming custom emoji reactions" do + user = insert(:user) + other_user = insert(:user, local: false) + {:ok, activity} = CommonAPI.post(user, %{status: "hello"}) + + data = + File.read!("test/fixtures/custom-emoji-reaction.json") + |> Jason.decode!() + |> Map.put("object", activity.data["object"]) + |> Map.put("actor", other_user.ap_id) + + {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) + + assert data["actor"] == other_user.ap_id + assert data["type"] == "EmojiReact" + assert data["id"] == "https://misskey.local.live/likes/917ocsybgp" + assert data["object"] == activity.data["object"] + assert data["content"] == ":hanapog:" + + assert data["tag"] == [ + %{ + "id" => "https://misskey.local.live/emojis/hanapog", + "type" => "Emoji", + "name" => "hanapog", + "updated" => "2022-06-07T12:00:05.773Z", + "icon" => %{ + "type" => "Image", + "url" => + "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd" + } + } + ] + + object = Object.get_by_ap_id(data["object"]) + + assert object.data["reaction_count"] == 1 + + assert match?( + [ + [ + "hanapog", + _, + "https://misskey.local.live/files/webpublic-8f8a9768-7264-4171-88d6-2356aabeadcd" + ] + ], + object.data["reactions"] + ) end test "it works for incoming unqualified emoji reactions" do @@ -65,7 +114,7 @@ test "it works for incoming unqualified emoji reactions" do object = Object.get_by_ap_id(data["object"]) assert object.data["reaction_count"] == 1 - assert match?([[^emoji, _]], object.data["reactions"]) + assert match?([[^emoji, _, _]], object.data["reactions"]) end test "it reject invalid emoji reactions" do 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 6ea8946915..e52985f8d9 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -190,7 +190,8 @@ test "EmojiReact notification" do emoji: "☕", 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) + created_at: Utils.to_masto_date(notification.inserted_at), + emoji_url: nil } test_notifications_rendering([notification], user, [expected]) diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index f76b115b7a..a1f3e3e1dd 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -35,16 +35,26 @@ test "has an emoji reaction list" do {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"}) {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, ":dinosaur:") {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕") + {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:") + activity = Repo.get(Activity, activity.id) status = StatusView.render("show.json", activity: activity) assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: false}, - %{name: "🍵", count: 1, me: false} + %{name: "☕", count: 2, me: false, url: nil, account_ids: [other_user.id, user.id]}, + %{ + count: 2, + me: false, + name: "dinosaur", + url: "http://localhost:4001/emoji/dino walking.gif", + account_ids: [other_user.id, user.id] + }, + %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} ] status = StatusView.render("show.json", activity: activity, for: user) @@ -52,8 +62,15 @@ test "has an emoji reaction list" do assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec()) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: true}, - %{name: "🍵", count: 1, me: false} + %{name: "☕", count: 2, me: true, url: nil, account_ids: [other_user.id, user.id]}, + %{ + count: 2, + me: true, + name: "dinosaur", + url: "http://localhost:4001/emoji/dino walking.gif", + account_ids: [other_user.id, user.id] + }, + %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]} ] end @@ -66,11 +83,10 @@ test "works correctly with badly formatted emojis" do |> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}}) activity = Activity.get_by_id(activity.id) - status = StatusView.render("show.json", activity: activity, for: user) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: true} + %{name: "☕", count: 1, me: true, url: nil, account_ids: [user.id]} ] end @@ -90,7 +106,7 @@ test "doesn't show reactions from muted and blocked users" do status = StatusView.render("show.json", activity: activity) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: false} + %{name: "☕", count: 1, me: false, url: nil, account_ids: [other_user.id]} ] status = StatusView.render("show.json", activity: activity, for: user) @@ -102,19 +118,25 @@ test "doesn't show reactions from muted and blocked users" do status = StatusView.render("show.json", activity: activity) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 2, me: false} + %{ + name: "☕", + count: 2, + me: false, + url: nil, + account_ids: [third_user.id, other_user.id] + } ] status = StatusView.render("show.json", activity: activity, for: user) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: false} + %{name: "☕", count: 1, me: false, url: nil, account_ids: [third_user.id]} ] status = StatusView.render("show.json", activity: activity, for: other_user) assert status[:pleroma][:emoji_reactions] == [ - %{name: "☕", count: 1, me: true} + %{name: "☕", count: 1, me: true, url: nil, account_ids: [other_user.id]} ] end From 8d3b29aaba1a1879c9495ae15178b6e16ffc5faf Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Wed, 4 Jan 2023 21:13:51 +0300 Subject: [PATCH 4/7] Allow custom emoji reactions: add test for mixed emoji react, fix credo errors --- .../web/mastodon_api/views/notification_view.ex | 2 +- .../controllers/emoji_reaction_controller_test.exs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 314246d3e2..2a51f37559 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -14,10 +14,10 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MediaProxy alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.MediaProxy alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs index 6af68cb527..d81b47a6b3 100644 --- a/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/emoji_reaction_controller_test.exs @@ -76,7 +76,12 @@ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do note = insert(:note, user: user, - data: %{"reactions" => [["wow", [other_user.ap_id], "https://remote/emoji/wow"]]} + data: %{ + "reactions" => [ + ["👍", [other_user.ap_id], nil], + ["wow", [other_user.ap_id], "https://remote/emoji/wow"] + ] + } ) activity = insert(:note_activity, note: note, user: user) @@ -89,6 +94,13 @@ test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do |> json_response(200) assert result["pleroma"]["emoji_reactions"] == [ + %{ + "account_ids" => [other_user.id], + "count" => 1, + "me" => false, + "name" => "👍", + "url" => nil + }, %{ "name" => "wow@remote", "count" => 2, From 8e072baed0744c8e4e29f54c608f3f29abb4fba8 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 5 Mar 2023 08:41:15 +0100 Subject: [PATCH 5/7] docs: Be more explicit about the level of compatibility of OTP releases --- docs/installation/otp_en.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 8c02201e67..f2812346b1 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -2,15 +2,16 @@ {! backend/installation/otp_vs_from_source.include !} -This guide covers a installation using an OTP release. To install Pleroma from source, please check out the corresponding guide for your distro. +This guide covers a installation using OTP releases as built by the Pleroma project, it is meant as a fallback to distribution packages/recipes which are the preferred installation method. +To install Pleroma from source, please check out the corresponding guide for your distro. ## Pre-requisites -* A machine running Linux with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and `x86_64`, `aarch64` or `armv7l` CPU, you have root access to. If you are not sure if it's compatible see [Detecting flavour section](#detecting-flavour) below +* A machine you have root access to running Debian GNU/Linux or compatible (eg. Ubuntu), or Alpine on `x86_64`, `aarch64` or `armv7l` CPU. If you are not sure what you are running see [Detecting flavour section](#detecting-flavour) below * A (sub)domain pointed to the machine -You will be running commands as root. If you aren't root already, please elevate your privileges by executing `sudo su`/`su`. +You will be running commands as root. If you aren't root already, please elevate your privileges by executing `sudo -i`/`su`. -While in theory OTP releases are possbile to install on any compatible machine, for the sake of simplicity this guide focuses only on Debian/Ubuntu and Alpine. +Similarly to other binaries, OTP releases tend to be only compatible with the distro they are built on, as such this guide focuses only on Debian/Ubuntu and Alpine. ### Detecting flavour @@ -19,7 +20,7 @@ Paste the following into the shell: arch="$(uname -m)";if [ "$arch" = "x86_64" ];then arch="amd64";elif [ "$arch" = "armv7l" ];then arch="arm";elif [ "$arch" = "aarch64" ];then arch="arm64";else echo "Unsupported arch: $arch">&2;fi;if getconf GNU_LIBC_VERSION>/dev/null;then libc_postfix="";elif [ "$(ldd 2>&1|head -c 9)" = "musl libc" ];then libc_postfix="-musl";elif [ "$(find /lib/libc.musl*|wc -l)" ];then libc_postfix="-musl";else echo "Unsupported libc">&2;fi;echo "$arch$libc_postfix" ``` -If your platform is supported the output will contain the flavour string, you will need it later. If not, this just means that we don't build releases for your platform, you can still try installing from source. +This should give your flavour string. If not this just means that we don't build releases for your platform, you can still try installing from source. ### Installing the required packages From 2c2ea16b50a7b77b1d3f58722e45720dcf3c51b9 Mon Sep 17 00:00:00 2001 From: Alexander Tumin Date: Thu, 2 Mar 2023 10:09:13 +0300 Subject: [PATCH 6/7] 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 749b4f4cae..43a3447c34 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 9553bde232..8eab3a2413 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 904fdecedc..7b60c139ad 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 54efe5c42d..a0b82b3257 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 11f1b95c54..db7f79daca 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 abf7f29ab3..efd2a0af62 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 67db1a5ad9..6df4ab9d0d 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 e52985f8d9..ddbe4557f1 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) From 83c7415803774a4c56c718f5ebed2b9714ffdfcf Mon Sep 17 00:00:00 2001 From: kPherox Date: Wed, 15 Mar 2023 23:55:24 +0900 Subject: [PATCH 7/7] fix: append field values to bio before parsing --- lib/pleroma/web/metadata/providers/rel_me.ex | 14 +++++--------- .../pleroma/web/metadata/providers/rel_me_test.exs | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/web/metadata/providers/rel_me.ex b/lib/pleroma/web/metadata/providers/rel_me.ex index e1890a050a..eabd8cb001 100644 --- a/lib/pleroma/web/metadata/providers/rel_me.ex +++ b/lib/pleroma/web/metadata/providers/rel_me.ex @@ -9,8 +9,9 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do @impl Provider def build_tags(%{user: user}) do profile_tree = - Floki.parse_fragment!(user.bio) - |> prepend_fields_tag(user.fields) + user.bio + |> append_fields_tag(user.fields) + |> Floki.parse_fragment!() (Floki.attribute(profile_tree, "link[rel~=me]", "href") ++ Floki.attribute(profile_tree, "a[rel~=me]", "href")) @@ -19,13 +20,8 @@ def build_tags(%{user: user}) do end) end - defp prepend_fields_tag(bio_tree, fields) do + defp append_fields_tag(bio, fields) do fields - |> Enum.reduce(bio_tree, fn %{"value" => v}, tree -> - case Floki.parse_fragment(v) do - {:ok, [a | _]} -> [a | tree] - _ -> tree - end - end) + |> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end) end end diff --git a/test/pleroma/web/metadata/providers/rel_me_test.exs b/test/pleroma/web/metadata/providers/rel_me_test.exs index b1177edb73..793669037f 100644 --- a/test/pleroma/web/metadata/providers/rel_me_test.exs +++ b/test/pleroma/web/metadata/providers/rel_me_test.exs @@ -27,8 +27,8 @@ test "it renders all links with rel='me' from user bio" do assert RelMe.build_tags(%{user: user}) == [ {:link, [rel: "me", href: "http://some3.com"], []}, - {:link, [rel: "me", href: "http://profile.com"], []}, - {:link, [rel: "me", href: "https://another-link.com"], []} + {:link, [rel: "me", href: "https://another-link.com"], []}, + {:link, [rel: "me", href: "http://profile.com"], []} ] end end