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:
Thomas Citharel
2020-11-06 11:34:32 +01:00
parent 7baad7cafc
commit 7c11807c14
74 changed files with 1174 additions and 626 deletions

View File

@@ -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,

View File

@@ -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],

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")}

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View 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