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) Repo.get!(Actor, id)
end end
def get_actor(id) do
Repo.get(Actor, id)
end
# Get actor by ID and preload organized events, followers and followings # Get actor by ID and preload organized events, followers and followings
@spec get_actor_with_everything(integer()) :: Ecto.Query.t() @spec get_actor_with_everything(integer()) :: Ecto.Query.t()
defp do_get_actor_with_everything(id) do defp do_get_actor_with_everything(id) do

View file

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

View file

@ -17,9 +17,10 @@ defmodule Mobilizon.Events.Participant do
alias Mobilizon.Events.{Participant, Event} alias Mobilizon.Events.{Participant, Event}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
@primary_key false @primary_key {:id, :binary_id, autogenerate: true}
schema "participants" do schema "participants" do
field(:role, Mobilizon.Events.ParticipantRoleEnum, default: :participant) field(:role, Mobilizon.Events.ParticipantRoleEnum, default: :participant)
field(:url, :string)
belongs_to(:event, Event, primary_key: true) belongs_to(:event, Event, primary_key: true)
belongs_to(:actor, Actor, primary_key: true) belongs_to(:actor, Actor, primary_key: true)
@ -29,7 +30,49 @@ defmodule Mobilizon.Events.Participant do
@doc false @doc false
def changeset(%Participant{} = participant, attrs) do def changeset(%Participant{} = participant, attrs) do
participant participant
|> Ecto.Changeset.cast(attrs, [:role, :event_id, :actor_id]) |> Ecto.Changeset.cast(attrs, [:url, :role, :event_id, :actor_id])
|> validate_required([: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
end end

View file

@ -2,15 +2,12 @@ defmodule MobilizonWeb.API.Events do
@moduledoc """ @moduledoc """
API for Events API for Events
""" """
alias Mobilizon.Addresses
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils alias MobilizonWeb.API.Utils
@visibility %{"PUBLIC" => :public, "PRIVATE" => :private}
@doc """ @doc """
Create an event Create an event
""" """
@ -51,15 +48,4 @@ defmodule MobilizonWeb.API.Events do
}) })
end end
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 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
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Media.Picture alias Mobilizon.Media.Picture
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias MobilizonWeb.Resolvers.Person alias MobilizonWeb.Resolvers.Person
@ -108,20 +107,18 @@ defmodule MobilizonWeb.Resolvers.Event do
} }
) do ) do
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id), 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), {:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
role <- Mobilizon.Events.get_default_participant_role(event), {:ok, _activity, participant} <- MobilizonWeb.API.Participations.join(event, actor),
{:ok, participant} <-
Mobilizon.Events.create_participant(%{
role: role,
event_id: event.id,
actor_id: actor.id
}),
participant <- participant <-
Map.put(participant, :event, event) Map.put(participant, :event, event)
|> Map.put(:actor, Person.proxify_pictures(actor)) do |> Map.put(:actor, Person.proxify_pictures(actor)) do
{:ok, participant} {:ok, participant}
else else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, false} -> {:is_owned, false} ->
{:error, "Actor id is not owned by authenticated user"} {:error, "Actor id is not owned by authenticated user"}
@ -149,15 +146,15 @@ defmodule MobilizonWeb.Resolvers.Event do
} }
} }
) do ) do
with {:is_owned, true, _} <- User.owns_actor(user, actor_id), with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
{:ok, %Participant{} = participant} <- {:has_event, {:ok, %Event{} = event}} <-
Mobilizon.Events.get_participant(event_id, actor_id), {:has_event, Mobilizon.Events.get_event_full(event_id)},
{:only_organizer, false} <- {:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do
{:only_organizer, check_that_participant_is_not_only_organizer(event_id, actor_id)},
{:ok, _} <-
Mobilizon.Events.delete_participant(participant) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}} {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
else else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, false} -> {:is_owned, false} ->
{:error, "Actor id is not owned by authenticated user"} {: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"} {:error, "You need to be logged-in to leave an event"}
end 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 """ @doc """
Create an event Create an event
""" """

View file

@ -11,18 +11,19 @@ defmodule Mobilizon.Service.ActivityPub do
""" """
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment} alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Service.ActivityPub.Transmogrifier alias Mobilizon.Service.ActivityPub.Transmogrifier
alias Mobilizon.Service.WebFinger alias Mobilizon.Service.WebFinger
alias Mobilizon.Activity alias Mobilizon.Activity
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Actors.Follower
alias Mobilizon.Service.Federator alias Mobilizon.Service.Federator
alias Mobilizon.Service.HTTPSignatures.Signature alias Mobilizon.Service.HTTPSignatures.Signature
alias Mobilizon.Service.ActivityPub.Convertible
require Logger require Logger
import Mobilizon.Service.ActivityPub.Utils import Mobilizon.Service.ActivityPub.Utils
import Mobilizon.Service.ActivityPub.Visibility import Mobilizon.Service.ActivityPub.Visibility
@ -148,7 +149,7 @@ defmodule Mobilizon.Service.ActivityPub do
end end
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 # only accept false as false value
local = !(params[:local] == false) local = !(params[:local] == false)
@ -157,7 +158,7 @@ defmodule Mobilizon.Service.ActivityPub do
"type" => "Accept", "type" => "Accept",
"actor" => actor, "actor" => actor,
"object" => object, "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, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
@ -165,11 +166,17 @@ defmodule Mobilizon.Service.ActivityPub do
end end
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 # only accept false as false value
local = !(params[:local] == false) 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, activity, object} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
@ -383,6 +390,65 @@ defmodule Mobilizon.Service.ActivityPub do
end end
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 """ @doc """
Create an actor locally by it's URL (AP ID) 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 def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}") 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) digest = Signature.build_digest(json)
date = Signature.generate_date_header() date = Signature.generate_date_header()

View file

@ -10,8 +10,6 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub.Converter alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
require Logger require Logger
@behaviour Converter @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
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment} alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Service.ActivityPub.Visibility alias Mobilizon.Service.ActivityPub.Visibility
@ -185,7 +185,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
def handle_incoming( def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do ) do
with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true), with {:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower), {:ok, %Actor{} = follower} <- Actors.get_or_fetch_by_url(follower),
@ -198,61 +198,65 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
end end
# TODO : Handle object being a Link
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Accept", "type" => "Accept",
"object" => follow_object, "object" => accepted_object,
"actor" => _actor, "actor" => _actor,
"id" => _id "id" => id
} = data } = data
) do ) do
with followed_actor_url <- get_actor(data), with actor_url <- get_actor(data),
{:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed_actor_url), {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor_url),
{:ok, %Follower{approved: false, actor: follower, id: follow_id} = follow} <- {:object_not_found, {:ok, activity, object}} <-
get_follow(follow_object), {:object_not_found,
{:ok, activity, _} <- do_handle_incoming_accept_following(accepted_object, actor) ||
ActivityPub.accept( do_handle_incoming_accept_join(accepted_object, actor)} do
%{ {:ok, activity, object}
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}
else else
{:ok, %Follower{approved: true} = _follow} -> {:object_not_found, nil} ->
{:error, "Follow already accepted"} Logger.warn(
"Unable to process Accept activity #{inspect(id)}. Object #{inspect(accepted_object)} wasn't found."
)
:error
e -> 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 :error
end end
end end
def handle_incoming( def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data %{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data
) do ) do
with followed_actor_url <- get_actor(data), with actor_url <- get_actor(data),
{:ok, %Actor{} = followed} <- Actors.get_or_fetch_by_url(followed_actor_url), {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor_url),
{:ok, %Follower{approved: false, actor: follower, id: follow_id} = follow} <- {:object_not_found, {:ok, activity, object}} <-
get_follow(follow_object), {:object_not_found,
{:ok, activity, object} <- do_handle_incoming_reject_following(rejected_object, actor) ||
ActivityPub.reject(%{ do_handle_incoming_reject_join(rejected_object, actor)} do
to: [follower.url],
type: "Reject",
actor: followed,
object: follow_object,
local: false
}),
{:ok, _follower} <- Actor.unfollow(followed, follower) do
{:ok, activity, object} {:ok, activity, object}
else else
{:object_not_found, nil} ->
Logger.warn(
"Unable to process Reject activity #{inspect(id)}. Object #{inspect(rejected_object)} wasn't found."
)
:error
e -> e ->
Logger.debug(inspect(e)) Logger.warn(
"Unable to process Reject activity #{inspect(id)} for object #{inspect(rejected_object)} only returned #{
inspect(e)
}"
)
:error :error
end end
end end
@ -272,7 +276,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# end # end
# # # #
def handle_incoming( def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
) do ) do
with actor <- get_actor(data), with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor), {:ok, %Actor{} = actor} <- Actors.get_or_fetch_by_url(actor),
@ -320,7 +324,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
"object" => object_id, "object" => object_id,
"id" => cancelled_activity_id "id" => cancelled_activity_id
}, },
"actor" => actor, "actor" => _actor,
"id" => id "id" => id
} = data } = data
) do ) do
@ -378,6 +382,43 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
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 # # TODO
# # Accept # # Accept
@ -406,13 +447,187 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
{:error, :not_supported} {:error, :not_supported}
end 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 defp get_follow(follow_object) do
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object), with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
{:not_found, %Follower{} = follow} <- {:not_found, %Follower{} = follow} <-
{:not_found, Actors.get_follow_by_url(follow_object_id)} do {:not_found, Actors.get_follow_by_url(follow_object_id)} do
{:ok, follow} {:ok, follow}
else else
{:not_found, err} -> {:not_found, _err} ->
{:error, "Follow URL not found"} {:error, "Follow URL not found"}
_ -> _ ->
@ -420,6 +635,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end end
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 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) do
with false <- String.starts_with?(in_reply_to, "http"), with false <- String.starts_with?(in_reply_to, "http"),
{:ok, replied_to_object} <- fetch_obj_helper(in_reply_to) do {: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_actor_url,
object_url, object_url,
activity_id, activity_id,
public \\ true public
) do ) do
{to, cc} = {to, cc} =
if public do if public do
@ -611,6 +611,24 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
|> Map.merge(additional) |> Map.merge(additional)
end 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 """ @doc """
Converts PEM encoded keys to a public key representation 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 Utility functions related to content visibility
""" """
alias Mobilizon.Activity alias Mobilizon.Activity
alias Mobilizon.Events.Event
@public "https://www.w3.org/ns/activitystreams#Public" @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
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Comment, Event} alias Mobilizon.Events.{Comment, Event, Participant}
alias Mobilizon.Service.ActivityPub alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Service.ActivityPub.Transmogrifier alias Mobilizon.Service.ActivityPub.Transmogrifier
@ -695,6 +695,120 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
assert activity.data["actor"] == reporter_url assert activity.data["actor"] == reporter_url
assert activity.data["cc"] == [reported_url] assert activity.data["cc"] == [reported_url]
end 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 end
describe "prepare outgoing" do describe "prepare outgoing" do

View file

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

View file

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