Merge remote-tracking branch 'pleroma/develop' into merge-pleroma

This commit is contained in:
marcin mikołajczak 2023-03-16 17:02:57 +01:00
commit 2427bf4e50
19 changed files with 668 additions and 69 deletions

View file

@ -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

View file

@ -51,6 +51,8 @@ def reload do
@doc "Returns the path of the emoji `name`."
@spec get(String.t()) :: String.t() | nil
def get(name) do
name = maybe_strip_name(name)
case :ets.lookup(@ets, name) do
[{_, path}] -> path
_ -> nil
@ -139,6 +141,57 @@ def is_unicode_emoji?(unquote(emoji)), do: true
def is_unicode_emoji?(_), do: false
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
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
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
emoji = maybe_strip_name(emoji)
tag =
tags
|> Enum.find(fn tag ->
tag["type"] == "Emoji" && !is_nil(tag["name"]) && tag["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"))

View file

@ -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
@ -66,13 +67,87 @@ 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
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", tag)
end
defp remote_custom_emoji_react(
%{data: %{"reactions" => existing_reactions}},
data,
emoji
) do
[emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
matching_reaction =
Enum.find(
existing_reactions,
fn [name, _, url] ->
if url != nil do
url = URI.parse(url)
url.host == instance && name == emoji_code
end
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 %{file: path} = emojo <- Emoji.get(emoji) do
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

View file

@ -5,8 +5,10 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
use Ecto.Schema
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
@ -19,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields
message_fields()
activity_fields()
embeds_many(:tag, TagValidator)
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,16 @@ 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 = Map.put_new(data, "tag", [])
case Object.normalize(data["object"]) do
%Object{} = object ->
data
|> CommonFixes.fix_activity_context(object)
|> CommonFixes.fix_object_action_recipients(object)
_ ->
data
end
end
@ -82,11 +90,31 @@ defp fix_emoji_qualification(data), do: data
defp validate_emoji(cng) do
content = get_field(cng, :content)
if Pleroma.Emoji.is_unicode_emoji?(content) do
if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(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.maybe_strip_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 +125,6 @@ defp validate_data(data_cng) do
|> validate_actor_presence()
|> validate_object_presence()
|> validate_emoji()
|> maybe_validate_tag_presence()
end
end

View file

@ -332,21 +332,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.maybe_strip_name(emoji)
url = maybe_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
@ -355,18 +363,40 @@ def add_emoji_reaction_to_object(
update_element_in_object("reaction", new_reactions, object, count)
end
defp maybe_emoji_url(
name,
%Activity{
data: %{
"tag" => [
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
]
}
}
),
do: url
defp maybe_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.maybe_strip_name(emoji)
reactions = get_cached_emoji_reactions(object)
url = maybe_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
@ -374,9 +404,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)
@ -538,17 +568,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.maybe_strip_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 """

View file

@ -162,6 +162,7 @@ def features do
"safe_dm_mentions"
end,
"pleroma_emoji_reactions",
"pleroma_custom_emoji_reactions",
"pleroma_chat_messages",
"email_list",
if Config.get([:instance, :show_reactions]) do

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
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
@ -157,7 +158,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

View file

@ -377,8 +377,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()
@ -841,11 +841,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

View file

@ -8,12 +8,20 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do
@impl Provider
def build_tags(%{user: user}) do
bio_tree = Floki.parse_fragment!(user.bio)
profile_tree =
user.bio
|> append_fields_tag(user.fields)
|> Floki.parse_fragment!()
(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 append_fields_tag(bio, fields) do
fields
|> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end)
end
end

View file

@ -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)

View file

@ -7,17 +7,30 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
alias Pleroma.Web.MastodonAPI.AccountView
def emoji_name(emoji, nil), do: emoji
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: Pleroma.Web.MediaProxy.url(url),
me: !!(user && user.ap_id in user_ap_ids)
}
end

View file

@ -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"
}
}
]
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -191,7 +191,47 @@ 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])
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])

View file

@ -36,16 +36,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)
@ -53,8 +63,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
@ -67,11 +84,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
@ -91,7 +107,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)
@ -103,19 +119,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

View file

@ -11,11 +11,24 @@ test "it renders all links with rel='me' from user bio" do
bio =
~s(<a href="https://some-link.com">https://some-link.com</a> <a rel="me" href="https://another-link.com">https://another-link.com</a> <link href="http://some.com"> <link rel="me" href="http://some3.com">)
user = insert(:user, %{bio: bio})
fields = [
%{
"name" => "profile",
"value" => ~S(<a rel="me" href="http://profile.com">http://profile.com</a>)
},
%{
"name" => "like",
"value" => ~S(<a href="http://cofe.io">http://cofe.io</a>)
},
%{"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: "https://another-link.com"], []}
{:link, [rel: "me", href: "https://another-link.com"], []},
{:link, [rel: "me", href: "http://profile.com"], []}
]
end
end

View file

@ -17,23 +17,113 @@ 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" => [
["👍", [other_user.ap_id], nil],
["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"] == [
%{
"account_ids" => [other_user.id],
"count" => 1,
"me" => false,
"name" => "👍",
"url" => nil
},
%{
"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 +136,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 +163,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 +320,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)