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