From 44e8ac7e9ab071f61a0423b1120917cdb9af90fa Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 22 Oct 2021 11:40:47 +0200 Subject: [PATCH] Add support for GraphQL handling of group follows Signed-off-by: Thomas Citharel --- lib/graphql/resolvers/group.ex | 74 ++++++- lib/graphql/schema/actors/follower.ex | 5 + lib/graphql/schema/actors/group.ex | 37 ++++ lib/mobilizon/actors/follower.ex | 5 +- ...20211022093530_add_notify_to_followers.exs | 9 + test/graphql/resolvers/follower_test.exs | 2 +- test/graphql/resolvers/group_test.exs | 189 ++++++++++++++++++ 7 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 priv/repo/migrations/20211022093530_add_notify_to_followers.exs diff --git a/lib/graphql/resolvers/group.ex b/lib/graphql/resolvers/group.ex index 5a4ea22e..4942bac2 100644 --- a/lib/graphql/resolvers/group.ex +++ b/lib/graphql/resolvers/group.ex @@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do import Mobilizon.Users.Guards alias Mobilizon.Config alias Mobilizon.{Actors, Events} - alias Mobilizon.Actors.{Actor, Member} + alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Federation.ActivityPub.Actions alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.GraphQL.API @@ -320,6 +320,78 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do {:error, dgettext("errors", "You need to be logged-in to leave a group")} end + @doc """ + Follow a group + """ + @spec follow_group(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Follower.t()} | {:error, String.t()} + def follow_group(_parent, %{group_id: group_id, notify: _notify}, %{ + context: %{current_actor: %Actor{} = actor} + }) do + case Actors.get_actor(group_id) do + %Actor{type: :Group} = group -> + with {:ok, _activity, %Follower{} = follower} <- Actions.Follow.follow(actor, group) do + {:ok, follower} + end + + nil -> + {:error, dgettext("errors", "Group not found")} + end + end + + def follow_group(_parent, _args, _resolution) do + {:error, dgettext("errors", "You need to be logged-in to follow a group")} + end + + @doc """ + Update a group follow + """ + @spec update_group_follow(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Member.t()} | {:error, String.t()} + def update_group_follow(_parent, %{follow_id: follow_id, notify: notify}, %{ + context: %{current_actor: %Actor{} = actor} + }) do + case Actors.get_follower(follow_id) do + %Follower{} = follower -> + if follower.actor_id == actor.id do + # Update notify + Actors.update_follower(follower, %{notify: notify}) + else + {:error, dgettext("errors", "Follow does not match your account")} + end + + nil -> + {:error, dgettext("errors", "Follow not found")} + end + end + + def update_group_follow(_parent, _args, _resolution) do + {:error, dgettext("errors", "You need to be logged-in to update a group follow")} + end + + @doc """ + Unfollow a group + """ + @spec unfollow_group(any(), map(), Absinthe.Resolution.t()) :: + {:ok, Follower.t()} | {:error, String.t()} + def unfollow_group(_parent, %{group_id: group_id}, %{ + context: %{current_actor: %Actor{} = actor} + }) do + case Actors.get_actor(group_id) do + %Actor{type: :Group} = group -> + with {:ok, _activity, %Follower{} = follower} <- Actions.Follow.unfollow(actor, group) do + {:ok, follower} + end + + nil -> + {:error, dgettext("errors", "Group not found")} + end + end + + def unfollow_group(_parent, _args, _resolution) do + {:error, dgettext("errors", "You need to be logged-in to unfollow a group")} + end + @spec find_events_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Event.t())} def find_events_for_group( diff --git a/lib/graphql/schema/actors/follower.ex b/lib/graphql/schema/actors/follower.ex index 94da4335..fcfff284 100644 --- a/lib/graphql/schema/actors/follower.ex +++ b/lib/graphql/schema/actors/follower.ex @@ -17,6 +17,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.FollowerType do description: "Whether the follow has been approved by the target actor" ) + field(:notify, :boolean, + description: + "Whether the follower will be notified by the target actor's activity or not (applicable for profile/group follows)" + ) + field(:inserted_at, :datetime, description: "When the follow was created") field(:updated_at, :datetime, description: "When the follow was updated") end diff --git a/lib/graphql/schema/actors/group.ex b/lib/graphql/schema/actors/group.ex index 32e12a78..492382fa 100644 --- a/lib/graphql/schema/actors/group.ex +++ b/lib/graphql/schema/actors/group.ex @@ -205,6 +205,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do value(:private, description: "Visible only to people with the link - or invited") end + object :group_follow do + field(:group, :group, description: "The group followed") + field(:profile, :group, description: "The group followed") + field(:notify, :boolean, description: "Whether to notify profile from group activity") + end + object :group_queries do @desc "Get all groups" field :groups, :paginated_group_list do @@ -310,5 +316,36 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do resolve(&Group.delete_group/3) end + + @desc "Follow a group" + field :follow_group, :follower do + arg(:group_id, non_null(:id), description: "The group ID") + + arg(:notify, :boolean, + description: "Whether to notify profile from group activity", + default_value: true + ) + + resolve(&Group.follow_group/3) + end + + @desc "Update a group follow" + field :update_group_follow, :follower do + arg(:follow_id, non_null(:id), description: "The follow ID") + + arg(:notify, :boolean, + description: "Whether to notify profile from group activity", + default_value: true + ) + + resolve(&Group.update_group_follow/3) + end + + @desc "Unfollow a group" + field :unfollow_group, :follower do + arg(:group_id, non_null(:id), description: "The group ID") + + resolve(&Group.unfollow_group/3) + end end end diff --git a/lib/mobilizon/actors/follower.ex b/lib/mobilizon/actors/follower.ex index 396fb4b6..c40ab6d1 100644 --- a/lib/mobilizon/actors/follower.ex +++ b/lib/mobilizon/actors/follower.ex @@ -17,12 +17,14 @@ defmodule Mobilizon.Actors.Follower do url: String.t(), target_actor: Actor.t(), actor: Actor.t(), + notify: boolean(), inserted_at: DateTime.t(), updated_at: DateTime.t() } @required_attrs [:url, :approved, :target_actor_id, :actor_id] - @attrs @required_attrs + @optional_attrs [:notify] + @attrs @required_attrs ++ @optional_attrs @timestamps_opts [type: :utc_datetime] @@ -30,6 +32,7 @@ defmodule Mobilizon.Actors.Follower do schema "followers" do field(:approved, :boolean, default: false) field(:url, :string) + field(:notify, :boolean, default: true) timestamps() diff --git a/priv/repo/migrations/20211022093530_add_notify_to_followers.exs b/priv/repo/migrations/20211022093530_add_notify_to_followers.exs new file mode 100644 index 00000000..cd9ab1d1 --- /dev/null +++ b/priv/repo/migrations/20211022093530_add_notify_to_followers.exs @@ -0,0 +1,9 @@ +defmodule Mobilizon.Storage.Repo.Migrations.AddNotifyToFollowers do + use Ecto.Migration + + def change do + alter table(:followers) do + add(:notify, :boolean, default: true) + end + end +end diff --git a/test/graphql/resolvers/follower_test.exs b/test/graphql/resolvers/follower_test.exs index 682ef9dd..15af8bff 100644 --- a/test/graphql/resolvers/follower_test.exs +++ b/test/graphql/resolvers/follower_test.exs @@ -178,7 +178,7 @@ defmodule Mobilizon.Web.Resolvers.FollowerTest do } } """ - describe "update a follower update_follower/3" do + describe "approve a follower update_follower/3" do test "without being logged-in", %{ conn: conn, group: %Actor{} = group diff --git a/test/graphql/resolvers/group_test.exs b/test/graphql/resolvers/group_test.exs index 6143062c..4803fcc8 100644 --- a/test/graphql/resolvers/group_test.exs +++ b/test/graphql/resolvers/group_test.exs @@ -4,6 +4,7 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do import Mobilizon.Factory + alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.GraphQL.AbsintheHelpers @non_existent_username "nonexistent" @@ -468,4 +469,192 @@ defmodule Mobilizon.Web.Resolvers.GroupTest do assert hd(res["errors"])["message"] =~ "not an administrator" end end + + describe "follow a group" do + @follow_group_mutation """ + mutation FollowGroup($groupId: ID!, $notify: Boolean) { + followGroup(groupId: $groupId, notify: $notify) { + id + } + } + """ + + test "when not authenticated", %{conn: conn, user: _user} do + %Actor{type: :Group} = group = insert(:group) + + res = + conn + |> AbsintheHelpers.graphql_query( + query: @follow_group_mutation, + variables: %{groupId: group.id} + ) + + assert hd(res["errors"])["message"] == "You need to be logged-in to follow a group" + end + + test "when group doesn't exist", %{conn: conn, user: user} do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @follow_group_mutation, + variables: %{groupId: "89542"} + ) + + assert hd(res["errors"])["message"] == "Group not found" + end + + test "success", %{conn: conn, user: user} do + %Actor{type: :Group} = group = insert(:group) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @follow_group_mutation, + variables: %{groupId: group.id} + ) + + assert res["errors"] == nil + end + end + + describe "unfollow a group" do + @unfollow_group_mutation """ + mutation UnfollowGroup($groupId: ID!) { + unfollowGroup(groupId: $groupId) { + id + } + } + """ + + test "when not authenticated", %{conn: conn, user: _user} do + %Actor{type: :Group} = group = insert(:group) + + res = + conn + |> AbsintheHelpers.graphql_query( + query: @unfollow_group_mutation, + variables: %{groupId: group.id} + ) + + assert hd(res["errors"])["message"] == "You need to be logged-in to unfollow a group" + end + + test "when group doesn't exist", %{conn: conn, user: user} do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @unfollow_group_mutation, + variables: %{groupId: "89542"} + ) + + assert hd(res["errors"])["message"] == "Group not found" + end + + test "when the profile is not following the group", %{conn: conn, user: user} do + %Actor{type: :Group} = group = insert(:group) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @unfollow_group_mutation, + variables: %{groupId: group.id} + ) + + assert hd(res["errors"])["message"] =~ "Could not unfollow actor: you are not following" + end + + test "success", %{conn: conn, user: user, actor: actor} do + %Actor{type: :Group} = group = insert(:group) + + Mobilizon.Actors.follow(group, actor) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @unfollow_group_mutation, + variables: %{groupId: group.id} + ) + + assert res["errors"] == nil + assert Mobilizon.Actors.get_follower_by_followed_and_following(group, actor) == nil + end + end + + describe "update a group follow" do + @update_group_follow_mutation """ + mutation UpdateGroupFollow($followId: ID!, $notify: Boolean) { + updateGroupFollow(followId: $followId, notify: $notify) { + id + notify + } + } + """ + test "when not authenticated", %{conn: conn, user: _user, actor: actor} do + %Actor{type: :Group} = group = insert(:group) + follow = insert(:follower, target_actor: group, actor: actor) + + res = + conn + |> AbsintheHelpers.graphql_query( + query: @update_group_follow_mutation, + variables: %{followId: follow.id} + ) + + assert hd(res["errors"])["message"] == "You need to be logged-in to update a group follow" + end + + test "when follow doesn't exist", %{conn: conn, user: user} do + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_group_follow_mutation, + variables: %{followId: "d7c83493-e4a0-42a2-a15d-a469e955e80a"} + ) + + assert hd(res["errors"])["message"] == "Follow not found" + end + + test "when follow does not match the current actor", %{conn: conn, user: user} do + %Actor{type: :Group} = group = insert(:group) + follow = insert(:follower, target_actor: group) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_group_follow_mutation, + variables: %{followId: follow.id} + ) + + assert hd(res["errors"])["message"] == "Follow does not match your account" + end + + test "success", %{conn: conn, user: user, actor: actor} do + %Actor{type: :Group} = group = insert(:group) + follow = insert(:follower, target_actor: group, actor: actor) + + assert %Follower{notify: true} = + Mobilizon.Actors.get_follower_by_followed_and_following(group, actor) + + res = + conn + |> auth_conn(user) + |> AbsintheHelpers.graphql_query( + query: @update_group_follow_mutation, + variables: %{followId: follow.id, notify: false} + ) + + assert res["errors"] == nil + assert res["data"]["updateGroupFollow"]["notify"] == false + + assert %Follower{notify: false} = + Mobilizon.Actors.get_follower_by_followed_and_following(group, actor) + end + end end