Federate participations

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-08-14 17:45:11 +02:00
parent b8daf87dd9
commit 641129dc74
18 changed files with 752 additions and 123 deletions

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"