Allow custom emoji reactions: Fix tests, mixed custom and unicode reactions
This commit is contained in:
parent
787e30c5fd
commit
4b85d1c617
@ -51,6 +51,15 @@ defmodule Pleroma.Emoji do
|
|||||||
@doc "Returns the path of the emoji `name`."
|
@doc "Returns the path of the emoji `name`."
|
||||||
@spec get(String.t()) :: String.t() | nil
|
@spec get(String.t()) :: String.t() | nil
|
||||||
def get(name) do
|
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
|
case :ets.lookup(@ets, name) do
|
||||||
[{_, path}] -> path
|
[{_, path}] -> path
|
||||||
_ -> nil
|
_ -> nil
|
||||||
@ -139,6 +148,51 @@ defmodule Pleroma.Emoji do
|
|||||||
|
|
||||||
def is_unicode_emoji?(_), do: false
|
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 =
|
emoji_qualification_map =
|
||||||
emojis
|
emojis
|
||||||
|> Enum.filter(&String.contains?(&1, "\uFE0F"))
|
|> Enum.filter(&String.contains?(&1, "\uFE0F"))
|
||||||
|
@ -90,8 +90,10 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||||||
Enum.find(
|
Enum.find(
|
||||||
existing_reactions,
|
existing_reactions,
|
||||||
fn [name, _, url] ->
|
fn [name, _, url] ->
|
||||||
url = URI.parse(url)
|
if url != nil do
|
||||||
url.host == instance && name == emoji_code
|
url = URI.parse(url)
|
||||||
|
url.host == instance && name == emoji_code
|
||||||
|
end
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,10 +58,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
|||||||
field(:like_count, :integer, default: 0)
|
field(:like_count, :integer, default: 0)
|
||||||
field(:announcement_count, :integer, default: 0)
|
field(:announcement_count, :integer, default: 0)
|
||||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||||
|
field(:quoteUri, ObjectValidators.ObjectID)
|
||||||
field(:url, ObjectValidators.Uri)
|
field(:url, ObjectValidators.Uri)
|
||||||
|
|
||||||
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
||||||
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
|
||||||
|
@ -14,6 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||||||
alias Pleroma.Web.AdminAPI.Report
|
alias Pleroma.Web.AdminAPI.Report
|
||||||
alias Pleroma.Web.AdminAPI.ReportView
|
alias Pleroma.Web.AdminAPI.ReportView
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
|
alias Pleroma.Web.MediaProxy
|
||||||
alias Pleroma.Web.MastodonAPI.AccountView
|
alias Pleroma.Web.MastodonAPI.AccountView
|
||||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
@ -145,7 +146,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp put_emoji(response, activity) do
|
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
|
end
|
||||||
|
|
||||||
defp put_chat_message(response, activity, reading_user, opts) do
|
defp put_chat_message(response, activity, reading_user, opts) do
|
||||||
|
28
test/fixtures/custom-emoji-reaction.json
vendored
Normal file
28
test/fixtures/custom-emoji-reaction.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -38,16 +38,70 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactHandlingTest do
|
|||||||
assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
|
assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
|
||||||
end
|
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 =
|
without_emoji_content =
|
||||||
valid_emoji_react
|
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, [])
|
{:error, cng} = ObjectValidator.validate(without_emoji_content, [])
|
||||||
|
|
||||||
refute cng.valid?
|
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
|
end
|
||||||
end
|
end
|
||||||
|
@ -453,7 +453,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
|
|||||||
object = Object.get_by_ap_id(emoji_react.data["object"])
|
object = Object.get_by_ap_id(emoji_react.data["object"])
|
||||||
|
|
||||||
assert object.data["reaction_count"] == 1
|
assert object.data["reaction_count"] == 1
|
||||||
assert ["👌", [user.ap_id]] in object.data["reactions"]
|
assert ["👌", [user.ap_id], nil] in object.data["reactions"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
|
test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
|
||||||
|
@ -34,7 +34,56 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
|
|||||||
object = Object.get_by_ap_id(data["object"])
|
object = Object.get_by_ap_id(data["object"])
|
||||||
|
|
||||||
assert object.data["reaction_count"] == 1
|
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
|
end
|
||||||
|
|
||||||
test "it works for incoming unqualified emoji reactions" do
|
test "it works for incoming unqualified emoji reactions" do
|
||||||
@ -65,7 +114,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
|
|||||||
object = Object.get_by_ap_id(data["object"])
|
object = Object.get_by_ap_id(data["object"])
|
||||||
|
|
||||||
assert object.data["reaction_count"] == 1
|
assert object.data["reaction_count"] == 1
|
||||||
assert match?([[^emoji, _]], object.data["reactions"])
|
assert match?([[^emoji, _, _]], object.data["reactions"])
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it reject invalid emoji reactions" do
|
test "it reject invalid emoji reactions" do
|
||||||
|
@ -190,7 +190,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
|
|||||||
emoji: "☕",
|
emoji: "☕",
|
||||||
account: AccountView.render("show.json", %{user: other_user, for: user}),
|
account: AccountView.render("show.json", %{user: other_user, for: user}),
|
||||||
status: StatusView.render("show.json", %{activity: activity, 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])
|
test_notifications_rendering([notification], user, [expected])
|
||||||
|
@ -35,16 +35,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||||||
{:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
|
{:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
|
||||||
|
|
||||||
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
|
{: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, third_user, "🍵")
|
||||||
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_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)
|
activity = Repo.get(Activity, activity.id)
|
||||||
status = StatusView.render("show.json", activity: activity)
|
status = StatusView.render("show.json", activity: activity)
|
||||||
|
|
||||||
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
||||||
|
|
||||||
assert status[:pleroma][:emoji_reactions] == [
|
assert status[:pleroma][:emoji_reactions] == [
|
||||||
%{name: "☕", count: 2, me: false},
|
%{name: "☕", count: 2, me: false, url: nil, account_ids: [other_user.id, user.id]},
|
||||||
%{name: "🍵", count: 1, me: false}
|
%{
|
||||||
|
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)
|
status = StatusView.render("show.json", activity: activity, for: user)
|
||||||
@ -52,8 +62,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||||||
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
|
||||||
|
|
||||||
assert status[:pleroma][:emoji_reactions] == [
|
assert status[:pleroma][:emoji_reactions] == [
|
||||||
%{name: "☕", count: 2, me: true},
|
%{name: "☕", count: 2, me: true, url: nil, account_ids: [other_user.id, user.id]},
|
||||||
%{name: "🍵", count: 1, me: false}
|
%{
|
||||||
|
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
|
end
|
||||||
|
|
||||||
@ -66,11 +83,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||||||
|> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}})
|
|> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}})
|
||||||
|
|
||||||
activity = Activity.get_by_id(activity.id)
|
activity = Activity.get_by_id(activity.id)
|
||||||
|
|
||||||
status = StatusView.render("show.json", activity: activity, for: user)
|
status = StatusView.render("show.json", activity: activity, for: user)
|
||||||
|
|
||||||
assert status[:pleroma][:emoji_reactions] == [
|
assert status[:pleroma][:emoji_reactions] == [
|
||||||
%{name: "☕", count: 1, me: true}
|
%{name: "☕", count: 1, me: true, url: nil, account_ids: [user.id]}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -90,7 +106,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||||||
status = StatusView.render("show.json", activity: activity)
|
status = StatusView.render("show.json", activity: activity)
|
||||||
|
|
||||||
assert status[:pleroma][:emoji_reactions] == [
|
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)
|
status = StatusView.render("show.json", activity: activity, for: user)
|
||||||
@ -102,19 +118,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||||||
status = StatusView.render("show.json", activity: activity)
|
status = StatusView.render("show.json", activity: activity)
|
||||||
|
|
||||||
assert status[:pleroma][:emoji_reactions] == [
|
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)
|
status = StatusView.render("show.json", activity: activity, for: user)
|
||||||
|
|
||||||
assert status[:pleroma][:emoji_reactions] == [
|
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)
|
status = StatusView.render("show.json", activity: activity, for: other_user)
|
||||||
|
|
||||||
assert status[:pleroma][:emoji_reactions] == [
|
assert status[:pleroma][:emoji_reactions] == [
|
||||||
%{name: "☕", count: 1, me: true}
|
%{name: "☕", count: 1, me: true, url: nil, account_ids: [other_user.id]}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user