Refactoring of Actors context
This commit is contained in:
parent
60707b8f8d
commit
e4a446003d
|
@ -15,7 +15,7 @@ defmodule Mix.Tasks.Mobilizon.CreateBot do
|
||||||
Mix.Task.run("app.start")
|
Mix.Task.run("app.start")
|
||||||
|
|
||||||
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
|
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
|
||||||
actor <- Actors.register_bot_account(%{name: name, summary: summary}),
|
actor <- Actors.register_bot(%{name: name, summary: summary}),
|
||||||
{:ok, %Bot{} = bot} <-
|
{:ok, %Bot{} = bot} <-
|
||||||
Actors.create_bot(%{
|
Actors.create_bot(%{
|
||||||
"type" => type,
|
"type" => type,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -44,8 +44,11 @@ defmodule Mobilizon.Actors.Follower do
|
||||||
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||||
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do
|
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do
|
||||||
case fetch_change(changeset, :url) do
|
case fetch_change(changeset, :url) do
|
||||||
{:ok, _url} -> changeset
|
{:ok, _url} ->
|
||||||
:error -> generate_url(changeset)
|
changeset
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
generate_url(changeset)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -86,8 +86,11 @@ defmodule Mobilizon.Addresses do
|
||||||
|> filter_by_contry(Keyword.get(options, :country))
|
|> filter_by_contry(Keyword.get(options, :country))
|
||||||
|
|
||||||
case Keyword.get(options, :single, false) do
|
case Keyword.get(options, :single, false) do
|
||||||
true -> Repo.one(query)
|
true ->
|
||||||
false -> Repo.all(query)
|
Repo.one(query)
|
||||||
|
|
||||||
|
false ->
|
||||||
|
Repo.all(query)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -75,8 +75,11 @@ defmodule Mobilizon.Media do
|
||||||
|> Repo.transaction()
|
|> Repo.transaction()
|
||||||
|
|
||||||
case transaction do
|
case transaction do
|
||||||
{:ok, %{picture: %Picture{} = picture}} -> {:ok, picture}
|
{:ok, %{picture: %Picture{} = picture}} ->
|
||||||
{:error, :remove, error, _} -> {:error, error}
|
{:ok, picture}
|
||||||
|
|
||||||
|
{:error, :remove, error, _} ->
|
||||||
|
{:error, error}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -154,8 +154,11 @@ defmodule Mobilizon.Users.User do
|
||||||
case changeset do
|
case changeset do
|
||||||
%Ecto.Changeset{valid?: true, changes: %{email: email}} ->
|
%Ecto.Changeset{valid?: true, changes: %{email: email}} ->
|
||||||
case EmailChecker.valid?(email) do
|
case EmailChecker.valid?(email) do
|
||||||
false -> add_error(changeset, :email, "Email doesn't fit required format")
|
false ->
|
||||||
true -> changeset
|
add_error(changeset, :email, "Email doesn't fit required format")
|
||||||
|
|
||||||
|
true ->
|
||||||
|
changeset
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
|
|
|
@ -59,8 +59,11 @@ defmodule Mobilizon.Users do
|
||||||
query = user_by_email_query(email, activated)
|
query = user_by_email_query(email, activated)
|
||||||
|
|
||||||
case Repo.one(query) do
|
case Repo.one(query) do
|
||||||
nil -> {:error, :user_not_found}
|
nil ->
|
||||||
user -> {:ok, user}
|
{:error, :user_not_found}
|
||||||
|
|
||||||
|
user ->
|
||||||
|
{:ok, user}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -147,8 +150,11 @@ defmodule Mobilizon.Users do
|
||||||
case actor do
|
case actor do
|
||||||
nil ->
|
nil ->
|
||||||
case get_actors_for_user(user) do
|
case get_actors_for_user(user) do
|
||||||
[] -> nil
|
[] ->
|
||||||
actors -> hd(actors)
|
nil
|
||||||
|
|
||||||
|
actors ->
|
||||||
|
hd(actors)
|
||||||
end
|
end
|
||||||
|
|
||||||
actor ->
|
actor ->
|
||||||
|
|
|
@ -32,7 +32,7 @@ defmodule MobilizonWeb.API.Follows do
|
||||||
|
|
||||||
def accept(%Actor{} = follower, %Actor{} = followed) do
|
def accept(%Actor{} = follower, %Actor{} = followed) do
|
||||||
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <-
|
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <-
|
||||||
Actors.following?(follower, followed),
|
Actors.is_following(follower, followed),
|
||||||
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}",
|
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}",
|
||||||
data <-
|
data <-
|
||||||
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),
|
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),
|
||||||
|
|
|
@ -9,7 +9,7 @@ defmodule MobilizonWeb.Resolvers.Member do
|
||||||
Find members for group
|
Find members for group
|
||||||
"""
|
"""
|
||||||
def find_members_for_group(%Actor{} = actor, _args, _resolution) do
|
def find_members_for_group(%Actor{} = actor, _args, _resolution) do
|
||||||
members = Actors.memberships_for_group(actor)
|
members = Actors.list_members_for_group(actor)
|
||||||
{:ok, members}
|
{:ok, members}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -207,7 +207,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||||
# We check that the actor is not the last administrator/creator of a group
|
# We check that the actor is not the last administrator/creator of a group
|
||||||
@spec last_admin_of_a_group?(integer()) :: boolean()
|
@spec last_admin_of_a_group?(integer()) :: boolean()
|
||||||
defp last_admin_of_a_group?(actor_id) do
|
defp last_admin_of_a_group?(actor_id) do
|
||||||
length(Actors.list_group_id_where_last_administrator(actor_id)) > 0
|
length(Actors.list_group_ids_where_last_administrator(actor_id)) > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec proxify_avatar(Actor.t()) :: Actor.t()
|
@spec proxify_avatar(Actor.t()) :: Actor.t()
|
||||||
|
|
|
@ -49,7 +49,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||||
def render("following.json", %{actor: actor, page: page}) do
|
def render("following.json", %{actor: actor, page: page}) do
|
||||||
%{total: total, elements: following} =
|
%{total: total, elements: following} =
|
||||||
if Actor.is_public_visibility(actor),
|
if Actor.is_public_visibility(actor),
|
||||||
do: Actors.get_followings(actor, page),
|
do: Actors.build_followings_for_actor(actor, page),
|
||||||
else: @private_visibility_empty_collection
|
else: @private_visibility_empty_collection
|
||||||
|
|
||||||
following
|
following
|
||||||
|
@ -60,7 +60,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||||
def render("following.json", %{actor: actor}) do
|
def render("following.json", %{actor: actor}) do
|
||||||
%{total: total, elements: following} =
|
%{total: total, elements: following} =
|
||||||
if Actor.is_public_visibility(actor),
|
if Actor.is_public_visibility(actor),
|
||||||
do: Actors.get_followings(actor),
|
do: Actors.build_followings_for_actor(actor),
|
||||||
else: @private_visibility_empty_collection
|
else: @private_visibility_empty_collection
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
@ -75,7 +75,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||||
def render("followers.json", %{actor: actor, page: page}) do
|
def render("followers.json", %{actor: actor, page: page}) do
|
||||||
%{total: total, elements: followers} =
|
%{total: total, elements: followers} =
|
||||||
if Actor.is_public_visibility(actor),
|
if Actor.is_public_visibility(actor),
|
||||||
do: Actors.get_followers(actor, page),
|
do: Actors.build_followers_for_actor(actor, page),
|
||||||
else: @private_visibility_empty_collection
|
else: @private_visibility_empty_collection
|
||||||
|
|
||||||
followers
|
followers
|
||||||
|
@ -86,7 +86,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||||
def render("followers.json", %{actor: actor}) do
|
def render("followers.json", %{actor: actor}) do
|
||||||
%{total: total, elements: followers} =
|
%{total: total, elements: followers} =
|
||||||
if Actor.is_public_visibility(actor),
|
if Actor.is_public_visibility(actor),
|
||||||
do: Actors.get_followers(actor),
|
do: Actors.build_followers_for_actor(actor),
|
||||||
else: @private_visibility_empty_collection
|
else: @private_visibility_empty_collection
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -551,7 +551,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||||
|
|
||||||
followers =
|
followers =
|
||||||
if actor.followers_url in activity.recipients do
|
if actor.followers_url in activity.recipients do
|
||||||
Actors.get_full_external_followers(actor)
|
Actors.list_external_followers_for_actor(actor)
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
|
||||||
|
|
||||||
def get_actor do
|
def get_actor do
|
||||||
with {:ok, %Actor{} = actor} <-
|
with {:ok, %Actor{} = actor} <-
|
||||||
Actors.get_or_create_service_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
|
Actors.get_or_create_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
|
||||||
actor
|
actor
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -642,7 +642,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
|
||||||
defp get_follow(follow_object) do
|
defp get_follow(follow_object) do
|
||||||
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
|
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
|
||||||
{:not_found, %Follower{} = follow} <-
|
{:not_found, %Follower{} = follow} <-
|
||||||
{:not_found, Actors.get_follow_by_url(follow_object_id)} do
|
{:not_found, Actors.get_follower_by_url(follow_object_id)} do
|
||||||
{:ok, follow}
|
{:ok, follow}
|
||||||
else
|
else
|
||||||
{:not_found, _err} ->
|
{:not_found, _err} ->
|
||||||
|
|
|
@ -166,11 +166,11 @@ defmodule Mobilizon.ActorsTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "test get_local_actor_by_username/1 returns local actors with similar usernames", %{
|
test "test list_local_actor_by_username/1 returns local actors with similar usernames", %{
|
||||||
actor: actor
|
actor: actor
|
||||||
} do
|
} do
|
||||||
actor2 = insert(:actor, preferred_username: "tcit")
|
actor2 = insert(:actor, preferred_username: "tcit")
|
||||||
[%Actor{id: actor_found_id} | tail] = Actors.get_local_actor_by_username("tcit")
|
[%Actor{id: actor_found_id} | tail] = Actors.list_local_actor_by_username("tcit")
|
||||||
%Actor{id: actor2_found_id} = hd(tail)
|
%Actor{id: actor2_found_id} = hd(tail)
|
||||||
assert MapSet.new([actor_found_id, actor2_found_id]) == MapSet.new([actor.id, actor2.id])
|
assert MapSet.new([actor_found_id, actor2_found_id]) == MapSet.new([actor.id, actor2.id])
|
||||||
end
|
end
|
||||||
|
@ -416,11 +416,6 @@ defmodule Mobilizon.ActorsTest do
|
||||||
assert {:ok, %Bot{}} = Actors.delete_bot(bot)
|
assert {:ok, %Bot{}} = Actors.delete_bot(bot)
|
||||||
assert_raise Ecto.NoResultsError, fn -> Actors.get_bot!(bot.id) end
|
assert_raise Ecto.NoResultsError, fn -> Actors.get_bot!(bot.id) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "change_bot/1 returns a bot changeset" do
|
|
||||||
bot = insert(:bot)
|
|
||||||
assert %Ecto.Changeset{} = Actors.change_bot(bot)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "followers" do
|
describe "followers" do
|
||||||
|
@ -458,8 +453,8 @@ defmodule Mobilizon.ActorsTest do
|
||||||
assert {:ok, %Follower{} = follower} = Actors.create_follower(valid_attrs)
|
assert {:ok, %Follower{} = follower} = Actors.create_follower(valid_attrs)
|
||||||
assert follower.approved == true
|
assert follower.approved == true
|
||||||
|
|
||||||
assert %{total: 1, elements: [target_actor]} = Actors.get_followings(actor)
|
assert %{total: 1, elements: [target_actor]} = Actors.build_followings_for_actor(actor)
|
||||||
assert %{total: 1, elements: [actor]} = Actors.get_followers(target_actor)
|
assert %{total: 1, elements: [actor]} = Actors.build_followers_for_actor(target_actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_follower/1 with valid data but same actors fails to create a follower", %{
|
test "create_follower/1 with valid data but same actors fails to create a follower", %{
|
||||||
|
@ -568,8 +563,8 @@ defmodule Mobilizon.ActorsTest do
|
||||||
assert {:ok, %Member{} = member} = Actors.create_member(valid_attrs)
|
assert {:ok, %Member{} = member} = Actors.create_member(valid_attrs)
|
||||||
assert member.role == :member
|
assert member.role == :member
|
||||||
|
|
||||||
assert [group] = Actors.get_groups_member_of(actor)
|
assert [group] = Actors.list_groups_member_of(actor)
|
||||||
assert [actor] = Actors.get_members_for_group(group)
|
assert [actor] = Actors.list_members_for_group(group)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_member/1 with valid data but same actors fails to create a member", %{
|
test "create_member/1 with valid data but same actors fails to create a member", %{
|
||||||
|
|
|
@ -223,7 +223,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2"
|
assert data["id"] == "https://social.tcit.fr/users/tcit#follows/2"
|
||||||
|
|
||||||
actor = Actors.get_actor_with_preload(actor.id)
|
actor = Actors.get_actor_with_preload(actor.id)
|
||||||
assert Actors.following?(Actors.get_actor_by_url!(data["actor"], true), actor)
|
assert Actors.is_following(Actors.get_actor_by_url!(data["actor"], true), actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
# test "it works for incoming follow requests from hubzilla" do
|
# test "it works for incoming follow requests from hubzilla" do
|
||||||
|
@ -240,7 +240,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
# assert data["actor"] == "https://hubzilla.example.org/channel/kaniini"
|
# assert data["actor"] == "https://hubzilla.example.org/channel/kaniini"
|
||||||
# assert data["type"] == "Follow"
|
# assert data["type"] == "Follow"
|
||||||
# assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2"
|
# assert data["id"] == "https://hubzilla.example.org/channel/kaniini#follows/2"
|
||||||
# assert User.following?(User.get_by_ap_id(data["actor"]), user)
|
# assert User.is_following(User.get_by_ap_id(data["actor"]), user)
|
||||||
# end
|
# end
|
||||||
|
|
||||||
# test "it works for incoming likes" do
|
# test "it works for incoming likes" do
|
||||||
|
@ -498,7 +498,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
assert data["actor"] == "https://social.tcit.fr/users/tcit"
|
assert data["actor"] == "https://social.tcit.fr/users/tcit"
|
||||||
|
|
||||||
{:ok, followed} = Actors.get_actor_by_url(data["actor"])
|
{:ok, followed} = Actors.get_actor_by_url(data["actor"])
|
||||||
refute Actors.following?(followed, actor)
|
refute Actors.is_following(followed, actor)
|
||||||
end
|
end
|
||||||
|
|
||||||
# test "it works for incoming blocks" do
|
# test "it works for incoming blocks" do
|
||||||
|
@ -581,10 +581,10 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
follower = insert(:actor)
|
follower = insert(:actor)
|
||||||
followed = insert(:actor)
|
followed = insert(:actor)
|
||||||
|
|
||||||
refute Actors.following?(follower, followed)
|
refute Actors.is_following(follower, followed)
|
||||||
|
|
||||||
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
|
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
|
||||||
assert Actors.following?(follower, followed)
|
assert Actors.is_following(follower, followed)
|
||||||
|
|
||||||
accept_data =
|
accept_data =
|
||||||
File.read!("test/fixtures/mastodon-accept-activity.json")
|
File.read!("test/fixtures/mastodon-accept-activity.json")
|
||||||
|
@ -605,7 +605,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
|
|
||||||
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
||||||
|
|
||||||
assert Actors.following?(follower, followed)
|
assert Actors.is_following(follower, followed)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works for incoming accepts which are referenced by IRI only" do
|
test "it works for incoming accepts which are referenced by IRI only" do
|
||||||
|
@ -627,7 +627,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
|
|
||||||
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
||||||
|
|
||||||
assert Actors.following?(follower, followed)
|
assert Actors.is_following(follower, followed)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it fails for incoming accepts which cannot be correlated" do
|
test "it fails for incoming accepts which cannot be correlated" do
|
||||||
|
@ -646,7 +646,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
|
|
||||||
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
||||||
|
|
||||||
refute Actors.following?(follower, followed)
|
refute Actors.is_following(follower, followed)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it fails for incoming rejects which cannot be correlated" do
|
test "it fails for incoming rejects which cannot be correlated" do
|
||||||
|
@ -665,7 +665,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
|
|
||||||
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
{:ok, follower} = Actors.get_actor_by_url(follower.url)
|
||||||
|
|
||||||
refute Actors.following?(follower, followed)
|
refute Actors.is_following(follower, followed)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it works for incoming rejects which are referenced by IRI only" do
|
test "it works for incoming rejects which are referenced by IRI only" do
|
||||||
|
@ -674,7 +674,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
|
|
||||||
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
|
{:ok, follow_activity, _} = ActivityPub.follow(follower, followed)
|
||||||
|
|
||||||
assert Actors.following?(follower, followed)
|
assert Actors.is_following(follower, followed)
|
||||||
|
|
||||||
reject_data =
|
reject_data =
|
||||||
File.read!("test/fixtures/mastodon-reject-activity.json")
|
File.read!("test/fixtures/mastodon-reject-activity.json")
|
||||||
|
@ -684,7 +684,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
|
||||||
|
|
||||||
{:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data)
|
{:ok, %Activity{data: _}, _} = Transmogrifier.handle_incoming(reject_data)
|
||||||
|
|
||||||
refute Actors.following?(follower, followed)
|
refute Actors.is_following(follower, followed)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it rejects activities without a valid ID" do
|
test "it rejects activities without a valid ID" do
|
||||||
|
|
|
@ -22,7 +22,7 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do
|
||||||
{:ok, target_actor} = Actors.get_actor_by_url(target_instance)
|
{:ok, target_actor} = Actors.get_actor_by_url(target_instance)
|
||||||
refute is_nil(target_actor.domain)
|
refute is_nil(target_actor.domain)
|
||||||
|
|
||||||
assert Actors.following?(local_actor, target_actor)
|
assert Actors.is_following(local_actor, target_actor)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -36,11 +36,11 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do
|
||||||
|
|
||||||
%Actor{} = local_actor = Relay.get_actor()
|
%Actor{} = local_actor = Relay.get_actor()
|
||||||
{:ok, %Actor{} = target_actor} = Actors.get_actor_by_url(target_instance)
|
{:ok, %Actor{} = target_actor} = Actors.get_actor_by_url(target_instance)
|
||||||
assert %Follower{} = Actors.following?(local_actor, target_actor)
|
assert %Follower{} = Actors.is_following(local_actor, target_actor)
|
||||||
|
|
||||||
Mix.Tasks.Mobilizon.Relay.run(["unfollow", target_instance])
|
Mix.Tasks.Mobilizon.Relay.run(["unfollow", target_instance])
|
||||||
|
|
||||||
refute Actors.following?(local_actor, target_actor)
|
refute Actors.is_following(local_actor, target_actor)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue