Allow to join an open group
Also: * Refactor interacting with a remote event so that you can interact with a remote group as well * Add a setting for group admins to pick between an open and invite-only group * Fix new groups without posts/todos/resources/events/conversations URL set * Repair local groups that haven't got their posts/todos/resources/events/conversations URL set * Add a scheduled job to refresh remote groups every hour * Add a user setting to pick when to receive notifications when there's new members to approve (will be used when this feature is available) * Fix pagination for members Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -32,6 +32,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
Federator,
|
||||
Fetcher,
|
||||
Preloader,
|
||||
Refresher,
|
||||
Relay,
|
||||
Transmogrifier,
|
||||
Types,
|
||||
@@ -373,7 +374,9 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local \\ true, additional \\ %{}) do
|
||||
def join(entity_to_join, actor_joining, local \\ true, additional \\ %{})
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local, additional) do
|
||||
with {:ok, activity_data, participant} <- Types.Events.join(event, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
@@ -387,18 +390,15 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
def join_group(
|
||||
%{parent_id: _parent_id, actor_id: _actor_id, role: _role} = args,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
with {:ok, %Member{} = member} <-
|
||||
Mobilizon.Actors.create_member(args),
|
||||
activity_data when is_map(activity_data) <-
|
||||
Convertible.model_to_as(member),
|
||||
{:ok, activity} <- create_activity(Map.merge(activity_data, additional), local),
|
||||
def join(%Actor{type: :Group} = group, %Actor{} = actor, local, additional) do
|
||||
with {:ok, activity_data, %Member{} = member} <-
|
||||
Types.Actors.join(group, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
|
||||
@@ -899,6 +899,36 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_join(Member.t(), map) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp accept_join(%Member{} = member, additional) do
|
||||
with {:ok, %Member{} = member} <-
|
||||
Actors.update_member(member, %{role: :member}),
|
||||
_ <-
|
||||
unless(is_nil(member.parent.domain),
|
||||
do: Refresher.fetch_group(member.parent.url, member.actor)
|
||||
),
|
||||
Absinthe.Subscription.publish(Endpoint, member.actor,
|
||||
group_membership_changed: member.actor.id
|
||||
),
|
||||
member_as_data <- Convertible.model_to_as(member),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(member),
|
||||
update_data <-
|
||||
make_accept_join_data(
|
||||
member_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{member.id}"
|
||||
})
|
||||
) do
|
||||
{:ok, member, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp accept_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Share
|
||||
@@ -150,6 +150,12 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
%{"to" => [participant.actor.url], "cc" => actor_participants_urls}
|
||||
end
|
||||
|
||||
def calculate_to_and_cc_from_mentions(%Member{} = member) do
|
||||
member = Repo.preload(member, [:parent])
|
||||
|
||||
%{"to" => [member.parent.members_url], "cc" => []}
|
||||
end
|
||||
|
||||
def calculate_to_and_cc_from_mentions(%Actor{} = actor) do
|
||||
%{
|
||||
"to" => [@ap_public],
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier, Utils}
|
||||
alias Mobilizon.Storage.Repo
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
@@ -58,6 +59,10 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
:ok <- fetch_collection(discussions_url, on_behalf_of),
|
||||
:ok <- fetch_collection(events_url, on_behalf_of) do
|
||||
:ok
|
||||
else
|
||||
err ->
|
||||
Logger.error("Error while refreshing a group")
|
||||
Logger.error(inspect(err))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,6 +75,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of),
|
||||
:ok <- Logger.debug("Fetch ok, passing to process_collection"),
|
||||
:ok <- process_collection(data, on_behalf_of) do
|
||||
Logger.debug("Finished processing a collection")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -90,6 +96,19 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
end
|
||||
end
|
||||
|
||||
@spec refresh_all_external_groups :: any()
|
||||
def refresh_all_external_groups do
|
||||
Repo.transaction(fn ->
|
||||
Actors.list_external_groups_for_stream()
|
||||
|> Stream.map(fn %Actor{id: group_id, url: group_url} ->
|
||||
{group_url, Actors.get_single_group_member_actor(group_id)}
|
||||
end)
|
||||
|> Stream.filter(fn {_group_url, member_actor} -> not is_nil(member_actor) end)
|
||||
|> Stream.map(fn {group_url, member_actor} -> fetch_group(group_url, member_actor) end)
|
||||
|> Stream.run()
|
||||
end)
|
||||
end
|
||||
|
||||
defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of)
|
||||
when type in ["OrderedCollection", "OrderedCollectionPage"] do
|
||||
Logger.debug(
|
||||
@@ -99,6 +118,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
Logger.debug(inspect(items))
|
||||
|
||||
Enum.each(items, &handling_element/1)
|
||||
Logger.debug("Finished processing a collection")
|
||||
:ok
|
||||
end
|
||||
|
||||
|
||||
@@ -135,13 +135,22 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
|
||||
with object_data when is_map(object_data) <-
|
||||
object |> Converter.Member.as_to_model_data() do
|
||||
Logger.debug("Produced the following model data for member")
|
||||
Logger.debug(inspect(object_data))
|
||||
|
||||
with {:existing_member, nil} <-
|
||||
{:existing_member, Actors.get_member_by_url(object_data.url)},
|
||||
%Actor{type: :Group} = group <- Actors.get_actor(object_data.parent_id),
|
||||
%Actor{} = actor <- Actors.get_actor(object_data.actor_id),
|
||||
{:ok, %Activity{} = activity, %Member{} = member} <-
|
||||
ActivityPub.join_group(object_data, false) do
|
||||
ActivityPub.join(group, actor, false, %{
|
||||
url: object_data.url,
|
||||
metadata: %{role: object_data.role}
|
||||
}) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:existing_member, %Member{} = member} ->
|
||||
Logger.debug("Member already exists, updating member")
|
||||
{:ok, %Member{} = member} = Actors.update_member(member, object_data)
|
||||
|
||||
{:ok, nil, member}
|
||||
@@ -608,8 +617,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
"type" => "Join",
|
||||
"object" => object,
|
||||
"actor" => _actor,
|
||||
"id" => id,
|
||||
"participationMessage" => note
|
||||
"id" => id
|
||||
} = data
|
||||
) do
|
||||
with actor <- Utils.get_actor(data),
|
||||
@@ -618,7 +626,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.join(object, actor, false, %{url: id, metadata: %{message: note}}) do
|
||||
ActivityPub.join(object, actor, false, %{
|
||||
url: id,
|
||||
metadata: %{message: Map.get(data, "participationMessage")}
|
||||
}) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
@@ -804,8 +815,11 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
|
||||
{:error, _err} ->
|
||||
case get_member(join_object) do
|
||||
{:ok, member} ->
|
||||
do_handle_incoming_accept_join_group(member, actor_accepting)
|
||||
{:ok, %Member{invited_by: nil} = member} ->
|
||||
do_handle_incoming_accept_join_group(member, :join)
|
||||
|
||||
{:ok, %Member{} = member} ->
|
||||
do_handle_incoming_accept_join_group(member, :invite)
|
||||
|
||||
{:error, _err} ->
|
||||
nil
|
||||
@@ -847,7 +861,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_handle_incoming_accept_join_group(%Member{role: :member}, _actor) do
|
||||
defp do_handle_incoming_accept_join_group(%Member{role: :member}, _type) do
|
||||
Logger.debug(
|
||||
"Tried to handle an Accept activity on a Join activity with a group object but the member is already validated"
|
||||
)
|
||||
@@ -857,14 +871,14 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
|
||||
defp do_handle_incoming_accept_join_group(
|
||||
%Member{role: role, parent: _group} = member,
|
||||
%Actor{} = _actor_accepting
|
||||
type
|
||||
)
|
||||
when role in [:not_approved, :rejected, :invited] do
|
||||
when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do
|
||||
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
|
||||
# Or maybe for groups it's the group that sends the Accept activity
|
||||
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
|
||||
ActivityPub.accept(
|
||||
:invite,
|
||||
type,
|
||||
member,
|
||||
false
|
||||
) do
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
@moduledoc false
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
alias Mobilizon.Service.Notifications.Scheduler
|
||||
alias Mobilizon.Web.Endpoint
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
|
||||
|
||||
@behaviour Entity
|
||||
@@ -91,6 +94,42 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
def role_needed_to_update(%Actor{} = _group), do: :administrator
|
||||
def role_needed_to_delete(%Actor{} = _group), do: :administrator
|
||||
|
||||
@spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, map(), Member.t()}
|
||||
def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do
|
||||
with role <-
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Actors.get_default_member_role(group)),
|
||||
{:ok, %Member{} = member} <-
|
||||
Mobilizon.Actors.create_member(%{
|
||||
role: role,
|
||||
parent_id: group.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url),
|
||||
metadata:
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||
}),
|
||||
Absinthe.Subscription.publish(Endpoint, actor, group_membership_changed: actor.id),
|
||||
join_data <- %{
|
||||
"type" => "Join",
|
||||
"id" => member.url,
|
||||
"actor" => actor.url,
|
||||
"object" => group.url
|
||||
},
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(member) do
|
||||
approve_if_default_role_is_member(
|
||||
group,
|
||||
actor,
|
||||
Map.merge(join_data, audience),
|
||||
member,
|
||||
role
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_args_for_actor(args) do
|
||||
args
|
||||
|> maybe_sanitize_username()
|
||||
@@ -115,4 +154,39 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
end
|
||||
|
||||
defp maybe_sanitize_summary(args), do: args
|
||||
|
||||
# Set the participant to approved if the default role for new participants is :participant
|
||||
@spec approve_if_default_role_is_member(Actor.t(), Actor.t(), map(), Member.t(), atom()) ::
|
||||
{:ok, map(), Member.t()}
|
||||
defp approve_if_default_role_is_member(
|
||||
%Actor{type: :Group} = group,
|
||||
%Actor{} = actor,
|
||||
activity_data,
|
||||
%Member{} = member,
|
||||
role
|
||||
) do
|
||||
if is_nil(group.domain) && !is_nil(actor.domain) do
|
||||
cond do
|
||||
Mobilizon.Actors.get_default_member_role(group) === :member &&
|
||||
role == :member ->
|
||||
{:accept,
|
||||
ActivityPub.accept(
|
||||
:join,
|
||||
member,
|
||||
true,
|
||||
%{"actor" => group.url}
|
||||
)}
|
||||
|
||||
Mobilizon.Actors.get_default_member_role(group) === :not_approved &&
|
||||
role == :not_approved ->
|
||||
Scheduler.pending_membership_notification(group)
|
||||
{:ok, activity_data, member}
|
||||
|
||||
true ->
|
||||
{:ok, activity_data, member}
|
||||
end
|
||||
else
|
||||
{:ok, activity_data, member}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -65,7 +65,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
||||
domain: URI.parse(data["id"]).host,
|
||||
manually_approves_followers: data["manuallyApprovesFollowers"],
|
||||
type: data["type"],
|
||||
visibility: if(Map.get(data, "discoverable", false) == true, do: :public, else: :unlisted)
|
||||
visibility: if(Map.get(data, "discoverable", false) == true, do: :public, else: :unlisted),
|
||||
openness: data["openness"]
|
||||
}
|
||||
end
|
||||
|
||||
@@ -98,6 +99,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
||||
"sharedInbox" => actor.shared_inbox_url
|
||||
},
|
||||
"discoverable" => actor.visibility == :public,
|
||||
"openness" => actor.openness,
|
||||
"manuallyApprovesFollowers" => actor.manually_approves_followers,
|
||||
"publicKey" => %{
|
||||
"id" => "#{actor.url}#main-key",
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Member do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert an event struct to an ActivityStream representation.
|
||||
Convert an member struct to an ActivityStream representation.
|
||||
"""
|
||||
@spec model_to_as(MemberModel.t()) :: map
|
||||
def model_to_as(%MemberModel{} = member) do
|
||||
|
||||
@@ -72,6 +72,18 @@ defmodule Mobilizon.GraphQL.API.Search do
|
||||
end
|
||||
end
|
||||
|
||||
def interact(uri) do
|
||||
case ActivityPub.fetch_object_from_url(uri) do
|
||||
{:ok, object} ->
|
||||
{:ok, object}
|
||||
|
||||
{:error, _err} ->
|
||||
Logger.debug(fn -> "Unable to find or make object from URI '#{uri}'" end)
|
||||
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
# If the search string is an username
|
||||
@spec process_from_username(String.t()) :: Page.t()
|
||||
defp process_from_username(search) do
|
||||
|
||||
@@ -75,7 +75,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
||||
|
||||
def create_discussion(
|
||||
_parent,
|
||||
%{title: title, text: text, actor_id: actor_id},
|
||||
%{title: title, text: text, actor_id: group_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
@@ -83,20 +83,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, group_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.create(
|
||||
:discussion,
|
||||
%{
|
||||
title: title,
|
||||
text: text,
|
||||
actor_id: actor_id,
|
||||
actor_id: group_id,
|
||||
creator_id: creator_id,
|
||||
attributed_to_id: actor_id
|
||||
attributed_to_id: group_id
|
||||
},
|
||||
true
|
||||
) do
|
||||
{:ok, discussion}
|
||||
else
|
||||
{:member, false} ->
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -215,35 +215,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
@doc """
|
||||
Join an existing group
|
||||
"""
|
||||
def join_group(
|
||||
_parent,
|
||||
%{group_id: group_id, actor_id: actor_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {actor_id, ""} <- Integer.parse(actor_id),
|
||||
{group_id, ""} <- Integer.parse(group_id),
|
||||
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
def join_group(_parent, %{group_id: group_id} = args, %{
|
||||
context: %{current_user: %User{} = user}
|
||||
}) do
|
||||
with %Actor{} = actor <- Users.get_actor_for_user(user),
|
||||
{:ok, %Actor{type: :Group} = group} <-
|
||||
Actors.get_group_by_actor_id(group_id),
|
||||
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
|
||||
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
|
||||
role <- Member.get_default_member_role(group),
|
||||
{:ok, _} <- Actors.create_member(%{parent_id: group.id, actor_id: actor.id, role: role}) do
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
parent: Person.proxify_pictures(group),
|
||||
actor: Person.proxify_pictures(actor),
|
||||
role: role
|
||||
}
|
||||
}
|
||||
{:ok, _activity, %Member{} = member} <-
|
||||
ActivityPub.join(group, actor, true, args) do
|
||||
{:ok, member}
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
|
||||
{:error, :group_not_found} ->
|
||||
{:error, dgettext("errors", "Group not found")}
|
||||
|
||||
|
||||
@@ -25,4 +25,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
|
||||
def search_events(_parent, %{page: page, limit: limit} = args, _resolution) do
|
||||
Search.search_events(args, page, limit)
|
||||
end
|
||||
|
||||
def interact(_parent, %{uri: uri}, _resolution) do
|
||||
Search.interact(uri)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
Represents a group of actors
|
||||
"""
|
||||
object :group do
|
||||
interfaces([:actor])
|
||||
interfaces([:actor, :interactable])
|
||||
|
||||
field(:id, :id, description: "Internal ID for this group")
|
||||
field(:url, :string, description: "The ActivityPub actor's URL")
|
||||
@@ -196,6 +196,10 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
|
||||
arg(:visibility, :group_visibility, description: "The visibility for the group")
|
||||
|
||||
arg(:openness, :openness,
|
||||
description: "Whether the group can be join freely, with approval or is invite-only."
|
||||
)
|
||||
|
||||
arg(:avatar, :picture_input,
|
||||
description:
|
||||
"The avatar for the group, either as an object or directly the ID of an existing Picture"
|
||||
|
||||
@@ -38,7 +38,6 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
|
||||
@desc "Join a group"
|
||||
field :join_group, :member do
|
||||
arg(:group_id, non_null(:id))
|
||||
arg(:actor_id, non_null(:id))
|
||||
|
||||
resolve(&Group.join_group/3)
|
||||
end
|
||||
|
||||
@@ -197,5 +197,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||
{:ok, topic: args.person_id}
|
||||
end)
|
||||
end
|
||||
|
||||
field :group_membership_changed, :person do
|
||||
arg(:person_id, non_null(:id))
|
||||
|
||||
config(fn args, _ ->
|
||||
{:ok, topic: args.person_id}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
|
||||
@desc "An event"
|
||||
object :event do
|
||||
interfaces([:action_log_object])
|
||||
interfaces([:action_log_object, :interactable])
|
||||
field(:id, :id, description: "Internal ID for this event")
|
||||
field(:uuid, :uuid, description: "The Event UUID")
|
||||
field(:url, :string, description: "The ActivityPub Event URL")
|
||||
|
||||
@@ -4,6 +4,8 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.GraphQL.Resolvers.Search
|
||||
|
||||
@desc "Search persons result"
|
||||
@@ -24,6 +26,21 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
||||
field(:elements, non_null(list_of(:event)), description: "Event elements")
|
||||
end
|
||||
|
||||
interface :interactable do
|
||||
field(:url, :string, description: "A public URL for the entity")
|
||||
|
||||
resolve_type(fn
|
||||
%Actor{type: :Group}, _ ->
|
||||
:group
|
||||
|
||||
%Event{}, _ ->
|
||||
:event
|
||||
|
||||
_, _ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
|
||||
object :search_queries do
|
||||
@desc "Search persons"
|
||||
field :search_persons, :persons do
|
||||
@@ -58,5 +75,12 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
|
||||
|
||||
resolve(&Search.search_events/3)
|
||||
end
|
||||
|
||||
@desc "Interact with an URI"
|
||||
field :interact, :interactable do
|
||||
arg(:uri, non_null(:string), description: "The URI for to interact with")
|
||||
|
||||
resolve(&Search.interact/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -132,12 +132,17 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
description: "Whether this user will receive a notification right before event"
|
||||
)
|
||||
|
||||
field(:notification_pending_participation, :notification_pending_participation_enum,
|
||||
field(:notification_pending_participation, :notification_pending_enum,
|
||||
description: "When does the user receives a notification about new pending participations"
|
||||
)
|
||||
|
||||
field(:notification_pending_membership, :notification_pending_enum,
|
||||
description:
|
||||
"When does the user receives a notification about a new pending membership in one of the group they're admin for"
|
||||
)
|
||||
end
|
||||
|
||||
enum :notification_pending_participation_enum do
|
||||
enum :notification_pending_enum do
|
||||
value(:none, as: :none)
|
||||
value(:direct, as: :direct)
|
||||
value(:one_hour, as: :one_hour)
|
||||
@@ -258,7 +263,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
arg(:notification_on_day, :boolean)
|
||||
arg(:notification_each_week, :boolean)
|
||||
arg(:notification_before_event, :boolean)
|
||||
arg(:notification_pending_participation, :notification_pending_participation_enum)
|
||||
arg(:notification_pending_participation, :notification_pending_enum)
|
||||
arg(:notification_pending_membership, :notification_pending_enum)
|
||||
resolve(&User.set_user_setting/3)
|
||||
end
|
||||
|
||||
|
||||
@@ -84,7 +84,14 @@ defmodule Mobilizon.Actors.Actor do
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@update_required_attrs @required_attrs -- [:url]
|
||||
@update_optional_attrs [:name, :summary, :manually_approves_followers, :user_id, :visibility]
|
||||
@update_optional_attrs [
|
||||
:name,
|
||||
:summary,
|
||||
:manually_approves_followers,
|
||||
:user_id,
|
||||
:visibility,
|
||||
:openness
|
||||
]
|
||||
@update_attrs @update_required_attrs ++ @update_optional_attrs
|
||||
|
||||
@registration_required_attrs [:preferred_username, :keys, :suspended, :url, :type]
|
||||
@@ -113,7 +120,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:name,
|
||||
:summary,
|
||||
:manually_approves_followers,
|
||||
:visibility
|
||||
:visibility,
|
||||
:openness
|
||||
]
|
||||
@remote_actor_creation_attrs @remote_actor_creation_required_attrs ++
|
||||
@remote_actor_creation_optional_attrs
|
||||
@@ -349,6 +357,17 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|> put_change(:inbox_url, build_url(username, :inbox))
|
||||
|> put_change(:shared_inbox_url, "#{Endpoint.url()}/inbox")
|
||||
|> put_change(:members_url, if(type == :Group, do: build_url(username, :members), else: nil))
|
||||
|> put_change(
|
||||
:resources_url,
|
||||
if(type == :Group, do: build_url(username, :resources), else: nil)
|
||||
)
|
||||
|> put_change(:todos_url, if(type == :Group, do: build_url(username, :todos), else: nil))
|
||||
|> put_change(:posts_url, if(type == :Group, do: build_url(username, :posts), else: nil))
|
||||
|> put_change(:events_url, if(type == :Group, do: build_url(username, :events), else: nil))
|
||||
|> put_change(
|
||||
:discussions_url,
|
||||
if(type == :Group, do: build_url(username, :discussions), else: nil)
|
||||
)
|
||||
|> put_change(:url, build_url(username, :page))
|
||||
end
|
||||
|
||||
|
||||
@@ -640,6 +640,15 @@ defmodule Mobilizon.Actors do
|
||||
|> Repo.stream()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lists the groups.
|
||||
"""
|
||||
@spec list_groups_for_stream :: Enum.t()
|
||||
def list_external_groups_for_stream do
|
||||
external_groups_query()
|
||||
|> Repo.stream()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of groups an actor is member of.
|
||||
"""
|
||||
@@ -720,6 +729,13 @@ defmodule Mobilizon.Actors do
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the default member role depending on the event join options.
|
||||
"""
|
||||
@spec get_default_member_role(Actor.t()) :: :member | :not_approved
|
||||
def get_default_member_role(%Actor{openness: :open}), do: :member
|
||||
def get_default_member_role(%Actor{openness: _}), do: :not_approved
|
||||
|
||||
@doc """
|
||||
Gets a single member of an actor (for example a group).
|
||||
"""
|
||||
@@ -859,7 +875,7 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of administrator members for a group.
|
||||
Returns a paginated list of administrator members for a group.
|
||||
"""
|
||||
@spec list_administrator_members_for_group(integer | String.t(), integer | nil, integer | nil) ::
|
||||
Page.t()
|
||||
@@ -869,6 +885,16 @@ defmodule Mobilizon.Actors do
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the complete list of administrator members for a group.
|
||||
"""
|
||||
@spec list_all_administrator_members_for_group(integer | String.t()) :: [Member.t()]
|
||||
def list_all_administrator_members_for_group(id) do
|
||||
id
|
||||
|> administrator_members_for_group_query()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of all group ids where the actor_id is the last administrator.
|
||||
"""
|
||||
@@ -1400,6 +1426,11 @@ defmodule Mobilizon.Actors do
|
||||
)
|
||||
end
|
||||
|
||||
@spec external_groups_query :: Ecto.Query.t()
|
||||
defp external_groups_query do
|
||||
where(Actor, [a], a.type == ^:Group and not is_nil(a.domain))
|
||||
end
|
||||
|
||||
@spec list_members_for_user_query(integer()) :: Ecto.Query.t()
|
||||
defp list_members_for_user_query(user_id) do
|
||||
from(
|
||||
@@ -1422,11 +1453,10 @@ defmodule Mobilizon.Actors do
|
||||
|
||||
@spec members_for_group_query(integer | String.t()) :: Ecto.Query.t()
|
||||
defp members_for_group_query(group_id) do
|
||||
from(
|
||||
m in Member,
|
||||
where: m.parent_id == ^group_id,
|
||||
preload: [:parent, :actor]
|
||||
)
|
||||
Member
|
||||
|> where(parent_id: ^group_id)
|
||||
|> order_by(desc: :updated_at)
|
||||
|> preload([:parent, :actor])
|
||||
end
|
||||
|
||||
@spec group_external_member_actor_query(integer()) :: Ecto.Query.t()
|
||||
|
||||
@@ -13,6 +13,7 @@ defmodule Mobilizon.Users.Setting do
|
||||
notification_each_week: boolean,
|
||||
notification_before_event: boolean,
|
||||
notification_pending_participation: NotificationPendingNotificationDelay.t(),
|
||||
notification_pending_membership: NotificationPendingNotificationDelay.t(),
|
||||
user: User.t()
|
||||
}
|
||||
|
||||
@@ -23,7 +24,8 @@ defmodule Mobilizon.Users.Setting do
|
||||
:notification_on_day,
|
||||
:notification_each_week,
|
||||
:notification_before_event,
|
||||
:notification_pending_participation
|
||||
:notification_pending_participation,
|
||||
:notification_pending_membership
|
||||
]
|
||||
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
@@ -39,6 +41,10 @@ defmodule Mobilizon.Users.Setting do
|
||||
default: :one_day
|
||||
)
|
||||
|
||||
field(:notification_pending_membership, NotificationPendingNotificationDelay,
|
||||
default: :one_day
|
||||
)
|
||||
|
||||
belongs_to(:user, User, primary_key: true, type: :id, foreign_key: :id, define_field: false)
|
||||
|
||||
timestamps()
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Service.Workers.Notification
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
@@ -193,6 +193,77 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
|
||||
|
||||
def pending_participation_notification(_), do: {:ok, nil}
|
||||
|
||||
def pending_membership_notification(%Actor{type: :Group, id: group_id}) do
|
||||
group_id
|
||||
|> Actors.list_all_administrator_members_for_group()
|
||||
|> Enum.map(fn %Member{actor: %Actor{id: actor_id}} ->
|
||||
Actors.get_actor(actor_id)
|
||||
end)
|
||||
|> Enum.each(fn actor -> pending_membership_admin_notification(actor, group_id) end)
|
||||
end
|
||||
|
||||
def pending_membership_notification(_), do: {:ok, nil}
|
||||
|
||||
defp pending_membership_admin_notification(%Actor{user_id: user_id}, group_id)
|
||||
when not is_nil(user_id) do
|
||||
case Users.get_user_with_settings!(user_id) do
|
||||
%User{} = user ->
|
||||
pending_membership_admin_notification_user(user, group_id)
|
||||
|
||||
# No user for actor, probably a remote actor, ignore
|
||||
_ ->
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp pending_membership_admin_notification_user(
|
||||
%User{
|
||||
id: user_id,
|
||||
locale: locale,
|
||||
settings: %Setting{
|
||||
notification_pending_membership: notification_pending_membership,
|
||||
timezone: timezone
|
||||
}
|
||||
},
|
||||
group_id
|
||||
) do
|
||||
send_at =
|
||||
case notification_pending_membership do
|
||||
:none ->
|
||||
nil
|
||||
|
||||
:direct ->
|
||||
:direct
|
||||
|
||||
:one_day ->
|
||||
calculate_next_day_notification(Date.utc_today(), timezone, locale)
|
||||
|
||||
:one_hour ->
|
||||
DateTime.utc_now()
|
||||
|> DateTime.shift_zone!(timezone)
|
||||
|> (&%{&1 | minute: 0, second: 0, microsecond: {0, 0}}).()
|
||||
end
|
||||
|
||||
params = %{
|
||||
user_id: user_id,
|
||||
group_id: group_id
|
||||
}
|
||||
|
||||
cond do
|
||||
# Sending directly
|
||||
send_at == :direct ->
|
||||
Notification.enqueue(:pending_membership_notification, params)
|
||||
|
||||
# Not sending
|
||||
is_nil(send_at) ->
|
||||
{:ok, nil}
|
||||
|
||||
# Sending to calculated time
|
||||
true ->
|
||||
Notification.enqueue(:pending_membership_notification, params, scheduled_at: send_at)
|
||||
end
|
||||
end
|
||||
|
||||
defp shift_zone(datetime, timezone) do
|
||||
case DateTime.shift_zone(datetime, timezone) do
|
||||
{:ok, shift_datetime} -> shift_datetime
|
||||
|
||||
12
lib/service/workers/refresh_groups.ex
Normal file
12
lib/service/workers/refresh_groups.ex
Normal file
@@ -0,0 +1,12 @@
|
||||
defmodule Mobilizon.Service.Workers.RefreshGroups do
|
||||
@moduledoc """
|
||||
Worker to build sitemap
|
||||
"""
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Refresher
|
||||
|
||||
use Oban.Worker, queue: "background"
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(%Job{}), do: Refresher.refresh_all_external_groups()
|
||||
end
|
||||
Reference in New Issue
Block a user