Merge branch 'feature/federate-participations' into 'master'

Federate participations

See merge request framasoft/mobilizon!166
This commit is contained in:
Thomas Citharel 2019-08-20 10:37:18 +02:00
commit 4bc70d5070
18 changed files with 752 additions and 123 deletions

View File

@ -58,6 +58,10 @@ defmodule Mobilizon.Actors do
Repo.get!(Actor, id)
end
def get_actor(id) do
Repo.get(Actor, id)
end
# Get actor by ID and preload organized events, followers and followings
@spec get_actor_with_everything(integer()) :: Ecto.Query.t()
defp do_get_actor_with_everything(id) do

View File

@ -214,6 +214,27 @@ defmodule Mobilizon.Events do
])
end
@doc """
Gets a single event, with all associations loaded.
"""
def get_event_full(id) do
case Repo.get(Event, id) do
%Event{} = event ->
{:ok,
Repo.preload(event, [
:organizer_actor,
:sessions,
:tracks,
:tags,
:participants,
:physical_address
])}
err ->
{:error, err}
end
end
@doc """
Gets an event by it's URL
"""
@ -700,17 +721,28 @@ defmodule Mobilizon.Events do
[%Participant{}, ...]
"""
def list_participants_for_event(uuid, page \\ nil, limit \\ nil) do
Repo.all(
from(
p in Participant,
join: e in Event,
on: p.event_id == e.id,
where: e.uuid == ^uuid and p.role != ^:not_approved,
preload: [:actor]
)
|> paginate(page, limit)
def list_participants_for_event(uuid, page \\ nil, limit \\ nil, include_not_improved \\ false)
def list_participants_for_event(uuid, page, limit, false) do
query = do_list_participants_for_event(uuid, page, limit)
query = from(p in query, where: p.role != ^:not_approved)
Repo.all(query)
end
def list_participants_for_event(uuid, page, limit, true) do
query = do_list_participants_for_event(uuid, page, limit)
Repo.all(query)
end
defp do_list_participants_for_event(uuid, page, limit) do
from(
p in Participant,
join: e in Event,
on: p.event_id == e.id,
where: e.uuid == ^uuid,
preload: [:actor]
)
|> paginate(page, limit)
end
@doc """
@ -787,6 +819,15 @@ defmodule Mobilizon.Events do
end
end
def get_participant_by_url(url) do
Repo.one(
from(p in Participant,
where: p.url == ^url,
preload: [:actor, :event]
)
)
end
@doc """
Creates a participant.
@ -800,9 +841,10 @@ defmodule Mobilizon.Events do
"""
def create_participant(attrs \\ %{}) do
%Participant{}
|> Participant.changeset(attrs)
|> Repo.insert()
with {:ok, %Participant{} = participant} <-
%Participant{} |> Participant.changeset(attrs) |> Repo.insert() do
{:ok, Repo.preload(participant, [:event, :actor])}
end
end
@doc """

View File

@ -17,9 +17,10 @@ defmodule Mobilizon.Events.Participant do
alias Mobilizon.Events.{Participant, Event}
alias Mobilizon.Actors.Actor
@primary_key false
@primary_key {:id, :binary_id, autogenerate: true}
schema "participants" do
field(:role, Mobilizon.Events.ParticipantRoleEnum, default: :participant)
field(:url, :string)
belongs_to(:event, Event, primary_key: true)
belongs_to(:actor, Actor, primary_key: true)
@ -29,7 +30,49 @@ defmodule Mobilizon.Events.Participant do
@doc false
def changeset(%Participant{} = participant, attrs) do
participant
|> Ecto.Changeset.cast(attrs, [:role, :event_id, :actor_id])
|> validate_required([:role, :event_id, :actor_id])
|> Ecto.Changeset.cast(attrs, [:url, :role, :event_id, :actor_id])
|> generate_url()
|> validate_required([:url, :role, :event_id, :actor_id])
end
# If there's a blank URL that's because we're doing the first insert
defp generate_url(%Ecto.Changeset{data: %Participant{url: nil}} = changeset) do
case fetch_change(changeset, :url) do
{:ok, _url} -> changeset
:error -> do_generate_url(changeset)
end
end
# Most time just go with the given URL
defp generate_url(%Ecto.Changeset{} = changeset), do: changeset
defp do_generate_url(%Ecto.Changeset{} = changeset) do
uuid = Ecto.UUID.generate()
changeset
|> put_change(
:url,
"#{MobilizonWeb.Endpoint.url()}/join/event/#{uuid}"
)
|> put_change(
:id,
uuid
)
end
@doc """
We check that the actor asking to leave the event is not it's only organizer
We start by fetching the list of organizers and if there's only one of them
and that it's the actor requesting leaving the event we return true
"""
@spec check_that_participant_is_not_only_organizer(integer(), integer()) :: boolean()
def check_that_participant_is_not_only_organizer(event_id, actor_id) do
case Mobilizon.Events.list_organizers_participants_for_event(event_id) do
[%Participant{actor: %Actor{id: participant_actor_id}}] ->
participant_actor_id == actor_id
_ ->
false
end
end
end

View File

@ -2,15 +2,12 @@ defmodule MobilizonWeb.API.Events do
@moduledoc """
API for Events
"""
alias Mobilizon.Addresses
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils
@visibility %{"PUBLIC" => :public, "PRIVATE" => :private}
@doc """
Create an event
"""
@ -51,15 +48,4 @@ defmodule MobilizonWeb.API.Events do
})
end
end
defp get_physical_address(address_id) when is_number(address_id),
do: Addresses.get_address!(address_id)
defp get_physical_address(address_id) when is_binary(address_id) do
with {address_id, ""} <- Integer.parse(address_id) do
get_physical_address(address_id)
end
end
defp get_physical_address(nil), do: nil
end

View File

@ -0,0 +1,24 @@
defmodule MobilizonWeb.API.Participations do
@moduledoc """
Common API to join events and groups
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Service.ActivityPub
require Logger
@spec join(Event.t(), Actor.t()) :: {:ok, Participant.t()}
def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor) do
with {:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
{:ok, activity, participant} <- ActivityPub.join(event, actor, true) do
{:ok, activity, participant}
end
end
def leave(%Event{} = event, %Actor{} = actor) do
with {:ok, activity, participant} <- ActivityPub.leave(event, actor, true) do
{:ok, activity, participant}
end
end
end

View File

@ -8,7 +8,6 @@ defmodule MobilizonWeb.Resolvers.Event do
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Media.Picture
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
alias MobilizonWeb.Resolvers.Person
@ -108,20 +107,18 @@ defmodule MobilizonWeb.Resolvers.Event do
}
) do
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
{:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_full(event_id)},
{:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
role <- Mobilizon.Events.get_default_participant_role(event),
{:ok, participant} <-
Mobilizon.Events.create_participant(%{
role: role,
event_id: event.id,
actor_id: actor.id
}),
{:ok, _activity, participant} <- MobilizonWeb.API.Participations.join(event, actor),
participant <-
Map.put(participant, :event, event)
|> Map.put(:actor, Person.proxify_pictures(actor)) do
{:ok, participant}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, false} ->
{:error, "Actor id is not owned by authenticated user"}
@ -149,15 +146,15 @@ defmodule MobilizonWeb.Resolvers.Event do
}
}
) do
with {:is_owned, true, _} <- User.owns_actor(user, actor_id),
{:ok, %Participant{} = participant} <-
Mobilizon.Events.get_participant(event_id, actor_id),
{:only_organizer, false} <-
{:only_organizer, check_that_participant_is_not_only_organizer(event_id, actor_id)},
{:ok, _} <-
Mobilizon.Events.delete_participant(participant) do
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_full(event_id)},
{:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, false} ->
{:error, "Actor id is not owned by authenticated user"}
@ -173,20 +170,6 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to leave an event"}
end
# We check that the actor asking to leave the event is not it's only organizer
# We start by fetching the list of organizers and if there's only one of them
# and that it's the actor requesting leaving the event we return true
@spec check_that_participant_is_not_only_organizer(integer(), integer()) :: boolean()
defp check_that_participant_is_not_only_organizer(event_id, actor_id) do
case Mobilizon.Events.list_organizers_participants_for_event(event_id) do
[%Participant{actor: %Actor{id: participant_actor_id}}] ->
participant_actor_id == actor_id
_ ->
false
end
end
@doc """
Create an event
"""

View File

@ -11,18 +11,19 @@ defmodule Mobilizon.Service.ActivityPub do
"""
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment}
alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Service.ActivityPub.Transmogrifier
alias Mobilizon.Service.WebFinger
alias Mobilizon.Activity
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors.Follower
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Service.Federator
alias Mobilizon.Service.HTTPSignatures.Signature
alias Mobilizon.Service.ActivityPub.Convertible
require Logger
import Mobilizon.Service.ActivityPub.Utils
import Mobilizon.Service.ActivityPub.Visibility
@ -148,7 +149,7 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def accept(%{to: to, actor: actor, object: object} = params, activity_follow_id \\ nil) do
def accept(%{to: to, actor: actor, object: object} = params, activity_wrapper_id \\ nil) do
# only accept false as false value
local = !(params[:local] == false)
@ -157,7 +158,7 @@ defmodule Mobilizon.Service.ActivityPub do
"type" => "Accept",
"actor" => actor,
"object" => object,
"id" => activity_follow_id || get_url(object) <> "/activity"
"id" => activity_wrapper_id || get_url(object) <> "/activity"
},
{:ok, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do
@ -165,11 +166,17 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def reject(%{to: to, actor: actor, object: object} = params) do
def reject(%{to: to, actor: actor, object: object} = params, activity_wrapper_id \\ nil) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.url, "object" => object},
with data <- %{
"to" => to,
"type" => "Reject",
"actor" => actor,
"object" => object,
"id" => activity_wrapper_id || get_url(object) <> "/activity"
},
{:ok, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
@ -383,6 +390,65 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def join(object, actor, local \\ true)
def join(%Event{} = event, %Actor{} = actor, local) do
with role <- Mobilizon.Events.get_default_participant_role(event),
{:ok, %Participant{} = participant} <-
Mobilizon.Events.create_participant(%{
role: role,
event_id: event.id,
actor_id: actor.id
}),
join_data <- Convertible.model_to_as(participant),
join_data <- Map.put(join_data, "to", [event.organizer_actor.url]),
join_data <- Map.put(join_data, "cc", []),
{:ok, activity, _} <- insert(join_data, local),
:ok <- maybe_federate(activity) do
if role === :participant do
accept(
%{to: [actor.url], actor: event.organizer_actor.url, object: join_data["id"]},
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{participant.id}"
)
end
{:ok, activity, participant}
end
end
# TODO: Implement me
def join(%Actor{type: :Group} = _group, %Actor{} = _actor, _local) do
:error
end
def leave(object, actor, local \\ true)
# TODO: If we want to use this for exclusion we need to have an extra field for the actor that excluded the participant
def leave(
%Event{id: event_id, url: event_url} = event,
%Actor{id: actor_id, url: actor_url} = _actor,
local
) do
with {:only_organizer, false} <-
{:only_organizer,
Participant.check_that_participant_is_not_only_organizer(event_id, actor_id)},
{:ok, %Participant{} = participant} <-
Mobilizon.Events.get_participant(event_id, actor_id),
{:ok, %Participant{} = participant} <- Mobilizon.Events.delete_participant(participant),
leave_data <- %{
"type" => "Leave",
# If it's an exclusion it should be something else
"actor" => actor_url,
"object" => event_url,
"to" => [event.organizer_actor.url],
"cc" => []
},
{:ok, activity, _} <- insert(leave_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, participant}
end
end
@doc """
Create an actor locally by it's URL (AP ID)
"""
@ -482,7 +548,7 @@ defmodule Mobilizon.Service.ActivityPub do
"""
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}")
%URI{host: host, path: path} = URI.parse(inbox)
%URI{host: host, path: _path} = URI.parse(inbox)
digest = Signature.build_digest(json)
date = Signature.generate_date_header()

View File

@ -10,8 +10,6 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
require Logger
@behaviour Converter

View File

@ -0,0 +1,29 @@
defmodule Mobilizon.Service.ActivityPub.Converters.Participant do
@moduledoc """
Flag converter
This module allows to convert reports from ActivityStream format to our own internal one, and back.
Note: Reports are named Flag in AS.
"""
alias Mobilizon.Events.Participant, as: ParticipantModel
@doc """
Convert an event struct to an ActivityStream representation
"""
@spec model_to_as(ParticipantModel.t()) :: map()
def model_to_as(%ParticipantModel{} = participant) do
%{
"type" => "Join",
"id" => participant.url,
"actor" => participant.actor.url,
"object" => participant.event.url
}
end
defimpl Mobilizon.Service.ActivityPub.Convertible, for: Mobilizon.Events.Participant do
alias Mobilizon.Service.ActivityPub.Converters.Participant, as: ParticipantConverter
defdelegate model_to_as(event), to: ParticipantConverter
end
end

View File

@ -10,7 +10,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment}
alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Service.ActivityPub.Visibility
@ -185,7 +185,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
@ -198,61 +198,65 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
# TODO : Handle object being a Link
def handle_incoming(
%{
"type" => "Accept",
"object" => follow_object,
"object" => accepted_object,
"actor" => _actor,
"id" => _id
"id" => id
} = data
) do
with followed_actor_url <- get_actor(data),
{:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed_actor_url),
{:ok, %Follower{approved: false, actor: follower, id: follow_id} = follow} <-
get_follow(follow_object),
{:ok, activity, _} <-
ActivityPub.accept(
%{
to: [follower.url],
actor: followed.url,
object: follow_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}"
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
{:ok, activity, follow}
with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <-
{:object_not_found,
do_handle_incoming_accept_following(accepted_object, actor) ||
do_handle_incoming_accept_join(accepted_object, actor)} do
{:ok, activity, object}
else
{:ok, %Follower{approved: true} = _follow} ->
{:error, "Follow already accepted"}
{:object_not_found, nil} ->
Logger.warn(
"Unable to process Accept activity #{inspect(id)}. Object #{inspect(accepted_object)} wasn't found."
)
:error
e ->
Logger.warn("Unable to process Accept Follow activity #{inspect(e)}")
Logger.warn(
"Unable to process Accept activity #{inspect(id)} for object #{inspect(accepted_object)} only returned #{
inspect(e)
}"
)
:error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
%{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data
) do
with followed_actor_url <- get_actor(data),
{:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed_actor_url),
{:ok, %Follower{approved: false, actor: follower, id: follow_id} = follow} <-
get_follow(follow_object),
{:ok, activity, object} <-
ActivityPub.reject(%{
to: [follower.url],
type: "Reject",
actor: followed,
object: follow_object,
local: false
}),
{:ok, _follower} <- Actor.unfollow(followed, follower) do
with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <-
{:object_not_found,
do_handle_incoming_reject_following(rejected_object, actor) ||
do_handle_incoming_reject_join(rejected_object, actor)} do
{:ok, activity, object}
else
{:object_not_found, nil} ->
Logger.warn(
"Unable to process Reject activity #{inspect(id)}. Object #{inspect(rejected_object)} wasn't found."
)
:error
e ->
Logger.debug(inspect(e))
Logger.warn(
"Unable to process Reject activity #{inspect(id)} for object #{inspect(rejected_object)} only returned #{
inspect(e)
}"
)
:error
end
end
@ -272,7 +276,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# end
# #
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
@ -320,7 +324,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
"object" => object_id,
"id" => cancelled_activity_id
},
"actor" => actor,
"actor" => _actor,
"id" => id
} = data
) do
@ -378,6 +382,43 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Join", "object" => object, "actor" => _actor, "id" => _id} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{url: _actor_url} = actor} <- Actors.get_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper(object),
{:ok, activity, object} <- ActivityPub.join(object, actor, false) do
{:ok, activity, object}
else
e ->
Logger.debug(inspect(e))
:error
end
end
def handle_incoming(
%{"type" => "Leave", "object" => object, "actor" => actor, "id" => _id} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper(object),
{:ok, activity, object} <- ActivityPub.leave(object, actor, false) do
{:ok, activity, object}
else
{:only_organizer, true} ->
Logger.warn(
"Actor #{inspect(actor)} tried to leave event #{inspect(object)} but it was the only organizer so we didn't detach it"
)
:error
e ->
Logger.error(inspect(e))
:error
end
end
#
# # TODO
# # Accept
@ -406,13 +447,187 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
{:error, :not_supported}
end
@doc """
Handle incoming `Accept` activities wrapping a `Follow` activity
"""
def do_handle_incoming_accept_following(follow_object, %Actor{} = actor) do
with {:follow,
{:ok,
%Follower{approved: false, actor: follower, id: follow_id, target_actor: followed} =
follow}} <-
{:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <-
ActivityPub.accept(
%{
to: [follower.url],
actor: actor.url,
object: follow_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}"
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
{:ok, activity, follow}
else
{:follow, _} ->
Logger.debug(
"Tried to handle an Accept activity but it's not containing a Follow activity"
)
nil
{:same_actor} ->
{:error, "Actor who accepted the follow wasn't the target. Quite odd."}
{:ok, %Follower{approved: true} = _follow} ->
{:error, "Follow already accepted"}
end
end
@doc """
Handle incoming `Reject` activities wrapping a `Follow` activity
"""
def do_handle_incoming_reject_following(follow_object, %Actor{} = actor) do
with {:follow,
{:ok,
%Follower{approved: false, actor: follower, id: follow_id, target_actor: followed} =
follow}} <-
{:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <-
ActivityPub.reject(
%{
to: [follower.url],
actor: actor.url,
object: follow_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/reject/follow/#{follow_id}"
),
{:ok, %Follower{}} <- Actors.delete_follower(follow) do
{:ok, activity, follow}
else
{:follow, _} ->
Logger.debug(
"Tried to handle a Reject activity but it's not containing a Follow activity"
)
nil
{:same_actor} ->
{:error, "Actor who rejected the follow wasn't the target. Quite odd."}
{:ok, %Follower{approved: true} = _follow} ->
{:error, "Follow already accepted"}
end
end
@doc """
Handle incoming `Accept` activities wrapping a `Join` activity on an event
"""
def do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
with {:join_event,
{:ok,
%Participant{role: :not_approved, actor: actor, id: join_id, event: event} =
participant}} <-
{:join_event, get_participant(join_object)},
# 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
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
{:ok, activity, _} <-
ActivityPub.accept(
%{
to: [actor.url],
actor: actor_accepting.url,
object: join_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{join_id}"
),
{:ok, %Participant{role: :participant}} <-
Events.update_participant(participant, %{"role" => :participant}) do
{:ok, activity, participant}
else
{:join_event, {:ok, %Participant{role: :participant}}} ->
Logger.debug(
"Tried to handle an Accept activity on a Join activity with a event object but the participant is already validated"
)
nil
{:join_event, _err} ->
Logger.debug(
"Tried to handle an Accept activity but it's not containing a Join activity on a event"
)
nil
{:same_actor} ->
{:error, "Actor who accepted the join wasn't the event organizer. Quite odd."}
{:ok, %Participant{role: :participant} = _follow} ->
{:error, "Participant"}
end
end
@doc """
Handle incoming `Reject` activities wrapping a `Join` activity on an event
"""
def do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do
with {:join_event,
{:ok,
%Participant{role: :not_approved, actor: actor, id: join_id, event: event} =
participant}} <-
{:join_event, get_participant(join_object)},
# 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
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
{:ok, activity, _} <-
ActivityPub.reject(
%{
to: [actor.url],
actor: actor_accepting.url,
object: join_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/reject/join/#{join_id}"
),
{:ok, %Participant{}} <-
Events.delete_participant(participant) do
{:ok, activity, participant}
else
{:join_event, {:ok, %Participant{role: :participant}}} ->
Logger.debug(
"Tried to handle an Reject activity on a Join activity with a event object but the participant is already validated"
)
nil
{:join_event, _err} ->
Logger.debug(
"Tried to handle an Reject activity but it's not containing a Join activity on a event"
)
nil
{:same_actor} ->
{:error, "Actor who rejected the join wasn't the event organizer. Quite odd."}
{:ok, %Participant{role: :participant} = _follow} ->
{:error, "Participant"}
end
end
# TODO: Add do_handle_incoming_accept_join/1 on Groups
defp get_follow(follow_object) do
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
{:not_found, %Follower{} = follow} <-
{:not_found, Actors.get_follow_by_url(follow_object_id)} do
{:ok, follow}
else
{:not_found, err} ->
{:not_found, _err} ->
{:error, "Follow URL not found"}
_ ->
@ -420,6 +635,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
defp get_participant(join_object) do
with join_object_id when not is_nil(join_object_id) <- Utils.get_url(join_object),
{:not_found, %Participant{} = participant} <-
{:not_found, Events.get_participant_by_url(join_object_id)} do
{:ok, participant}
else
{:not_found, _err} ->
{:error, "Participant URL not found"}
_ ->
{:error, "ActivityPub ID not found in Accept Join object"}
end
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) do
with false <- String.starts_with?(in_reply_to, "http"),
{:ok, replied_to_object} <- fetch_obj_helper(in_reply_to) do

View File

@ -512,7 +512,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
object_actor_url,
object_url,
activity_id,
public \\ true
public
) do
{to, cc} =
if public do
@ -611,6 +611,24 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|> Map.merge(additional)
end
def make_join_data(%Event{} = event, %Actor{} = actor) do
%{
"type" => "Join",
"id" => "#{actor.url}/join/event/id",
"actor" => actor.url,
"object" => event.url
}
end
def make_join_data(%Actor{type: :Group} = event, %Actor{} = actor) do
%{
"type" => "Join",
"id" => "#{actor.url}/join/group/id",
"actor" => actor.url,
"object" => event.url
}
end
@doc """
Converts PEM encoded keys to a public key representation
"""

View File

@ -8,7 +8,6 @@ defmodule Mobilizon.Service.ActivityPub.Visibility do
Utility functions related to content visibility
"""
alias Mobilizon.Activity
alias Mobilizon.Events.Event
@public "https://www.w3.org/ns/activitystreams#Public"

View File

@ -0,0 +1,43 @@
defmodule Mobilizon.Repo.Migrations.AddUrlAndUuidToParticipants do
use Ecto.Migration
def up do
drop(index(:participants, :event_id))
drop_if_exists(index(:participants, :account_id))
drop_if_exists(index(:participants, :actor_id))
drop(constraint(:participants, "participants_event_id_fkey"))
# This is because even though we renamed the table accounts to actors indexes kept this name
drop_if_exists(constraint(:participants, "participants_account_id_fkey"))
drop_if_exists(constraint(:participants, "participants_actor_id_fkey"))
drop(constraint(:participants, "participants_pkey"))
alter table(:participants, primary_key: false) do
modify(:event_id, references(:events, on_delete: :delete_all), primary_key: false)
modify(:actor_id, references(:actors, on_delete: :delete_all), primary_key: false)
add(:id, :uuid, primary_key: true)
add(:url, :string, null: false)
end
create(index(:participants, :event_id))
create(index(:participants, :actor_id))
end
def down do
drop(index(:participants, :event_id))
drop(index(:participants, :actor_id))
drop(constraint(:participants, "participants_event_id_fkey"))
drop(constraint(:participants, "participants_actor_id_fkey"))
drop(constraint(:participants, "participants_pkey"))
alter table(:participants, primary_key: false) do
modify(:event_id, references(:events, on_delete: :nothing), primary_key: true)
modify(:actor_id, references(:actors, on_delete: :nothing), primary_key: true)
remove(:id)
remove(:url)
end
create(index(:participants, :event_id))
create(index(:participants, :actor_id))
end
end

View File

@ -0,0 +1,22 @@
{
"type": "Join",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==",
"creator": "http://mobilizon.test/users/tcit#main-key",
"created": "2018-02-17T13:29:31Z"
},
"object": "http://mobilizon.test/events/some-uuid",
"id": "http://mobilizon2.test/@admin/join/event/1",
"actor": "http://mobilizon2.test/@admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"Hashtag": "as:Hashtag"
}
]
}

View File

@ -0,0 +1,22 @@
{
"type": "Leave",
"signature": {
"type": "RsaSignature2017",
"signatureValue": "Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==",
"creator": "http://mobilizon.test/users/tcit#main-key",
"created": "2018-02-17T13:29:31Z"
},
"object": "http://mobilizon.test/events/some-uuid",
"id": "http://mobilizon2.test/@admin/join/event/1",
"actor": "http://mobilizon2.test/@admin",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"Hashtag": "as:Hashtag"
}
]
}

View File

@ -12,7 +12,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Events.{Comment, Event, Participant}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Service.ActivityPub.Transmogrifier
@ -695,6 +695,120 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert activity.data["actor"] == reporter_url
assert activity.data["cc"] == [reported_url]
end
test "it accepts Join activities" do
%Actor{url: _organizer_url} = organizer = insert(:actor)
%Actor{url: participant_url} = _participant = insert(:actor)
%Event{url: event_url} = _event = insert(:event, organizer_actor: organizer)
join_data =
File.read!("test/fixtures/mobilizon-join-activity.json")
|> Jason.decode!()
|> Map.put("actor", participant_url)
|> Map.put("object", event_url)
assert {:ok, activity, _} = Transmogrifier.handle_incoming(join_data)
assert activity.data["object"] == event_url
assert activity.data["actor"] == participant_url
end
test "it accepts Accept activities for Join activities" do
%Actor{url: organizer_url} = organizer = insert(:actor)
%Actor{} = participant_actor = insert(:actor)
%Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted)
{:ok, join_activity, participation} = ActivityPub.join(event, participant_actor)
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Jason.decode!()
|> Map.put("actor", organizer_url)
|> Map.put("object", participation.url)
{:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data)
assert accept_activity.data["object"] == join_activity.data["id"]
assert accept_activity.data["object"] =~ "/join/"
assert accept_activity.data["id"] =~ "/accept/join/"
# We don't accept already accepted Accept activities
:error = Transmogrifier.handle_incoming(accept_data)
end
test "it accepts Reject activities for Join activities" do
%Actor{url: organizer_url} = organizer = insert(:actor)
%Actor{} = participant_actor = insert(:actor)
%Event{} = event = insert(:event, organizer_actor: organizer, join_options: :restricted)
{:ok, join_activity, participation} = ActivityPub.join(event, participant_actor)
reject_data =
File.read!("test/fixtures/mastodon-reject-activity.json")
|> Jason.decode!()
|> Map.put("actor", organizer_url)
|> Map.put("object", participation.url)
{:ok, reject_activity, _} = Transmogrifier.handle_incoming(reject_data)
assert reject_activity.data["object"] == join_activity.data["id"]
assert reject_activity.data["object"] =~ "/join/"
assert reject_activity.data["id"] =~ "/reject/join/"
# We don't accept already rejected Reject activities
assert :error == Transmogrifier.handle_incoming(reject_data)
# Organiser is not present since we use factories directly
assert Events.list_participants_for_event(event.uuid, 1, 10, true) |> Enum.map(& &1.id) ==
[]
end
test "it accepts Leave activities" do
%Actor{url: _organizer_url} = organizer = insert(:actor)
%Actor{url: participant_url} = participant_actor = insert(:actor)
%Event{url: event_url} =
event = insert(:event, organizer_actor: organizer, join_options: :restricted)
organizer_participation =
%Participant{} = insert(:participant, event: event, actor: organizer, role: :creator)
{:ok, _join_activity, _participation} = ActivityPub.join(event, participant_actor)
join_data =
File.read!("test/fixtures/mobilizon-leave-activity.json")
|> Jason.decode!()
|> Map.put("actor", participant_url)
|> Map.put("object", event_url)
assert {:ok, activity, _} = Transmogrifier.handle_incoming(join_data)
assert activity.data["object"] == event_url
assert activity.data["actor"] == participant_url
# The only participant left is the organizer
assert Events.list_participants_for_event(event.uuid, 1, 10, true) |> Enum.map(& &1.id) == [
organizer_participation.id
]
end
test "it refuses Leave activities when actor is the only organizer" do
%Actor{url: organizer_url} = organizer = insert(:actor)
%Event{url: event_url} =
event = insert(:event, organizer_actor: organizer, join_options: :restricted)
%Participant{} = insert(:participant, event: event, actor: organizer, role: :creator)
join_data =
File.read!("test/fixtures/mobilizon-leave-activity.json")
|> Jason.decode!()
|> Map.put("actor", organizer_url)
|> Map.put("object", event_url)
assert :error = Transmogrifier.handle_incoming(join_data)
end
end
describe "prepare outgoing" do

View File

@ -116,7 +116,8 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert hd(json_response(res, 200)["errors"])["message"] =~ "Event id not found"
assert hd(json_response(res, 200)["errors"])["message"] ==
"Event with this ID 1042 doesn't exist"
end
test "actor_leave_event/3 should delete a participant from an event", %{
@ -290,13 +291,14 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
user: user,
actor: actor
} do
event = insert(:event)
participant = insert(:participant, %{actor: actor})
mutation = """
mutation {
leaveEvent(
actor_id: #{participant.actor.id},
event_id: 1042
event_id: #{event.id}
) {
actor {
id
@ -355,13 +357,13 @@ defmodule MobilizonWeb.Resolvers.ParticipantResolverTest do
|> get("/api", AbsintheHelpers.query_skeleton(query, "participants"))
assert json_response(res, 200)["data"]["participants"] == [
%{
"actor" => %{"preferredUsername" => context.actor.preferred_username},
"role" => "creator"
},
%{
"actor" => %{"preferredUsername" => participant2.actor.preferred_username},
"role" => "participant"
},
%{
"actor" => %{"preferredUsername" => context.actor.preferred_username},
"role" => "creator"
}
]
end

View File

@ -124,15 +124,20 @@ defmodule Mobilizon.Factory do
tags: build_list(3, :tag),
url: Routes.page_url(Endpoint, :event, uuid),
picture: insert(:picture),
uuid: uuid
uuid: uuid,
join_options: :free
}
end
def participant_factory do
uuid = Ecto.UUID.generate()
%Mobilizon.Events.Participant{
event: build(:event),
actor: build(:actor),
role: :creator
role: :creator,
url: "#{Endpoint.url()}/join/event/#{uuid}",
id: uuid
}
end