From fd0dba62e02af6a2aadecf3e6ba4470cc7984a7a Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 3 Dec 2018 11:58:57 +0100 Subject: [PATCH] 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 --- lib/mobilizon/actors/actor.ex | 54 ++++---- lib/mobilizon/actors/actors.ex | 18 ++- lib/mobilizon_web/resolvers/group.ex | 55 +++++++++ .../resolvers/{actor.ex => person.ex} | 16 +-- lib/mobilizon_web/schema.ex | 28 ++++- .../20181203092128_change_actors_indexes.exs | 19 +++ .../resolvers/group_resolver_test.exs | 116 ++++++++++++++++++ .../resolvers/person_resolver_test.exs | 2 +- 8 files changed, 261 insertions(+), 47 deletions(-) create mode 100644 lib/mobilizon_web/resolvers/group.ex rename lib/mobilizon_web/resolvers/{actor.ex => person.ex} (74%) create mode 100644 priv/repo/migrations/20181203092128_change_actors_indexes.exs create mode 100644 test/mobilizon_web/resolvers/group_resolver_test.exs diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 6e8402cc1..0b282f80b 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -80,9 +80,10 @@ defmodule Mobilizon.Actors.Actor do :banner_url, :user_id ]) - |> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/@#{attrs["preferred_username"]}") + |> build_urls() |> 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 def registration_changeset(%Actor{} = actor, attrs) do @@ -93,21 +94,15 @@ defmodule Mobilizon.Actors.Actor do :name, :summary, :keys, - :keys, :suspended, :url, :type, :avatar_url, :user_id ]) - |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_index) - |> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/@#{attrs.preferred_username}") - |> put_change(:inbox_url, "#{MobilizonWeb.Endpoint.url()}/@#{attrs.preferred_username}/inbox") - |> put_change( - :outbox_url, - "#{MobilizonWeb.Endpoint.url()}/@#{attrs.preferred_username}/outbox" - ) - |> put_change(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox") + |> build_urls() + |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) + |> unique_constraint(:url, name: :actors_url_index) |> validate_required([:preferred_username, :keys, :suspended, :url, :type]) end @@ -142,7 +137,8 @@ defmodule Mobilizon.Actors.Actor do :preferred_username, :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(:preferred_username, max: 100) |> put_change(:local, false) @@ -167,24 +163,36 @@ defmodule Mobilizon.Actors.Actor do :avatar_url, :banner_url ]) - |> put_change( - :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"]}") + |> build_urls(:Group) |> put_change(:domain, nil) |> 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(:preferred_username, max: 100) |> put_change(:local, true) 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 """ Get a public key for a given ActivityPub actor ID (url) """ diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 3bbd24e0b..bc0b5b078 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -177,6 +177,8 @@ defmodule Mobilizon.Actors do """ def create_group(attrs \\ %{}) do + attrs = Map.put(attrs, :keys, create_keys()) + %Actor{} |> Actor.group_creation(attrs) |> Repo.insert() @@ -211,7 +213,7 @@ defmodule Mobilizon.Actors do 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} @@ -509,15 +511,19 @@ defmodule Mobilizon.Actors do 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 """ Register user """ @spec register(map()) :: {:ok, Actor.t()} | {:error, String.t()} 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), user_changeset <- User.registration_changeset(%User{}, %{ @@ -526,7 +532,7 @@ defmodule Mobilizon.Actors do default_actor: %{ preferred_username: username, domain: nil, - keys: pem, + keys: create_keys(), avatar_url: avatar } }), diff --git a/lib/mobilizon_web/resolvers/group.ex b/lib/mobilizon_web/resolvers/group.ex new file mode 100644 index 000000000..1a8166dc9 --- /dev/null +++ b/lib/mobilizon_web/resolvers/group.ex @@ -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 diff --git a/lib/mobilizon_web/resolvers/actor.ex b/lib/mobilizon_web/resolvers/person.ex similarity index 74% rename from lib/mobilizon_web/resolvers/actor.ex rename to lib/mobilizon_web/resolvers/person.ex index 1dfd02630..d478f9727 100644 --- a/lib/mobilizon_web/resolvers/actor.ex +++ b/lib/mobilizon_web/resolvers/person.ex @@ -1,7 +1,8 @@ -defmodule MobilizonWeb.Resolvers.Actor do +defmodule MobilizonWeb.Resolvers.Person do alias Mobilizon.Actors alias Mobilizon.Service.ActivityPub + @deprecated "Use find_person/3 or find_group/3 instead" def find_actor(_parent, %{preferred_username: name}, _resolution) do case ActivityPub.find_or_make_actor_from_nickname(name) do {:ok, actor} -> @@ -25,19 +26,6 @@ defmodule MobilizonWeb.Resolvers.Actor do 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 """ Returns the current actor for the currently logged-in user """ diff --git a/lib/mobilizon_web/schema.ex b/lib/mobilizon_web/schema.ex index b5754354f..826a4b4e5 100644 --- a/lib/mobilizon_web/schema.ex +++ b/lib/mobilizon_web/schema.ex @@ -398,6 +398,11 @@ defmodule MobilizonWeb.Schema do resolve(&Resolvers.Event.list_events/3) 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" field :search, list_of(:search_result) do arg(:search, non_null(:string)) @@ -418,6 +423,12 @@ defmodule MobilizonWeb.Schema do resolve(&Resolvers.Event.list_participants_for_event/3) 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" field :user, :user do arg(:id, non_null(:id)) @@ -431,13 +442,13 @@ defmodule MobilizonWeb.Schema do @desc "Get the current actor for the logged-in user" field :logged_person, :person do - resolve(&Resolvers.Actor.get_current_person/3) + resolve(&Resolvers.Person.get_current_person/3) end - @desc "Get a person" + @desc "Get a person by it's preferred username" field :person, :person do arg(:preferred_username, non_null(:string)) - resolve(&Resolvers.Actor.find_person/3) + resolve(&Resolvers.Person.find_person/3) end @desc "Get the list of categories" @@ -529,6 +540,17 @@ defmodule MobilizonWeb.Schema do resolve(&Resolvers.User.change_default_actor/3) 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" # field :upload_picture, :picture do # arg(:file, non_null(:upload)) diff --git a/priv/repo/migrations/20181203092128_change_actors_indexes.exs b/priv/repo/migrations/20181203092128_change_actors_indexes.exs new file mode 100644 index 000000000..90158e944 --- /dev/null +++ b/priv/repo/migrations/20181203092128_change_actors_indexes.exs @@ -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 diff --git a/test/mobilizon_web/resolvers/group_resolver_test.exs b/test/mobilizon_web/resolvers/group_resolver_test.exs new file mode 100644 index 000000000..b2e34714e --- /dev/null +++ b/test/mobilizon_web/resolvers/group_resolver_test.exs @@ -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 diff --git a/test/mobilizon_web/resolvers/person_resolver_test.exs b/test/mobilizon_web/resolvers/person_resolver_test.exs index 6f850c5df..6f0475ab0 100644 --- a/test/mobilizon_web/resolvers/person_resolver_test.exs +++ b/test/mobilizon_web/resolvers/person_resolver_test.exs @@ -8,7 +8,7 @@ defmodule MobilizonWeb.Resolvers.PersonResolverTest do @non_existent_username "nonexistent" 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) query = """