Work on actors
* Implement group GraphQL APIs * Change Actors changeset to properly set urls * Remove old actors indexes and add some new ones Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
1d547ce66a
commit
fd0dba62e0
|
@ -80,9 +80,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
:banner_url,
|
:banner_url,
|
||||||
:user_id
|
:user_id
|
||||||
])
|
])
|
||||||
|> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/@#{attrs["preferred_username"]}")
|
|> build_urls()
|
||||||
|> validate_required([:preferred_username, :keys, :suspended, :url])
|
|> validate_required([:preferred_username, :keys, :suspended, :url])
|
||||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_index)
|
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||||
|
|> unique_constraint(:url, name: :actors_url_index)
|
||||||
end
|
end
|
||||||
|
|
||||||
def registration_changeset(%Actor{} = actor, attrs) do
|
def registration_changeset(%Actor{} = actor, attrs) do
|
||||||
|
@ -93,21 +94,15 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
:name,
|
:name,
|
||||||
:summary,
|
:summary,
|
||||||
:keys,
|
:keys,
|
||||||
:keys,
|
|
||||||
:suspended,
|
:suspended,
|
||||||
:url,
|
:url,
|
||||||
:type,
|
:type,
|
||||||
:avatar_url,
|
:avatar_url,
|
||||||
:user_id
|
:user_id
|
||||||
])
|
])
|
||||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_index)
|
|> build_urls()
|
||||||
|> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/@#{attrs.preferred_username}")
|
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||||
|> put_change(:inbox_url, "#{MobilizonWeb.Endpoint.url()}/@#{attrs.preferred_username}/inbox")
|
|> unique_constraint(:url, name: :actors_url_index)
|
||||||
|> put_change(
|
|
||||||
:outbox_url,
|
|
||||||
"#{MobilizonWeb.Endpoint.url()}/@#{attrs.preferred_username}/outbox"
|
|
||||||
)
|
|
||||||
|> put_change(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox")
|
|
||||||
|> validate_required([:preferred_username, :keys, :suspended, :url, :type])
|
|> validate_required([:preferred_username, :keys, :suspended, :url, :type])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -142,7 +137,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
:preferred_username,
|
:preferred_username,
|
||||||
:keys
|
:keys
|
||||||
])
|
])
|
||||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_index)
|
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||||
|
|> unique_constraint(:url, name: :actors_url_index)
|
||||||
|> validate_length(:summary, max: 5000)
|
|> validate_length(:summary, max: 5000)
|
||||||
|> validate_length(:preferred_username, max: 100)
|
|> validate_length(:preferred_username, max: 100)
|
||||||
|> put_change(:local, false)
|
|> put_change(:local, false)
|
||||||
|
@ -167,24 +163,36 @@ defmodule Mobilizon.Actors.Actor do
|
||||||
:avatar_url,
|
:avatar_url,
|
||||||
:banner_url
|
:banner_url
|
||||||
])
|
])
|
||||||
|> put_change(
|
|> build_urls(:Group)
|
||||||
:outbox_url,
|
|
||||||
"#{MobilizonWeb.Endpoint.url()}/@#{params["preferred_username"]}/outbox"
|
|
||||||
)
|
|
||||||
|> put_change(
|
|
||||||
:inbox_url,
|
|
||||||
"#{MobilizonWeb.Endpoint.url()}/@#{params["preferred_username"]}/inbox"
|
|
||||||
)
|
|
||||||
|> put_change(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox")
|
|
||||||
|> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/@#{params["preferred_username"]}")
|
|
||||||
|> put_change(:domain, nil)
|
|> put_change(:domain, nil)
|
||||||
|> put_change(:type, :Group)
|
|> put_change(:type, :Group)
|
||||||
|> validate_required([:url, :outbox_url, :inbox_url, :type, :name, :preferred_username])
|
|> validate_required([:url, :outbox_url, :inbox_url, :type, :preferred_username])
|
||||||
|
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||||
|
|> unique_constraint(:url, name: :actors_url_index)
|
||||||
|> validate_length(:summary, max: 5000)
|
|> validate_length(:summary, max: 5000)
|
||||||
|> validate_length(:preferred_username, max: 100)
|
|> validate_length(:preferred_username, max: 100)
|
||||||
|> put_change(:local, true)
|
|> put_change(:local, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec build_urls(Ecto.Changeset.t, atom()) :: Ecto.Changeset.t
|
||||||
|
defp build_urls(changeset, type \\ :Person)
|
||||||
|
defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, type) do
|
||||||
|
symbol = if type == :Group, do: "~", else: "@"
|
||||||
|
changeset
|
||||||
|
|> put_change(
|
||||||
|
:outbox_url,
|
||||||
|
"#{MobilizonWeb.Endpoint.url()}/#{symbol}#{username}/outbox"
|
||||||
|
)
|
||||||
|
|> put_change(
|
||||||
|
:inbox_url,
|
||||||
|
"#{MobilizonWeb.Endpoint.url()}/#{symbol}#{username}/inbox"
|
||||||
|
)
|
||||||
|
|> put_change(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox")
|
||||||
|
|> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/#{symbol}#{username}")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Get a public key for a given ActivityPub actor ID (url)
|
Get a public key for a given ActivityPub actor ID (url)
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -177,6 +177,8 @@ defmodule Mobilizon.Actors do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def create_group(attrs \\ %{}) do
|
def create_group(attrs \\ %{}) do
|
||||||
|
attrs = Map.put(attrs, :keys, create_keys())
|
||||||
|
|
||||||
%Actor{}
|
%Actor{}
|
||||||
|> Actor.group_creation(attrs)
|
|> Actor.group_creation(attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
@ -211,7 +213,7 @@ defmodule Mobilizon.Actors do
|
||||||
name: data.name
|
name: data.name
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
conflict_target: [:preferred_username, :domain]
|
conflict_target: [:preferred_username, :domain, :type]
|
||||||
)
|
)
|
||||||
|
|
||||||
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
|
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
|
||||||
|
@ -509,15 +511,19 @@ defmodule Mobilizon.Actors do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Create a new RSA key
|
||||||
|
@spec create_keys() :: String.t()
|
||||||
|
defp create_keys() do
|
||||||
|
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||||
|
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||||
|
[entry] |> :public_key.pem_encode() |> String.trim_trailing()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Register user
|
Register user
|
||||||
"""
|
"""
|
||||||
@spec register(map()) :: {:ok, Actor.t()} | {:error, String.t()}
|
@spec register(map()) :: {:ok, Actor.t()} | {:error, String.t()}
|
||||||
def register(%{email: email, password: password, username: username}) do
|
def register(%{email: email, password: password, username: username}) do
|
||||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
|
||||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
|
||||||
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing()
|
|
||||||
|
|
||||||
with avatar <- gravatar(email),
|
with avatar <- gravatar(email),
|
||||||
user_changeset <-
|
user_changeset <-
|
||||||
User.registration_changeset(%User{}, %{
|
User.registration_changeset(%User{}, %{
|
||||||
|
@ -526,7 +532,7 @@ defmodule Mobilizon.Actors do
|
||||||
default_actor: %{
|
default_actor: %{
|
||||||
preferred_username: username,
|
preferred_username: username,
|
||||||
domain: nil,
|
domain: nil,
|
||||||
keys: pem,
|
keys: create_keys(),
|
||||||
avatar_url: avatar
|
avatar_url: avatar
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule MobilizonWeb.Resolvers.Group do
|
||||||
|
alias Mobilizon.Actors
|
||||||
|
alias Mobilizon.Actors.{Actor}
|
||||||
|
alias Mobilizon.Service.ActivityPub
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Find a group
|
||||||
|
"""
|
||||||
|
def find_group(_parent, %{preferred_username: name}, _resolution) do
|
||||||
|
case ActivityPub.find_or_make_group_from_nickname(name) do
|
||||||
|
{:ok, actor} ->
|
||||||
|
{:ok, actor}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, "Group with name #{name} not found"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Lists all groups
|
||||||
|
"""
|
||||||
|
def list_groups(_parent, _args, _resolution) do
|
||||||
|
{:ok, Actors.list_groups}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Create a new group. The creator is automatically added as admin
|
||||||
|
"""
|
||||||
|
def create_group(
|
||||||
|
_parent,
|
||||||
|
%{preferred_username: preferred_username, creator_username: actor_username},
|
||||||
|
%{
|
||||||
|
context: %{current_user: user}
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
|
||||||
|
with %Actor{id: actor_id} <- Actors.get_local_actor_by_name(actor_username),
|
||||||
|
{:user_actor, true} <-
|
||||||
|
{:user_actor, actor_id in Enum.map(Actors.get_actors_for_user(user), & &1.id)},
|
||||||
|
{:ok, %Actor{} = group} <- Actors.create_group(%{preferred_username: preferred_username}) do
|
||||||
|
{:ok, group}
|
||||||
|
else
|
||||||
|
{:error, %Ecto.Changeset{errors: [url: {"has already been taken", []}]}} ->
|
||||||
|
{:error, :group_name_not_available}
|
||||||
|
err ->
|
||||||
|
Logger.error(inspect(err))
|
||||||
|
err
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_group(_parent, _args, _resolution) do
|
||||||
|
{:error, "You need to be logged-in to create a group"}
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +1,8 @@
|
||||||
defmodule MobilizonWeb.Resolvers.Actor do
|
defmodule MobilizonWeb.Resolvers.Person do
|
||||||
alias Mobilizon.Actors
|
alias Mobilizon.Actors
|
||||||
alias Mobilizon.Service.ActivityPub
|
alias Mobilizon.Service.ActivityPub
|
||||||
|
|
||||||
|
@deprecated "Use find_person/3 or find_group/3 instead"
|
||||||
def find_actor(_parent, %{preferred_username: name}, _resolution) do
|
def find_actor(_parent, %{preferred_username: name}, _resolution) do
|
||||||
case ActivityPub.find_or_make_actor_from_nickname(name) do
|
case ActivityPub.find_or_make_actor_from_nickname(name) do
|
||||||
{:ok, actor} ->
|
{:ok, actor} ->
|
||||||
|
@ -25,19 +26,6 @@ defmodule MobilizonWeb.Resolvers.Actor do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Find a person
|
|
||||||
"""
|
|
||||||
def find_group(_parent, %{preferred_username: name}, _resolution) do
|
|
||||||
case ActivityPub.find_or_make_group_from_nickname(name) do
|
|
||||||
{:ok, actor} ->
|
|
||||||
{:ok, actor}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:error, "Group with name #{name} not found"}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the current actor for the currently logged-in user
|
Returns the current actor for the currently logged-in user
|
||||||
"""
|
"""
|
|
@ -398,6 +398,11 @@ defmodule MobilizonWeb.Schema do
|
||||||
resolve(&Resolvers.Event.list_events/3)
|
resolve(&Resolvers.Event.list_events/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Get all groups"
|
||||||
|
field :groups, list_of(:group) do
|
||||||
|
resolve(&Resolvers.Group.list_groups/3)
|
||||||
|
end
|
||||||
|
|
||||||
@desc "Search through events, persons and groups"
|
@desc "Search through events, persons and groups"
|
||||||
field :search, list_of(:search_result) do
|
field :search, list_of(:search_result) do
|
||||||
arg(:search, non_null(:string))
|
arg(:search, non_null(:string))
|
||||||
|
@ -418,6 +423,12 @@ defmodule MobilizonWeb.Schema do
|
||||||
resolve(&Resolvers.Event.list_participants_for_event/3)
|
resolve(&Resolvers.Event.list_participants_for_event/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Get a group by it's preferred username"
|
||||||
|
field :group, :group do
|
||||||
|
arg(:preferred_username, non_null(:string))
|
||||||
|
resolve(&Resolvers.Group.find_group/3)
|
||||||
|
end
|
||||||
|
|
||||||
@desc "Get an user"
|
@desc "Get an user"
|
||||||
field :user, :user do
|
field :user, :user do
|
||||||
arg(:id, non_null(:id))
|
arg(:id, non_null(:id))
|
||||||
|
@ -431,13 +442,13 @@ defmodule MobilizonWeb.Schema do
|
||||||
|
|
||||||
@desc "Get the current actor for the logged-in user"
|
@desc "Get the current actor for the logged-in user"
|
||||||
field :logged_person, :person do
|
field :logged_person, :person do
|
||||||
resolve(&Resolvers.Actor.get_current_person/3)
|
resolve(&Resolvers.Person.get_current_person/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc "Get a person"
|
@desc "Get a person by it's preferred username"
|
||||||
field :person, :person do
|
field :person, :person do
|
||||||
arg(:preferred_username, non_null(:string))
|
arg(:preferred_username, non_null(:string))
|
||||||
resolve(&Resolvers.Actor.find_person/3)
|
resolve(&Resolvers.Person.find_person/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc "Get the list of categories"
|
@desc "Get the list of categories"
|
||||||
|
@ -529,6 +540,17 @@ defmodule MobilizonWeb.Schema do
|
||||||
resolve(&Resolvers.User.change_default_actor/3)
|
resolve(&Resolvers.User.change_default_actor/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc "Create a group"
|
||||||
|
field :create_group, :group do
|
||||||
|
arg(:preferred_username, non_null(:string), description: "The name for the group")
|
||||||
|
arg(:name, :string, description: "The displayed name for the group")
|
||||||
|
|
||||||
|
arg(:creator_username, :string,
|
||||||
|
description: "The actor's username which will be the admin (otherwise user's default one)"
|
||||||
|
)
|
||||||
|
resolve(&Resolvers.Group.create_group/3)
|
||||||
|
end
|
||||||
|
|
||||||
# @desc "Upload a picture"
|
# @desc "Upload a picture"
|
||||||
# field :upload_picture, :picture do
|
# field :upload_picture, :picture do
|
||||||
# arg(:file, non_null(:upload))
|
# arg(:file, non_null(:upload))
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
defmodule Mobilizon.Repo.Migrations.ChangeActorsIndexes do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
drop index("actors", [:preferred_username, :domain], name: :actors_preferred_username_domain_index)
|
||||||
|
drop index("actors", [:name, :domain], name: :accounts_username_domain_index)
|
||||||
|
execute "ALTER INDEX accounts_pkey RENAME TO actors_pkey"
|
||||||
|
create index("actors", [:preferred_username, :domain, :type], unique: true)
|
||||||
|
create index("actors", [:url], unique: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
create index("actors", [:preferred_username, :domain], name: :actors_preferred_username_domain_index)
|
||||||
|
create index("actors", [:name, :domain], name: :accounts_username_domain_index)
|
||||||
|
execute "ALTER INDEX actors_pkey RENAME TO accounts_pkey"
|
||||||
|
drop index("actors", [:preferred_username, :domain, :type])
|
||||||
|
drop index("actors", [:url])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,116 @@
|
||||||
|
defmodule MobilizonWeb.Resolvers.GroupResolverTest do
|
||||||
|
use MobilizonWeb.ConnCase
|
||||||
|
alias Mobilizon.Actors
|
||||||
|
alias Mobilizon.Actors.{User, Actor}
|
||||||
|
alias MobilizonWeb.AbsintheHelpers
|
||||||
|
import Mobilizon.Factory
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@non_existent_username "nonexistent"
|
||||||
|
@new_group_params %{groupname: "new group"}
|
||||||
|
|
||||||
|
setup %{conn: conn} do
|
||||||
|
{:ok, %User{default_actor: %Actor{} = actor} = user} =
|
||||||
|
Actors.register(%{email: "test2@test.tld", password: "testest", username: "test"})
|
||||||
|
|
||||||
|
{:ok, conn: conn, actor: actor, user: user}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Group Resolver" do
|
||||||
|
test "create_group/3 creates a group", %{conn: conn, user: user, actor: actor} do
|
||||||
|
mutation = """
|
||||||
|
mutation {
|
||||||
|
createGroup(
|
||||||
|
preferred_username: "#{@new_group_params.groupname}",
|
||||||
|
creator_username: "#{actor.preferred_username}"
|
||||||
|
) {
|
||||||
|
preferred_username,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||||
|
|
||||||
|
assert json_response(res, 200)["data"]["createGroup"]["preferred_username"] ==
|
||||||
|
@new_group_params.groupname
|
||||||
|
assert json_response(res, 200)["data"]["createGroup"]["type"] == "GROUP"
|
||||||
|
|
||||||
|
mutation = """
|
||||||
|
mutation {
|
||||||
|
createGroup(
|
||||||
|
preferred_username: "#{@new_group_params.groupname}",
|
||||||
|
creator_username: "#{actor.preferred_username}",
|
||||||
|
) {
|
||||||
|
preferred_username,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||||
|
|
||||||
|
assert hd(json_response(res, 200)["errors"])["message"] == "group_name_not_available"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "list_groups/3 returns all groups", context do
|
||||||
|
group = insert(:group)
|
||||||
|
query = """
|
||||||
|
{
|
||||||
|
groups {
|
||||||
|
preferredUsername,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
context.conn
|
||||||
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
||||||
|
|
||||||
|
assert hd(json_response(res, 200)["data"]["groups"])["preferredUsername"] ==
|
||||||
|
group.preferred_username
|
||||||
|
end
|
||||||
|
|
||||||
|
test "find_group/3 returns a group by it's username", context do
|
||||||
|
group = insert(:group)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
{
|
||||||
|
group(preferredUsername: "#{group.preferred_username}") {
|
||||||
|
preferredUsername,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
context.conn
|
||||||
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "group"))
|
||||||
|
|
||||||
|
assert json_response(res, 200)["data"]["group"]["preferredUsername"] ==
|
||||||
|
group.preferred_username
|
||||||
|
|
||||||
|
query = """
|
||||||
|
{
|
||||||
|
group(preferredUsername: "#{@non_existent_username}") {
|
||||||
|
preferredUsername,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
res =
|
||||||
|
context.conn
|
||||||
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "group"))
|
||||||
|
|
||||||
|
assert json_response(res, 200)["data"]["group"] == nil
|
||||||
|
|
||||||
|
assert hd(json_response(res, 200)["errors"])["message"] ==
|
||||||
|
"Group with name #{@non_existent_username} not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,7 +8,7 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do
|
||||||
@non_existent_username "nonexistent"
|
@non_existent_username "nonexistent"
|
||||||
|
|
||||||
describe "Person Resolver" do
|
describe "Person Resolver" do
|
||||||
test "find_actor/3 returns a person by it's username", context do
|
test "find_person/3 returns a person by it's username", context do
|
||||||
{:ok, %User{default_actor: %Actor{} = actor} = _user} = Actors.register(@valid_actor_params)
|
{:ok, %User{default_actor: %Actor{} = actor} = _user} = Actors.register(@valid_actor_params)
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
|
|
Loading…
Reference in New Issue