Refactor Mobilizon.Federation.ActivityPub and add typespecs
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
169
lib/federation/activity_pub/actions/accept.ex
Normal file
169
lib/federation/activity_pub/actions/accept.ex
Normal file
@@ -0,0 +1,169 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Accept do
|
||||
@moduledoc """
|
||||
Accept things
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Events}
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Events.Participant
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Refresher}
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Service.Notifications.Scheduler
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
make_accept_join_data: 2,
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@type acceptable_types :: :join | :follow | :invite
|
||||
@type acceptable_entities ::
|
||||
accept_join_entities | accept_follow_entities | accept_invite_entities
|
||||
|
||||
@spec accept(acceptable_types, acceptable_entities, boolean, map) ::
|
||||
{:ok, ActivityStream.t(), acceptable_entities}
|
||||
def accept(type, entity, local \\ true, additional \\ %{}) do
|
||||
Logger.debug("We're accepting something")
|
||||
|
||||
accept_res =
|
||||
case type do
|
||||
:join -> accept_join(entity, additional)
|
||||
:follow -> accept_follow(entity, additional)
|
||||
:invite -> accept_invite(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, entity, update_data} <- accept_res do
|
||||
{:ok, activity} = create_activity(update_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, entity}
|
||||
end
|
||||
end
|
||||
|
||||
@type accept_follow_entities :: Follower.t()
|
||||
|
||||
@spec accept_follow(Follower.t(), map) ::
|
||||
{:ok, Follower.t(), Activity.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp accept_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}) do
|
||||
follower_as_data = Convertible.model_to_as(follower)
|
||||
|
||||
update_data =
|
||||
make_accept_join_data(
|
||||
follower_as_data,
|
||||
Map.merge(additional, %{
|
||||
"id" => "#{Endpoint.url()}/accept/follow/#{follower.id}",
|
||||
"to" => [follower.actor.url],
|
||||
"cc" => [],
|
||||
"actor" => follower.target_actor.url
|
||||
})
|
||||
)
|
||||
|
||||
{:ok, follower, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@type accept_join_entities :: Participant.t() | Member.t()
|
||||
|
||||
@spec accept_join(Participant.t() | Member.t(), map) ::
|
||||
{:ok, Participant.t() | Member.t(), Activity.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp accept_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :participant}) do
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
)
|
||||
|
||||
Scheduler.trigger_notifications_for_participant(participant)
|
||||
participant_as_data = Convertible.model_to_as(participant)
|
||||
audience = Audience.get_audience(participant)
|
||||
|
||||
accept_join_data =
|
||||
make_accept_join_data(
|
||||
participant_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{participant.id}"
|
||||
})
|
||||
)
|
||||
|
||||
{:ok, participant, accept_join_data}
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_join(%Member{} = member, additional) do
|
||||
with {:ok, %Member{} = member} <-
|
||||
Actors.update_member(member, %{role: :member}) do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_approved"
|
||||
)
|
||||
|
||||
maybe_refresh_group(member)
|
||||
|
||||
Absinthe.Subscription.publish(Endpoint, member.actor,
|
||||
group_membership_changed: [
|
||||
Actor.preferred_username_and_domain(member.parent),
|
||||
member.actor.id
|
||||
]
|
||||
)
|
||||
|
||||
member_as_data = Convertible.model_to_as(member)
|
||||
audience = Audience.get_audience(member)
|
||||
|
||||
accept_join_data =
|
||||
make_accept_join_data(
|
||||
member_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{member.id}"
|
||||
})
|
||||
)
|
||||
|
||||
{:ok, member, accept_join_data}
|
||||
end
|
||||
end
|
||||
|
||||
@type accept_invite_entities :: Member.t()
|
||||
|
||||
@spec accept_invite(Member.t(), map()) ::
|
||||
{:ok, Member.t(), Activity.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp accept_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
_additional
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor!(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor!(actor_id),
|
||||
{:ok, %Member{id: member_id} = member} <-
|
||||
Actors.update_member(member, %{role: :member}) do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_accepted_invitation"
|
||||
)
|
||||
|
||||
maybe_refresh_group(member)
|
||||
|
||||
accept_data = %{
|
||||
"type" => "Accept",
|
||||
"attributedTo" => member.parent.url,
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"actor" => actor_url,
|
||||
"object" => Convertible.model_to_as(member),
|
||||
"id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}"
|
||||
}
|
||||
|
||||
{:ok, member, accept_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_refresh_group(Member.t()) :: :ok | nil
|
||||
defp maybe_refresh_group(%Member{
|
||||
parent: %Actor{domain: parent_domain, url: parent_url},
|
||||
actor: %Actor{} = actor
|
||||
}) do
|
||||
unless is_nil(parent_domain),
|
||||
do: Refresher.fetch_group(parent_url, actor)
|
||||
end
|
||||
end
|
||||
60
lib/federation/activity_pub/actions/announce.ex
Normal file
60
lib/federation/activity_pub/actions/announce.ex
Normal file
@@ -0,0 +1,60 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Announce do
|
||||
@moduledoc """
|
||||
Announce things
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Share
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
make_announce_data: 3,
|
||||
make_announce_data: 4,
|
||||
make_unannounce_data: 3
|
||||
]
|
||||
|
||||
@doc """
|
||||
Announce (reshare) an activity to the world, using an activity of type `Announce`.
|
||||
"""
|
||||
@spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) ::
|
||||
{:ok, Activity.t(), ActivityStream.t()} | {:error, any()}
|
||||
def announce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
public \\ true
|
||||
) do
|
||||
with {:ok, %Actor{id: object_owner_actor_id}} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]),
|
||||
{:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id) do
|
||||
announce_data = make_announce_data(actor, object, activity_id, public)
|
||||
{:ok, activity} = create_activity(announce_data, local)
|
||||
:ok = maybe_federate(activity)
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cancel the announcement of an activity to the world, using an activity of type `Undo` an `Announce`.
|
||||
"""
|
||||
@spec unannounce(Actor.t(), ActivityStream.t(), String.t() | nil, String.t() | nil, boolean) ::
|
||||
{:ok, Activity.t(), ActivityStream.t()}
|
||||
def unannounce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
cancelled_activity_id \\ nil,
|
||||
local \\ true
|
||||
) do
|
||||
announce_activity = make_announce_data(actor, object, cancelled_activity_id)
|
||||
unannounce_data = make_unannounce_data(actor, announce_activity, activity_id)
|
||||
{:ok, unannounce_activity} = create_activity(unannounce_data, local)
|
||||
maybe_federate(unannounce_activity)
|
||||
{:ok, unannounce_activity, object}
|
||||
end
|
||||
end
|
||||
71
lib/federation/activity_pub/actions/create.ex
Normal file
71
lib/federation/activity_pub/actions/create.ex
Normal file
@@ -0,0 +1,71 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Create do
|
||||
@moduledoc """
|
||||
Create things
|
||||
"""
|
||||
alias Mobilizon.Tombstone
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Types}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@type create_entities ::
|
||||
:event | :comment | :discussion | :actor | :todo_list | :todo | :resource | :post
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Create`
|
||||
|
||||
* Creates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Create` activity
|
||||
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec create(create_entities(), map(), boolean, map()) ::
|
||||
{:ok, Activity.t(), Entity.t()}
|
||||
| {:error, :entity_tombstoned | atom() | Ecto.Changeset.t()}
|
||||
def create(type, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("creating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
case check_for_tombstones(args) do
|
||||
nil ->
|
||||
case do_create(type, args, additional) do
|
||||
{:ok, entity, create_data} ->
|
||||
{:ok, activity} = create_activity(create_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, entity}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
%Tombstone{} ->
|
||||
{:error, :entity_tombstoned}
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_create(create_entities(), map(), map()) ::
|
||||
{:ok, Entity.t(), Activity.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
defp do_create(type, args, additional) do
|
||||
case type do
|
||||
:event -> Types.Events.create(args, additional)
|
||||
:comment -> Types.Comments.create(args, additional)
|
||||
:discussion -> Types.Discussions.create(args, additional)
|
||||
:actor -> Types.Actors.create(args, additional)
|
||||
:todo_list -> Types.TodoLists.create(args, additional)
|
||||
:todo -> Types.Todos.create(args, additional)
|
||||
:resource -> Types.Resources.create(args, additional)
|
||||
:post -> Types.Posts.create(args, additional)
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_for_tombstones(map()) :: Tombstone.t() | nil
|
||||
defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url)
|
||||
defp check_for_tombstones(_), do: nil
|
||||
end
|
||||
33
lib/federation/activity_pub/actions/delete.ex
Normal file
33
lib/federation/activity_pub/actions/delete.ex
Normal file
@@ -0,0 +1,33 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Delete do
|
||||
@moduledoc """
|
||||
Delete things
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.Types.{Entity, Managable, Ownable}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 2,
|
||||
check_for_actor_key_rotation: 1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Delete an entity, using an activity of type `Delete`
|
||||
"""
|
||||
@spec delete(Entity.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Entity.t()}
|
||||
def delete(object, actor, local \\ true, additional \\ %{}) do
|
||||
with {:ok, activity_data, actor, object} <-
|
||||
Managable.delete(object, actor, local, additional),
|
||||
group <- Ownable.group_actor(object),
|
||||
:ok <- check_for_actor_key_rotation(actor),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity, group) do
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
end
|
||||
31
lib/federation/activity_pub/actions/flag.ex
Normal file
31
lib/federation/activity_pub/actions/flag.ex
Normal file
@@ -0,0 +1,31 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Flag do
|
||||
@moduledoc """
|
||||
Delete things
|
||||
"""
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Types}
|
||||
alias Mobilizon.Web.Email.{Admin, Mailer}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1
|
||||
]
|
||||
|
||||
@spec flag(map, boolean, map) :: {:ok, Activity.t(), Report.t()} | {:error, Ecto.Changeset.t()}
|
||||
def flag(args, local \\ false, additional \\ %{}) do
|
||||
with {:ok, report, report_as_data} <- Types.Reports.flag(args, local, additional) do
|
||||
{:ok, activity} = create_activity(report_as_data, local)
|
||||
maybe_federate(activity)
|
||||
|
||||
Enum.each(Users.list_moderators(), fn moderator ->
|
||||
moderator
|
||||
|> Admin.report(report)
|
||||
|> Mailer.send_email_later()
|
||||
end)
|
||||
|
||||
{:ok, activity, report}
|
||||
end
|
||||
end
|
||||
end
|
||||
74
lib/federation/activity_pub/actions/follow.ex
Normal file
74
lib/federation/activity_pub/actions/follow.ex
Normal file
@@ -0,0 +1,74 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Follow do
|
||||
@moduledoc """
|
||||
Follow people
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Federation.ActivityPub.Types
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
make_unfollow_data: 4
|
||||
]
|
||||
|
||||
@doc """
|
||||
Make an actor follow another, using an activity of type `Follow`
|
||||
"""
|
||||
@spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom | Ecto.Changeset.t() | String.t()}
|
||||
def follow(
|
||||
%Actor{} = follower,
|
||||
%Actor{} = followed,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
if followed.id != follower.id do
|
||||
case Types.Actors.follow(
|
||||
follower,
|
||||
followed,
|
||||
local,
|
||||
Map.merge(additional, %{"activity_id" => activity_id})
|
||||
) do
|
||||
{:ok, activity_data, %Follower{} = follower} ->
|
||||
{:ok, activity} = create_activity(activity_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, follower}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:error, "Can't follow yourself"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor unfollow another, using an activity of type `Undo` a `Follow`.
|
||||
"""
|
||||
@spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, String.t()}
|
||||
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower) do
|
||||
# We recreate the follow activity
|
||||
follow_as_data =
|
||||
Convertible.model_to_as(%{follow | actor: follower, target_actor: followed})
|
||||
|
||||
{:ok, follow_activity} = create_activity(follow_as_data, local)
|
||||
activity_unfollow_id = activity_id || "#{Endpoint.url()}/unfollow/#{follow_id}/activity"
|
||||
|
||||
unfollow_data =
|
||||
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id)
|
||||
|
||||
{:ok, activity} = create_activity(unfollow_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, follow}
|
||||
end
|
||||
end
|
||||
end
|
||||
86
lib/federation/activity_pub/actions/invite.ex
Normal file
86
lib/federation/activity_pub/actions/invite.ex
Normal file
@@ -0,0 +1,86 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Invite do
|
||||
@moduledoc """
|
||||
Invite people to things
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Web.Email.Group
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, map(), Member.t()} | {:error, :not_able_to_invite | Ecto.Changeset.t()}
|
||||
def invite(
|
||||
%Actor{url: group_url, id: group_id, members_url: members_url} = group,
|
||||
%Actor{url: actor_url, id: actor_id} = actor,
|
||||
%Actor{url: target_actor_url, id: target_actor_id} = _target_actor,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}")
|
||||
|
||||
if is_able_to_invite?(actor, group) do
|
||||
with {:ok, %Member{url: member_url} = member} <-
|
||||
Actors.create_member(%{
|
||||
parent_id: group_id,
|
||||
actor_id: target_actor_id,
|
||||
role: :invited,
|
||||
invited_by_id: actor_id,
|
||||
url: Map.get(additional, :url)
|
||||
}) do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
moderator: actor,
|
||||
subject: "member_invited"
|
||||
)
|
||||
|
||||
{:ok, activity} =
|
||||
create_activity(
|
||||
%{
|
||||
"type" => "Invite",
|
||||
"attributedTo" => group_url,
|
||||
"actor" => actor_url,
|
||||
"object" => group_url,
|
||||
"target" => target_actor_url,
|
||||
"id" => member_url
|
||||
}
|
||||
|> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]})
|
||||
|> Map.merge(additional),
|
||||
local
|
||||
)
|
||||
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
Group.send_invite_to_user(member)
|
||||
{:ok, activity, member}
|
||||
end
|
||||
else
|
||||
{:error, :not_able_to_invite}
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_able_to_invite?(Actor.t(), Actor.t()) :: boolean
|
||||
defp is_able_to_invite?(%Actor{domain: actor_domain, id: actor_id}, %Actor{
|
||||
domain: group_domain,
|
||||
id: group_id
|
||||
}) do
|
||||
# If the actor comes from the same domain we trust it
|
||||
if actor_domain == group_domain do
|
||||
true
|
||||
else
|
||||
# If local group, we'll send the invite
|
||||
case Actors.get_member(actor_id, group_id) do
|
||||
{:ok, %Member{} = admin_member} ->
|
||||
Member.is_administrator(admin_member)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
51
lib/federation/activity_pub/actions/join.ex
Normal file
51
lib/federation/activity_pub/actions/join.ex
Normal file
@@ -0,0 +1,51 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Join do
|
||||
@moduledoc """
|
||||
Join things
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub.Types
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Join an entity (an event or a group), using an activity of type `Join`
|
||||
"""
|
||||
@spec join(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Participant.t()} | {:error, :maximum_attendee_capacity}
|
||||
@spec join(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
|
||||
def join(entity_to_join, actor_joining, local \\ true, additional \\ %{})
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local, additional) do
|
||||
case Types.Events.join(event, actor, local, additional) do
|
||||
{:ok, activity_data, participant} ->
|
||||
{:ok, activity} = create_activity(activity_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, participant}
|
||||
|
||||
{:error, :maximum_attendee_capacity_reached} ->
|
||||
{:error, :maximum_attendee_capacity_reached}
|
||||
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
|
||||
def join(%Actor{type: :Group} = group, %Actor{} = actor, local, additional) do
|
||||
with {:ok, activity_data, %Member{} = member} <-
|
||||
Types.Actors.join(group, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
end
|
||||
104
lib/federation/activity_pub/actions/leave.ex
Normal file
104
lib/federation/activity_pub/actions/leave.ex
Normal file
@@ -0,0 +1,104 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Leave do
|
||||
@moduledoc """
|
||||
Leave things
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Events}
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@spec leave(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Participant.t()}
|
||||
| {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()}
|
||||
@spec leave(Actor.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Member.t()} | {:error, atom() | Ecto.Changeset.t()}
|
||||
def leave(object, actor, local \\ true, additional \\ %{})
|
||||
|
||||
@doc """
|
||||
Leave an event or a group
|
||||
"""
|
||||
def leave(
|
||||
%Event{id: event_id, url: event_url} = _event,
|
||||
%Actor{id: actor_id, url: actor_url} = _actor,
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
if Participant.is_not_only_organizer(event_id, actor_id) do
|
||||
{:error, :is_only_organizer}
|
||||
else
|
||||
case Mobilizon.Events.get_participant(
|
||||
event_id,
|
||||
actor_id,
|
||||
Map.get(additional, :metadata, %{})
|
||||
) do
|
||||
{:ok, %Participant{} = participant} ->
|
||||
case Events.delete_participant(participant) do
|
||||
{:ok, %{participant: %Participant{} = participant}} ->
|
||||
leave_data = %{
|
||||
"type" => "Leave",
|
||||
# If it's an exclusion it should be something else
|
||||
"actor" => actor_url,
|
||||
"object" => event_url,
|
||||
"id" => "#{Endpoint.url()}/leave/event/#{participant.id}"
|
||||
}
|
||||
|
||||
audience = Audience.get_audience(participant)
|
||||
{:ok, activity} = create_activity(Map.merge(leave_data, audience), local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, participant}
|
||||
|
||||
{:error, _type, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, :participant_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def leave(
|
||||
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
|
||||
%Actor{id: actor_id, url: actor_url},
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
with {:member, {:ok, %Member{id: member_id} = member}} <-
|
||||
{:member, Actors.get_member(actor_id, group_id)},
|
||||
{:is_not_only_admin, true} <-
|
||||
{:is_not_only_admin,
|
||||
Map.get(additional, :force_member_removal, false) ||
|
||||
!Actors.is_only_administrator?(member_id, group_id)},
|
||||
{:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)} do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit")
|
||||
|
||||
leave_data = %{
|
||||
"to" => [group_members_url],
|
||||
"cc" => [group_url],
|
||||
"attributedTo" => group_url,
|
||||
"type" => "Leave",
|
||||
"actor" => actor_url,
|
||||
"object" => group_url
|
||||
}
|
||||
|
||||
{:ok, activity} = create_activity(leave_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:member, nil} -> {:error, :member_not_found}
|
||||
{:is_not_only_admin, false} -> {:error, :is_not_only_admin}
|
||||
{:error, %Ecto.Changeset{} = err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
30
lib/federation/activity_pub/actions/move.ex
Normal file
30
lib/federation/activity_pub/actions/move.ex
Normal file
@@ -0,0 +1,30 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Move do
|
||||
@moduledoc """
|
||||
Move things
|
||||
"""
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Types}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1
|
||||
]
|
||||
|
||||
@spec move(:resource, Resource.t(), map, boolean, map) ::
|
||||
{:ok, Activity.t(), Resource.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
def move(type, old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("We're moving something")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:ok, entity, update_data} <-
|
||||
(case type do
|
||||
:resource -> Types.Resources.move(old_entity, args, additional)
|
||||
end) do
|
||||
{:ok, activity} = create_activity(update_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, entity}
|
||||
end
|
||||
end
|
||||
end
|
||||
127
lib/federation/activity_pub/actions/reject.ex
Normal file
127
lib/federation/activity_pub/actions/reject.ex
Normal file
@@ -0,0 +1,127 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Reject do
|
||||
@moduledoc """
|
||||
Reject things
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Events}
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Events.Participant
|
||||
alias Mobilizon.Federation.ActivityPub.Actions.Accept
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@spec reject(Accept.acceptable_types(), Accept.acceptable_entities(), boolean, map) ::
|
||||
{:ok, ActivityStream.t(), Accept.acceptable_entities()}
|
||||
def reject(type, entity, local \\ true, additional \\ %{}) do
|
||||
{:ok, entity, update_data} =
|
||||
case type do
|
||||
:join -> reject_join(entity, additional)
|
||||
:follow -> reject_follow(entity, additional)
|
||||
:invite -> reject_invite(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any()
|
||||
defp reject_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :rejected}),
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
),
|
||||
participant_as_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
participant
|
||||
|> Audience.get_audience()
|
||||
|> Map.merge(additional),
|
||||
reject_data <- %{
|
||||
"type" => "Reject",
|
||||
"object" => participant_as_data
|
||||
},
|
||||
update_data <-
|
||||
reject_data
|
||||
|> Map.merge(audience)
|
||||
|> Map.merge(%{
|
||||
"id" => "#{Endpoint.url()}/reject/join/#{participant.id}"
|
||||
}) do
|
||||
{:ok, participant, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_follow(Follower.t(), map()) :: {:ok, Follower.t(), Activity.t()} | any()
|
||||
defp reject_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower),
|
||||
follower_as_data <- Convertible.model_to_as(follower),
|
||||
audience <-
|
||||
follower.actor |> Audience.get_audience() |> Map.merge(additional),
|
||||
reject_data <- %{
|
||||
"to" => [follower.actor.url],
|
||||
"type" => "Reject",
|
||||
"actor" => follower.target_actor.url,
|
||||
"object" => follower_as_data
|
||||
},
|
||||
update_data <-
|
||||
audience
|
||||
|> Map.merge(reject_data)
|
||||
|> Map.merge(%{
|
||||
"id" => "#{Endpoint.url()}/reject/follow/#{follower.id}"
|
||||
}) do
|
||||
{:ok, follower, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp reject_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
_additional
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor(actor_id),
|
||||
{:ok, %Member{url: member_url, id: member_id} = member} <-
|
||||
Actors.delete_member(member),
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_rejected_invitation"
|
||||
),
|
||||
accept_data <- %{
|
||||
"type" => "Reject",
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => member.parent.url,
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"object" => member_url,
|
||||
"id" => "#{Endpoint.url()}/reject/invite/member/#{member_id}"
|
||||
} do
|
||||
{:ok, member, accept_data}
|
||||
end
|
||||
end
|
||||
end
|
||||
56
lib/federation/activity_pub/actions/remove.ex
Normal file
56
lib/federation/activity_pub/actions/remove.ex
Normal file
@@ -0,0 +1,56 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Remove do
|
||||
@moduledoc """
|
||||
Remove things
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Web.Email.Group
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Remove an activity, using an activity of type `Remove`
|
||||
"""
|
||||
@spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Member.t()} | {:error, :member_not_found | Ecto.Changeset.t()}
|
||||
def remove(
|
||||
%Member{} = member,
|
||||
%Actor{type: :Group, url: group_url, members_url: group_members_url},
|
||||
%Actor{url: moderator_url} = moderator,
|
||||
local,
|
||||
_additional \\ %{}
|
||||
) do
|
||||
with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}),
|
||||
%Member{} = member <- Actors.get_member(member_id) do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
moderator: moderator,
|
||||
subject: "member_removed"
|
||||
)
|
||||
|
||||
Group.send_notification_to_removed_member(member)
|
||||
|
||||
remove_data = %{
|
||||
"to" => [group_members_url],
|
||||
"type" => "Remove",
|
||||
"actor" => moderator_url,
|
||||
"object" => member.url,
|
||||
"origin" => group_url
|
||||
}
|
||||
|
||||
{:ok, activity} = create_activity(remove_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, member}
|
||||
else
|
||||
nil -> {:error, :member_not_found}
|
||||
{:error, %Ecto.Changeset{} = err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
44
lib/federation/activity_pub/actions/update.ex
Normal file
44
lib/federation/activity_pub/actions/update.ex
Normal file
@@ -0,0 +1,44 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Actions.Update do
|
||||
@moduledoc """
|
||||
Update things
|
||||
"""
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Managable
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Update`
|
||||
|
||||
* Updates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Update` activity
|
||||
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec update(Entity.t(), map(), boolean, map()) ::
|
||||
{:ok, Activity.t(), Entity.t()} | {:error, atom() | Ecto.Changeset.t()}
|
||||
def update(old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("updating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
case Managable.update(old_entity, args, additional) do
|
||||
{:ok, entity, update_data} ->
|
||||
{:ok, activity} = create_activity(update_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, entity}
|
||||
|
||||
{:error, err} ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,64 +12,34 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
|
||||
alias Mobilizon.{
|
||||
Actors,
|
||||
Config,
|
||||
Discussions,
|
||||
Events,
|
||||
Posts,
|
||||
Resources,
|
||||
Share,
|
||||
Users
|
||||
Resources
|
||||
}
|
||||
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Tombstone
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.{
|
||||
Activity,
|
||||
Audience,
|
||||
Federator,
|
||||
Fetcher,
|
||||
Preloader,
|
||||
Refresher,
|
||||
Relay,
|
||||
Transmogrifier,
|
||||
Types,
|
||||
Visibility
|
||||
Relay
|
||||
}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Types.{Entity, Managable, Ownable}
|
||||
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Federation.HTTPSignatures.Signature
|
||||
|
||||
alias Mobilizon.Service.Notifications.Scheduler
|
||||
alias Mobilizon.Storage.Page
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Email.{Admin, Group, Mailer}
|
||||
|
||||
require Logger
|
||||
|
||||
@public_ap_adress "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
# Wraps an object into an activity
|
||||
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
|
||||
defp create_activity(map, local) when is_map(map) do
|
||||
with map <- lazy_put_activity_defaults(map) do
|
||||
{:ok,
|
||||
%Activity{
|
||||
data: map,
|
||||
local: local,
|
||||
actor: map["actor"],
|
||||
recipients: get_recipients(map)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetch an object from an URL, from our local database of events and comments, then eventually remote
|
||||
"""
|
||||
@@ -79,33 +49,32 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
def fetch_object_from_url(url, options \\ []) do
|
||||
Logger.info("Fetching object from url #{url}")
|
||||
|
||||
with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")},
|
||||
{:existing, nil} <-
|
||||
{:existing, Tombstone.find_tombstone(url)},
|
||||
{:existing, nil} <- {:existing, Events.get_event_by_url(url)},
|
||||
{:existing, nil} <-
|
||||
{:existing, Discussions.get_discussion_by_url(url)},
|
||||
{:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)},
|
||||
{:existing, nil} <- {:existing, Resources.get_resource_by_url(url)},
|
||||
{:existing, nil} <- {:existing, Posts.get_post_by_url(url)},
|
||||
{:existing, nil} <-
|
||||
{:existing, Actors.get_actor_by_url_2(url)},
|
||||
{:existing, nil} <- {:existing, Actors.get_member_by_url(url)},
|
||||
:ok <- Logger.info("Data for URL not found anywhere, going to fetch it"),
|
||||
{:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do
|
||||
Logger.debug("Going to preload the new entity")
|
||||
Preloader.maybe_preload(entity)
|
||||
if String.starts_with?(url, "http") do
|
||||
with {:existing, nil} <-
|
||||
{:existing, Tombstone.find_tombstone(url)},
|
||||
{:existing, nil} <- {:existing, Events.get_event_by_url(url)},
|
||||
{:existing, nil} <-
|
||||
{:existing, Discussions.get_discussion_by_url(url)},
|
||||
{:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)},
|
||||
{:existing, nil} <- {:existing, Resources.get_resource_by_url(url)},
|
||||
{:existing, nil} <- {:existing, Posts.get_post_by_url(url)},
|
||||
{:existing, nil} <-
|
||||
{:existing, Actors.get_actor_by_url_2(url)},
|
||||
{:existing, nil} <- {:existing, Actors.get_member_by_url(url)},
|
||||
:ok <- Logger.info("Data for URL not found anywhere, going to fetch it"),
|
||||
{:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do
|
||||
Logger.debug("Going to preload the new entity")
|
||||
Preloader.maybe_preload(entity)
|
||||
else
|
||||
{:existing, entity} ->
|
||||
handle_existing_entity(url, entity, options)
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
else
|
||||
{:existing, entity} ->
|
||||
handle_existing_entity(url, entity, options)
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
|
||||
{:error, e}
|
||||
{:error, :url_not_http}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -132,7 +101,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
|
||||
@spec refresh_entity(String.t(), struct(), Keyword.t()) ::
|
||||
{:ok, struct()} | {:error, atom(), struct()} | {:error, String.t()}
|
||||
{:ok, struct()} | {:error, atom(), struct()} | {:error, atom()}
|
||||
defp refresh_entity(url, entity, options) do
|
||||
force_fetch = Keyword.get(options, :force, false)
|
||||
|
||||
@@ -149,609 +118,14 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
{:error, :http_not_found} ->
|
||||
{:error, :http_not_found, entity}
|
||||
|
||||
{:error, "Object origin check failed"} ->
|
||||
{:error, "Object origin check failed"}
|
||||
end
|
||||
else
|
||||
{:ok, entity}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Create`
|
||||
|
||||
* Creates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Create` activity
|
||||
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), Entity.entities()} | any()
|
||||
def create(type, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("creating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)},
|
||||
{:ok, entity, create_data} <-
|
||||
(case type do
|
||||
:event -> Types.Events.create(args, additional)
|
||||
:comment -> Types.Comments.create(args, additional)
|
||||
:discussion -> Types.Discussions.create(args, additional)
|
||||
:actor -> Types.Actors.create(args, additional)
|
||||
:todo_list -> Types.TodoLists.create(args, additional)
|
||||
:todo -> Types.Todos.create(args, additional)
|
||||
:resource -> Types.Resources.create(args, additional)
|
||||
:post -> Types.Posts.create(args, additional)
|
||||
end),
|
||||
{:ok, activity} <- create_activity(create_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Update`
|
||||
|
||||
* Updates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Update` activity
|
||||
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec update(Entity.entities(), map(), boolean, map()) ::
|
||||
{:ok, Activity.t(), Entity.entities()} | {:error, any()}
|
||||
def update(old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("updating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
case Managable.update(old_entity, args, additional) do
|
||||
{:ok, entity, update_data} ->
|
||||
{:ok, activity} = create_activity(update_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, entity}
|
||||
|
||||
{:error, err} ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@type acceptable_types :: :join | :follow | :invite
|
||||
@type acceptable_entities ::
|
||||
accept_join_entities | accept_follow_entities | accept_invite_entities
|
||||
|
||||
@spec accept(acceptable_types, acceptable_entities, boolean, map) ::
|
||||
{:ok, ActivityStream.t(), acceptable_entities}
|
||||
def accept(type, entity, local \\ true, additional \\ %{}) do
|
||||
Logger.debug("We're accepting something")
|
||||
|
||||
{:ok, entity, update_data} =
|
||||
case type do
|
||||
:join -> accept_join(entity, additional)
|
||||
:follow -> accept_follow(entity, additional)
|
||||
:invite -> accept_invite(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject(acceptable_types, acceptable_entities, boolean, map) ::
|
||||
{:ok, ActivityStream.t(), acceptable_entities}
|
||||
def reject(type, entity, local \\ true, additional \\ %{}) do
|
||||
{:ok, entity, update_data} =
|
||||
case type do
|
||||
:join -> reject_join(entity, additional)
|
||||
:follow -> reject_follow(entity, additional)
|
||||
:invite -> reject_invite(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) ::
|
||||
{:ok, Activity.t(), ActivityStream.t()} | {:error, any()}
|
||||
def announce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
public \\ true
|
||||
) do
|
||||
with {:ok, %Actor{id: object_owner_actor_id}} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]),
|
||||
{:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id),
|
||||
announce_data <- make_announce_data(actor, object, activity_id, public),
|
||||
{:ok, activity} <- create_activity(announce_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec unannounce(Actor.t(), ActivityStream.t(), String.t() | nil, String.t() | nil, boolean) ::
|
||||
{:ok, Activity.t(), ActivityStream.t()}
|
||||
def unannounce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
cancelled_activity_id \\ nil,
|
||||
local \\ true
|
||||
) do
|
||||
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
|
||||
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
|
||||
{:ok, unannounce_activity} <- create_activity(unannounce_data, local),
|
||||
:ok <- maybe_federate(unannounce_activity) do
|
||||
{:ok, unannounce_activity, object}
|
||||
else
|
||||
_e -> {:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor follow another
|
||||
"""
|
||||
@spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom | Ecto.Changeset.t() | String.t()}
|
||||
def follow(
|
||||
%Actor{} = follower,
|
||||
%Actor{} = followed,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
if followed.id != follower.id do
|
||||
case Types.Actors.follow(
|
||||
follower,
|
||||
followed,
|
||||
local,
|
||||
Map.merge(additional, %{"activity_id" => activity_id})
|
||||
) do
|
||||
{:ok, activity_data, %Follower{} = follower} ->
|
||||
{:ok, activity} = create_activity(activity_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, follower}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:error, "Can't follow yourself"}
|
||||
{:ok, entity}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor unfollow another
|
||||
"""
|
||||
@spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, String.t()}
|
||||
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower),
|
||||
# We recreate the follow activity
|
||||
follow_as_data <-
|
||||
Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}),
|
||||
{:ok, follow_activity} <- create_activity(follow_as_data, local),
|
||||
activity_unfollow_id <-
|
||||
activity_id || "#{Endpoint.url()}/unfollow/#{follow_id}/activity",
|
||||
unfollow_data <-
|
||||
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
|
||||
{:ok, activity} <- create_activity(unfollow_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
err ->
|
||||
Logger.debug("Error while unfollowing an actor #{inspect(err)}")
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete(Entity.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Entity.t()}
|
||||
def delete(object, actor, local \\ true, additional \\ %{}) do
|
||||
with {:ok, activity_data, actor, object} <-
|
||||
Managable.delete(object, actor, local, additional),
|
||||
group <- Ownable.group_actor(object),
|
||||
:ok <- check_for_actor_key_rotation(actor),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity, group) do
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
@spec join(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Participant.t()} | {:error, :maximum_attendee_capacity}
|
||||
@spec join(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
|
||||
def join(entity_to_join, actor_joining, local \\ true, additional \\ %{})
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local, additional) do
|
||||
case Types.Events.join(event, actor, local, additional) do
|
||||
{:ok, activity_data, participant} ->
|
||||
{:ok, activity} = create_activity(activity_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, participant}
|
||||
|
||||
{:error, :maximum_attendee_capacity_reached} ->
|
||||
{:error, :maximum_attendee_capacity_reached}
|
||||
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
|
||||
def join(%Actor{type: :Group} = group, %Actor{} = actor, local, additional) do
|
||||
with {:ok, activity_data, %Member{} = member} <-
|
||||
Types.Actors.join(group, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
|
||||
@spec leave(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Participant.t()}
|
||||
| {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()}
|
||||
@spec leave(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
|
||||
def leave(object, actor, local \\ true, additional \\ %{})
|
||||
|
||||
@doc """
|
||||
Leave an event or a group
|
||||
"""
|
||||
def leave(
|
||||
%Event{id: event_id, url: event_url} = _event,
|
||||
%Actor{id: actor_id, url: actor_url} = _actor,
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
if Participant.is_not_only_organizer(event_id, actor_id) do
|
||||
{:error, :is_only_organizer}
|
||||
else
|
||||
case Mobilizon.Events.get_participant(
|
||||
event_id,
|
||||
actor_id,
|
||||
Map.get(additional, :metadata, %{})
|
||||
) do
|
||||
{:ok, %Participant{} = participant} ->
|
||||
case Events.delete_participant(participant) do
|
||||
{:ok, %{participant: %Participant{} = participant}} ->
|
||||
leave_data = %{
|
||||
"type" => "Leave",
|
||||
# If it's an exclusion it should be something else
|
||||
"actor" => actor_url,
|
||||
"object" => event_url,
|
||||
"id" => "#{Endpoint.url()}/leave/event/#{participant.id}"
|
||||
}
|
||||
|
||||
audience = Audience.get_audience(participant)
|
||||
{:ok, activity} = create_activity(Map.merge(leave_data, audience), local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, participant}
|
||||
|
||||
{:error, _type, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, :participant_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def leave(
|
||||
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
|
||||
%Actor{id: actor_id, url: actor_url},
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
with {:member, {:ok, %Member{id: member_id} = member}} <-
|
||||
{:member, Actors.get_member(actor_id, group_id)},
|
||||
{:is_not_only_admin, true} <-
|
||||
{:is_not_only_admin,
|
||||
Map.get(additional, :force_member_removal, false) ||
|
||||
!Actors.is_only_administrator?(member_id, group_id)},
|
||||
{:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)},
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit"),
|
||||
leave_data <- %{
|
||||
"to" => [group_members_url],
|
||||
"cc" => [group_url],
|
||||
"attributedTo" => group_url,
|
||||
"type" => "Leave",
|
||||
"actor" => actor_url,
|
||||
"object" => group_url
|
||||
},
|
||||
{:ok, activity} <- create_activity(leave_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
||||
@spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
|
||||
def remove(
|
||||
%Member{} = member,
|
||||
%Actor{type: :Group, url: group_url, members_url: group_members_url},
|
||||
%Actor{url: moderator_url} = moderator,
|
||||
local,
|
||||
_additional \\ %{}
|
||||
) do
|
||||
with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}),
|
||||
%Member{} = member <- Actors.get_member(member_id),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
moderator: moderator,
|
||||
subject: "member_removed"
|
||||
),
|
||||
:ok <- Group.send_notification_to_removed_member(member),
|
||||
remove_data <- %{
|
||||
"to" => [group_members_url],
|
||||
"type" => "Remove",
|
||||
"actor" => moderator_url,
|
||||
"object" => member.url,
|
||||
"origin" => group_url
|
||||
},
|
||||
{:ok, activity} <- create_activity(remove_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
||||
@spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, map(), Member.t()} | {:error, :member_not_found}
|
||||
def invite(
|
||||
%Actor{url: group_url, id: group_id, members_url: members_url} = group,
|
||||
%Actor{url: actor_url, id: actor_id} = actor,
|
||||
%Actor{url: target_actor_url, id: target_actor_id} = _target_actor,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}")
|
||||
|
||||
with {:is_able_to_invite, true} <- {:is_able_to_invite, is_able_to_invite?(actor, group)},
|
||||
{:ok, %Member{url: member_url} = member} <-
|
||||
Actors.create_member(%{
|
||||
parent_id: group_id,
|
||||
actor_id: target_actor_id,
|
||||
role: :invited,
|
||||
invited_by_id: actor_id,
|
||||
url: Map.get(additional, :url)
|
||||
}),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
moderator: actor,
|
||||
subject: "member_invited"
|
||||
),
|
||||
invite_data <- %{
|
||||
"type" => "Invite",
|
||||
"attributedTo" => group_url,
|
||||
"actor" => actor_url,
|
||||
"object" => group_url,
|
||||
"target" => target_actor_url,
|
||||
"id" => member_url
|
||||
},
|
||||
{:ok, activity} <-
|
||||
create_activity(
|
||||
invite_data
|
||||
|> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]})
|
||||
|> Map.merge(additional),
|
||||
local
|
||||
),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity),
|
||||
:ok <- Group.send_invite_to_user(member) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_able_to_invite?(Actor.t(), Actor.t()) :: boolean
|
||||
defp is_able_to_invite?(%Actor{domain: actor_domain, id: actor_id}, %Actor{
|
||||
domain: group_domain,
|
||||
id: group_id
|
||||
}) do
|
||||
# If the actor comes from the same domain we trust it
|
||||
if actor_domain == group_domain do
|
||||
true
|
||||
else
|
||||
# If local group, we'll send the invite
|
||||
case Actors.get_member(actor_id, group_id) do
|
||||
{:ok, %Member{} = admin_member} ->
|
||||
Member.is_administrator(admin_member)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec move(:resource, Resource.t(), map, boolean, map) :: {:ok, Activity.t(), Resource.t()}
|
||||
def move(type, old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("We're moving something")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:ok, entity, update_data} <-
|
||||
(case type do
|
||||
:resource -> Types.Resources.move(old_entity, args, additional)
|
||||
end),
|
||||
{:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating a Move activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec flag(map, boolean, map) :: {:ok, Activity.t(), Report.t()}
|
||||
def flag(args, local \\ false, additional \\ %{}) do
|
||||
with {report, report_as_data} <- Types.Reports.flag(args, local, additional),
|
||||
{:ok, activity} <- create_activity(report_as_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
Enum.each(Users.list_moderators(), fn moderator ->
|
||||
moderator
|
||||
|> Admin.report(report)
|
||||
|> Mailer.send_email_later()
|
||||
end)
|
||||
|
||||
{:ok, activity, report}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_create_activity?(Activity.t()) :: boolean
|
||||
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
|
||||
defp is_create_activity?(_), do: false
|
||||
|
||||
@spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
|
||||
defp convert_members_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc ->
|
||||
case Actors.get_group_by_members_url(recipient) do
|
||||
# If the group is local just add external members
|
||||
%Actor{domain: domain} = group when is_nil(domain) ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group)}
|
||||
|
||||
# If it's remote add the remote group actor as well
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]}
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())}
|
||||
defp convert_followers_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
|
||||
case Actors.get_actor_by_followers_url(recipient) do
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.followers_url end),
|
||||
follower_actors ++ Actors.list_external_followers_for_actor(group)}
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# @spec is_announce_activity?(Activity.t()) :: boolean
|
||||
# defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
|
||||
# defp is_announce_activity?(_), do: false
|
||||
|
||||
@doc """
|
||||
Publish an activity to all appropriated audiences inboxes
|
||||
"""
|
||||
# credo:disable-for-lines:47
|
||||
@spec publish(Actor.t(), Activity.t()) :: :ok
|
||||
def publish(actor, %Activity{recipients: recipients} = activity) do
|
||||
Logger.debug("Publishing an activity")
|
||||
Logger.debug(inspect(activity, pretty: true))
|
||||
|
||||
public = Visibility.is_public?(activity)
|
||||
Logger.debug("is public ? #{public}")
|
||||
|
||||
if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do
|
||||
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
|
||||
|
||||
Relay.publish(activity)
|
||||
end
|
||||
|
||||
recipients = Enum.uniq(recipients)
|
||||
|
||||
{recipients, followers} = convert_followers_in_recipients(recipients)
|
||||
|
||||
{recipients, members} = convert_members_in_recipients(recipients)
|
||||
|
||||
remote_inboxes =
|
||||
(remote_actors(recipients) ++ followers ++ members)
|
||||
|> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
Logger.debug(fn -> "Remote inboxes are : #{inspect(remote_inboxes)}" end)
|
||||
|
||||
Enum.each(remote_inboxes, fn inbox ->
|
||||
Federator.enqueue(:publish_single_ap, %{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor: actor,
|
||||
id: activity.data["id"]
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Publish an activity to a specific inbox
|
||||
"""
|
||||
@spec publish_one(%{inbox: String.t(), json: String.t(), actor: Actor.t(), id: String.t()}) ::
|
||||
Tesla.Env.result()
|
||||
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)
|
||||
|
||||
digest = Signature.build_digest(json)
|
||||
date = Signature.generate_date_header()
|
||||
|
||||
# request_target = Signature.generate_request_target("POST", path)
|
||||
|
||||
signature =
|
||||
Signature.sign(actor, %{
|
||||
"(request-target)": "post #{path}",
|
||||
host: host,
|
||||
"content-length": byte_size(json),
|
||||
digest: digest,
|
||||
date: date
|
||||
})
|
||||
|
||||
Tesla.post(
|
||||
inbox,
|
||||
json,
|
||||
headers: [
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"signature", signature},
|
||||
{"digest", digest},
|
||||
{"date", date}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return all public activities (events & comments) for an actor
|
||||
"""
|
||||
@@ -802,224 +176,4 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
local: local
|
||||
}
|
||||
end
|
||||
|
||||
# Get recipients for an activity or object
|
||||
@spec get_recipients(map()) :: list()
|
||||
defp get_recipients(data) do
|
||||
Map.get(data, "to", []) ++ Map.get(data, "cc", [])
|
||||
end
|
||||
|
||||
@spec check_for_tombstones(map()) :: Tombstone.t() | nil
|
||||
defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url)
|
||||
defp check_for_tombstones(_), do: nil
|
||||
|
||||
@typep accept_follow_entities :: Follower.t()
|
||||
|
||||
@spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any
|
||||
defp accept_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}),
|
||||
follower_as_data <- Convertible.model_to_as(follower),
|
||||
update_data <-
|
||||
make_accept_join_data(
|
||||
follower_as_data,
|
||||
Map.merge(additional, %{
|
||||
"id" => "#{Endpoint.url()}/accept/follow/#{follower.id}",
|
||||
"to" => [follower.actor.url],
|
||||
"cc" => [],
|
||||
"actor" => follower.target_actor.url
|
||||
})
|
||||
) do
|
||||
{:ok, follower, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@typep accept_join_entities :: Participant.t() | Member.t()
|
||||
|
||||
@spec accept_join(Participant.t(), map) :: {:ok, Participant.t(), Activity.t()}
|
||||
@spec accept_join(Member.t(), map) :: {:ok, Member.t(), Activity.t()}
|
||||
defp accept_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :participant}),
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
),
|
||||
{:ok, _} <-
|
||||
Scheduler.trigger_notifications_for_participant(participant),
|
||||
participant_as_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
Audience.get_audience(participant),
|
||||
accept_join_data <-
|
||||
make_accept_join_data(
|
||||
participant_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{participant.id}"
|
||||
})
|
||||
) do
|
||||
{:ok, participant, accept_join_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_join(%Member{} = member, additional) do
|
||||
with {:ok, %Member{} = member} <-
|
||||
Actors.update_member(member, %{role: :member}),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_approved"
|
||||
),
|
||||
_ <- maybe_refresh_group(member),
|
||||
Absinthe.Subscription.publish(Endpoint, member.actor,
|
||||
group_membership_changed: [
|
||||
Actor.preferred_username_and_domain(member.parent),
|
||||
member.actor.id
|
||||
]
|
||||
),
|
||||
member_as_data <- Convertible.model_to_as(member),
|
||||
audience <-
|
||||
Audience.get_audience(member),
|
||||
accept_join_data <-
|
||||
make_accept_join_data(
|
||||
member_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{member.id}"
|
||||
})
|
||||
) do
|
||||
{:ok, member, accept_join_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@typep accept_invite_entities :: Member.t()
|
||||
|
||||
@spec accept_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp accept_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
_additional
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor(actor_id),
|
||||
{:ok, %Member{id: member_id} = member} <-
|
||||
Actors.update_member(member, %{role: :member}),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_accepted_invitation"
|
||||
),
|
||||
_ <- maybe_refresh_group(member),
|
||||
accept_data <- %{
|
||||
"type" => "Accept",
|
||||
"attributedTo" => member.parent.url,
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"actor" => actor_url,
|
||||
"object" => Convertible.model_to_as(member),
|
||||
"id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}"
|
||||
} do
|
||||
{:ok, member, accept_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_refresh_group(Member.t()) :: :ok | nil
|
||||
defp maybe_refresh_group(%Member{
|
||||
parent: %Actor{domain: parent_domain, url: parent_url},
|
||||
actor: %Actor{} = actor
|
||||
}) do
|
||||
unless is_nil(parent_domain),
|
||||
do: Refresher.fetch_group(parent_url, actor)
|
||||
end
|
||||
|
||||
@spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any()
|
||||
defp reject_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :rejected}),
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
),
|
||||
participant_as_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
participant
|
||||
|> Audience.get_audience()
|
||||
|> Map.merge(additional),
|
||||
reject_data <- %{
|
||||
"type" => "Reject",
|
||||
"object" => participant_as_data
|
||||
},
|
||||
update_data <-
|
||||
reject_data
|
||||
|> Map.merge(audience)
|
||||
|> Map.merge(%{
|
||||
"id" => "#{Endpoint.url()}/reject/join/#{participant.id}"
|
||||
}) do
|
||||
{:ok, participant, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_follow(Follower.t(), map()) :: {:ok, Follower.t(), Activity.t()} | any()
|
||||
defp reject_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower),
|
||||
follower_as_data <- Convertible.model_to_as(follower),
|
||||
audience <-
|
||||
follower.actor |> Audience.get_audience() |> Map.merge(additional),
|
||||
reject_data <- %{
|
||||
"to" => [follower.actor.url],
|
||||
"type" => "Reject",
|
||||
"actor" => follower.target_actor.url,
|
||||
"object" => follower_as_data
|
||||
},
|
||||
update_data <-
|
||||
audience
|
||||
|> Map.merge(reject_data)
|
||||
|> Map.merge(%{
|
||||
"id" => "#{Endpoint.url()}/reject/follow/#{follower.id}"
|
||||
}) do
|
||||
{:ok, follower, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp reject_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
_additional
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor(actor_id),
|
||||
{:ok, %Member{url: member_url, id: member_id} = member} <-
|
||||
Actors.delete_member(member),
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_rejected_invitation"
|
||||
),
|
||||
accept_data <- %{
|
||||
"type" => "Reject",
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => member.parent.url,
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"object" => member_url,
|
||||
"id" => "#{Endpoint.url()}/reject/invite/member/#{member_id}"
|
||||
} do
|
||||
{:ok, member, accept_data}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
@@ -154,22 +155,20 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
{mentions, []}
|
||||
end
|
||||
|
||||
@spec maybe_add_group_members(List.t(), Actor.t()) :: List.t()
|
||||
@spec maybe_add_group_members(list(String.t()), Actor.t()) :: list(String.t())
|
||||
defp maybe_add_group_members(collection, %Actor{type: :Group, members_url: members_url}) do
|
||||
[members_url | collection]
|
||||
end
|
||||
|
||||
defp maybe_add_group_members(collection, %Actor{type: _}), do: collection
|
||||
|
||||
@spec maybe_add_followers(List.t(), Actor.t()) :: List.t()
|
||||
@spec maybe_add_followers(list(String.t()), Actor.t()) :: list(String.t())
|
||||
defp maybe_add_followers(collection, %Actor{type: :Group, followers_url: followers_url}) do
|
||||
[followers_url | collection]
|
||||
end
|
||||
|
||||
defp maybe_add_followers(collection, %Actor{type: _}), do: collection
|
||||
|
||||
def get_addressed_actors(mentioned_users, _), do: mentioned_users
|
||||
|
||||
defp add_in_reply_to(%Comment{actor: %Actor{url: url}} = _comment), do: [url]
|
||||
defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url]
|
||||
defp add_in_reply_to(_), do: []
|
||||
@@ -237,29 +236,27 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
|
||||
@spec extract_actors_from_mentions(list(), Actor.t(), atom()) :: {list(), list()}
|
||||
defp extract_actors_from_mentions(mentions, actor, visibility) do
|
||||
with mentioned_actors <- Enum.map(mentions, &process_mention/1),
|
||||
addressed_actors <- get_addressed_actors(mentioned_actors, nil) do
|
||||
get_to_and_cc(actor, addressed_actors, visibility)
|
||||
end
|
||||
get_to_and_cc(actor, Enum.map(mentions, &process_mention/1), visibility)
|
||||
end
|
||||
|
||||
@spec extract_actors_from_event(Event.t()) :: %{
|
||||
String.t() => list(String.t())
|
||||
}
|
||||
defp extract_actors_from_event(%Event{} = event) do
|
||||
with {to, cc} <-
|
||||
extract_actors_from_mentions(
|
||||
event.mentions,
|
||||
group_or_organizer_event(event),
|
||||
event.visibility
|
||||
),
|
||||
{to, cc} <-
|
||||
{to,
|
||||
Enum.uniq(
|
||||
cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url)
|
||||
)} do
|
||||
%{"to" => to, "cc" => cc}
|
||||
else
|
||||
_ ->
|
||||
%{"to" => [], "cc" => []}
|
||||
end
|
||||
{to, cc} =
|
||||
extract_actors_from_mentions(
|
||||
event.mentions,
|
||||
group_or_organizer_event(event),
|
||||
event.visibility
|
||||
)
|
||||
|
||||
{to, cc} =
|
||||
{to,
|
||||
Enum.uniq(
|
||||
cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url)
|
||||
)}
|
||||
|
||||
%{"to" => to, "cc" => cc}
|
||||
end
|
||||
|
||||
@spec group_or_organizer_event(Event.t()) :: Actor.t()
|
||||
|
||||
@@ -19,10 +19,12 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
|
||||
@max_jobs 20
|
||||
|
||||
@spec init(any()) :: {:ok, any()}
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
@spec start_link(any) :: GenServer.on_start()
|
||||
def start_link(_) do
|
||||
spawn(fn ->
|
||||
# 1 minute
|
||||
@@ -39,6 +41,8 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
)
|
||||
end
|
||||
|
||||
@spec handle(:publish | :publish_single_ap | atom(), Activity.t() | map()) ::
|
||||
:ok | {:ok, Activity.t()} | Tesla.Env.result() | {:error, String.t()}
|
||||
def handle(:publish, activity) do
|
||||
Logger.debug(inspect(activity))
|
||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||
@@ -46,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
with {:ok, %Actor{} = actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(activity.data["actor"]) do
|
||||
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
|
||||
ActivityPub.publish(actor, activity)
|
||||
ActivityPub.Publisher.publish(actor, activity)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -67,7 +71,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
end
|
||||
|
||||
def handle(:publish_single_ap, params) do
|
||||
ActivityPub.publish_one(params)
|
||||
ActivityPub.Publisher.publish_one(params)
|
||||
end
|
||||
|
||||
def handle(type, _) do
|
||||
@@ -75,6 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
{:error, "Don't know what to do with this"}
|
||||
end
|
||||
|
||||
@spec enqueue(atom(), map(), pos_integer()) :: :ok | {:ok, any()} | {:error, any()}
|
||||
def enqueue(type, payload, priority \\ 1) do
|
||||
Logger.debug("enqueue something with type #{inspect(type)}")
|
||||
|
||||
@@ -85,6 +90,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_start_job(any(), any()) :: {any(), any()}
|
||||
def maybe_start_job(running_jobs, queue) do
|
||||
if :sets.size(running_jobs) < @max_jobs && queue != [] do
|
||||
{{type, payload}, queue} = queue_pop(queue)
|
||||
@@ -96,6 +102,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
end
|
||||
end
|
||||
|
||||
@spec handle_cast(any(), any()) :: {:noreply, any()}
|
||||
def handle_cast({:enqueue, type, payload, _priority}, state)
|
||||
when type in [:incoming_doc, :incoming_ap_doc] do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
@@ -119,6 +126,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@spec handle_info({:DOWN, any(), :process, any, any()}, any) :: {:noreply, map()}
|
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
i_running_jobs = :sets.del_element(ref, i_running_jobs)
|
||||
@@ -129,11 +137,13 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
||||
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
|
||||
end
|
||||
|
||||
@spec enqueue_sorted(any(), any(), pos_integer()) :: any()
|
||||
def enqueue_sorted(queue, element, priority) do
|
||||
[%{item: element, priority: priority} | queue]
|
||||
|> Enum.sort_by(fn %{priority: priority} -> priority end)
|
||||
end
|
||||
|
||||
@spec queue_pop(list(any())) :: {any(), list(any())}
|
||||
def queue_pop([%{item: element} | queue]) do
|
||||
{element, queue}
|
||||
end
|
||||
|
||||
@@ -19,9 +19,8 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
|
||||
@spec fetch(String.t(), Keyword.t()) ::
|
||||
{:ok, map()}
|
||||
| {:ok, Tesla.Env.t()}
|
||||
| {:error, any()}
|
||||
| {:error, :invalid_url}
|
||||
| {:error,
|
||||
:invalid_url | :http_gone | :http_error | :http_not_found | :content_not_json}
|
||||
def fetch(url, options \\ []) do
|
||||
on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())
|
||||
date = Signature.generate_date_header()
|
||||
@@ -35,7 +34,7 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
|
||||
if address_valid?(url) do
|
||||
case ActivityPubClient.get(client, url) do
|
||||
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 ->
|
||||
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 and is_map(data) ->
|
||||
{:ok, data}
|
||||
|
||||
{:ok, %Tesla.Env{status: 410}} ->
|
||||
@@ -46,8 +45,12 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
Logger.debug("Resource at #{url} is 404 Gone")
|
||||
{:error, :http_not_found}
|
||||
|
||||
{:ok, %Tesla.Env{body: data}} when is_binary(data) ->
|
||||
{:error, :content_not_json}
|
||||
|
||||
{:ok, %Tesla.Env{} = res} ->
|
||||
{:error, res}
|
||||
Logger.debug("Resource returned bad HTTP code inspect #{res}")
|
||||
{:error, :http_error}
|
||||
end
|
||||
else
|
||||
{:error, :invalid_url}
|
||||
@@ -55,30 +58,32 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
end
|
||||
|
||||
@spec fetch_and_create(String.t(), Keyword.t()) ::
|
||||
{:ok, map(), struct()} | {:error, :invalid_url} | {:error, String.t()} | {:error, any}
|
||||
{:ok, map(), struct()} | {:error, atom()} | :error
|
||||
def fetch_and_create(url, options \\ []) do
|
||||
with {:ok, data} when is_map(data) <- fetch(url, options),
|
||||
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
|
||||
params <- %{
|
||||
"type" => "Create",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["actor"] || data["attributedTo"],
|
||||
"attributedTo" => data["attributedTo"] || data["actor"],
|
||||
"object" => data
|
||||
} do
|
||||
Transmogrifier.handle_incoming(params)
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, "Object origin check failed"}
|
||||
case fetch(url, options) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
if origin_check?(url, data) do
|
||||
case Transmogrifier.handle_incoming(%{
|
||||
"type" => "Create",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["actor"] || data["attributedTo"],
|
||||
"attributedTo" => data["attributedTo"] || data["actor"],
|
||||
"object" => data
|
||||
}) do
|
||||
{:ok, entity, structure} ->
|
||||
{:ok, entity, structure}
|
||||
|
||||
# Returned content is not JSON
|
||||
{:ok, data} when is_binary(data) ->
|
||||
{:error, "Failed to parse content as JSON"}
|
||||
{:error, error} when is_atom(error) ->
|
||||
{:error, error}
|
||||
|
||||
{:error, :invalid_url} ->
|
||||
{:error, :invalid_url}
|
||||
:error ->
|
||||
{:error, :transmogrifier_error}
|
||||
end
|
||||
else
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, :object_origin_check_failed}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
@@ -86,22 +91,23 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
end
|
||||
|
||||
@spec fetch_and_update(String.t(), Keyword.t()) ::
|
||||
{:ok, map(), struct()} | {:error, String.t()} | :error | {:error, any}
|
||||
{:ok, map(), struct()} | {:error, atom()}
|
||||
def fetch_and_update(url, options \\ []) do
|
||||
with {:ok, data} when is_map(data) <- fetch(url, options),
|
||||
{:origin_check, true} <- {:origin_check, origin_check(url, data)},
|
||||
params <- %{
|
||||
"type" => "Update",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["actor"] || data["attributedTo"],
|
||||
"attributedTo" => data["attributedTo"] || data["actor"],
|
||||
"object" => data
|
||||
} do
|
||||
Transmogrifier.handle_incoming(params)
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
{:error, "Object origin check failed"}
|
||||
case fetch(url, options) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
if origin_check(url, data) do
|
||||
Transmogrifier.handle_incoming(%{
|
||||
"type" => "Update",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["actor"] || data["attributedTo"],
|
||||
"attributedTo" => data["attributedTo"] || data["actor"],
|
||||
"object" => data
|
||||
})
|
||||
else
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, :object_origin_check_failed}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
128
lib/federation/activity_pub/publisher.ex
Normal file
128
lib/federation/activity_pub/publisher.ex
Normal file
@@ -0,0 +1,128 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Publisher do
|
||||
@moduledoc """
|
||||
Handle publishing activities
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay, Transmogrifier, Visibility}
|
||||
alias Mobilizon.Federation.HTTPSignatures.Signature
|
||||
require Logger
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [remote_actors: 1]
|
||||
|
||||
@doc """
|
||||
Publish an activity to all appropriated audiences inboxes
|
||||
"""
|
||||
# credo:disable-for-lines:47
|
||||
@spec publish(Actor.t(), Activity.t()) :: :ok
|
||||
def publish(actor, %Activity{recipients: recipients} = activity) do
|
||||
Logger.debug("Publishing an activity")
|
||||
Logger.debug(inspect(activity, pretty: true))
|
||||
|
||||
public = Visibility.is_public?(activity)
|
||||
Logger.debug("is public ? #{public}")
|
||||
|
||||
if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do
|
||||
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
|
||||
|
||||
Relay.publish(activity)
|
||||
end
|
||||
|
||||
recipients = Enum.uniq(recipients)
|
||||
|
||||
{recipients, followers} = convert_followers_in_recipients(recipients)
|
||||
|
||||
{recipients, members} = convert_members_in_recipients(recipients)
|
||||
|
||||
remote_inboxes =
|
||||
(remote_actors(recipients) ++ followers ++ members)
|
||||
|> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
Logger.debug(fn -> "Remote inboxes are : #{inspect(remote_inboxes)}" end)
|
||||
|
||||
Enum.each(remote_inboxes, fn inbox ->
|
||||
Federator.enqueue(:publish_single_ap, %{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor: actor,
|
||||
id: activity.data["id"]
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Publish an activity to a specific inbox
|
||||
"""
|
||||
@spec publish_one(%{inbox: String.t(), json: String.t(), actor: Actor.t(), id: String.t()}) ::
|
||||
Tesla.Env.result()
|
||||
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)
|
||||
|
||||
digest = Signature.build_digest(json)
|
||||
date = Signature.generate_date_header()
|
||||
|
||||
# request_target = Signature.generate_request_target("POST", path)
|
||||
|
||||
signature =
|
||||
Signature.sign(actor, %{
|
||||
"(request-target)": "post #{path}",
|
||||
host: host,
|
||||
"content-length": byte_size(json),
|
||||
digest: digest,
|
||||
date: date
|
||||
})
|
||||
|
||||
Tesla.post(
|
||||
inbox,
|
||||
json,
|
||||
headers: [
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"signature", signature},
|
||||
{"digest", digest},
|
||||
{"date", date}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())}
|
||||
defp convert_followers_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
|
||||
case Actors.get_actor_by_followers_url(recipient) do
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.followers_url end),
|
||||
follower_actors ++ Actors.list_external_followers_for_actor(group)}
|
||||
|
||||
nil ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec is_create_activity?(Activity.t()) :: boolean
|
||||
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
|
||||
defp is_create_activity?(_), do: false
|
||||
|
||||
@spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
|
||||
defp convert_members_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc ->
|
||||
case Actors.get_group_by_members_url(recipient) do
|
||||
# If the group is local just add external members
|
||||
%Actor{domain: domain} = group when is_nil(domain) ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group)}
|
||||
|
||||
# If it's remote add the remote group actor as well
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]}
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -11,8 +11,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier}
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.WebFinger
|
||||
alias Mobilizon.Service.Workers.Background
|
||||
@@ -118,7 +117,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
{:ok, Oban.Job.t()}
|
||||
| {:error, Ecto.Changeset.t()}
|
||||
| {:error, :bad_url}
|
||||
| {:error, Mobilizon.Federation.ActivityPub.Actor.make_actor_errors()}
|
||||
| {:error, ActivityPubActor.make_actor_errors()}
|
||||
| {:error, :no_internal_relay_actor}
|
||||
| {:error, :url_nil}
|
||||
def refresh(address) do
|
||||
@@ -145,7 +144,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
{object, object_id} <- fetch_object(object),
|
||||
id <- "#{object_id}/announces/#{actor_id}" do
|
||||
Logger.info("Publishing activity #{id} to all relays")
|
||||
ActivityPub.announce(actor, object, id, true, false)
|
||||
Actions.Announce.announce(actor, object, id, true, false)
|
||||
else
|
||||
e ->
|
||||
Logger.error("Error while getting local instance actor: #{inspect(e)}")
|
||||
|
||||
@@ -17,7 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Permission, Relay, Utils}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Permission, Relay, Utils}
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Ownable
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
@@ -50,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
local: false
|
||||
}
|
||||
|
||||
ActivityPub.flag(params, false)
|
||||
Actions.Flag.flag(params, false)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,10 +77,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:ok, %Activity{} = activity, entity} <-
|
||||
(if is_data_for_comment_or_discussion?(object_data) do
|
||||
Logger.debug("Chosing to create a regular comment")
|
||||
ActivityPub.create(:comment, object_data, false)
|
||||
Actions.Create.create(:comment, object_data, false)
|
||||
else
|
||||
Logger.debug("Chosing to initialize or add a comment to a conversation")
|
||||
ActivityPub.create(:discussion, object_data, false)
|
||||
Actions.Create.create(:discussion, object_data, false)
|
||||
end) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
@@ -110,7 +110,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object |> Converter.Event.as_to_model_data(),
|
||||
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Event{} = event} <-
|
||||
ActivityPub.create(:event, object_data, false) do
|
||||
Actions.Create.create(:event, object_data, false) do
|
||||
{:ok, activity, event}
|
||||
else
|
||||
{:existing_event, %Event{} = event} -> {:ok, nil, event}
|
||||
@@ -146,7 +146,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
%Actor{type: :Group} = group <- Actors.get_actor(object_data.parent_id),
|
||||
%Actor{} = actor <- Actors.get_actor(object_data.actor_id),
|
||||
{:ok, %Activity{} = activity, %Member{} = member} <-
|
||||
ActivityPub.join(group, actor, false, %{
|
||||
Actions.Join.join(group, actor, false, %{
|
||||
url: object_data.url,
|
||||
metadata: %{role: object_data.role}
|
||||
}) do
|
||||
@@ -173,7 +173,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:existing_post, nil} <-
|
||||
{:existing_post, Posts.get_post_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Post{} = post} <-
|
||||
ActivityPub.create(:post, object_data, false) do
|
||||
Actions.Create.create(:post, object_data, false) do
|
||||
{:ok, activity, post}
|
||||
else
|
||||
{:existing_post, %Post{} = post} ->
|
||||
@@ -198,7 +198,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:ok, nil, comment}
|
||||
|
||||
{:ok, entity} ->
|
||||
ActivityPub.delete(entity, Relay.get_actor(), false)
|
||||
Actions.Delete.delete(entity, Relay.get_actor(), false)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -207,7 +207,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
) do
|
||||
with {:ok, %Actor{} = followed} <- ActivityPubActor.get_or_fetch_actor_by_url(followed, true),
|
||||
{:ok, %Actor{} = follower} <- ActivityPubActor.get_or_fetch_actor_by_url(follower),
|
||||
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
{:ok, activity, object} <-
|
||||
Actions.Follow.follow(follower, followed, id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:error, :person_no_follow} ->
|
||||
@@ -233,7 +234,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object_data when is_map(object_data) <-
|
||||
object |> Converter.TodoList.as_to_model_data(),
|
||||
{:ok, %Activity{} = activity, %TodoList{} = todo_list} <-
|
||||
ActivityPub.create(:todo_list, object_data, false, %{"actor" => actor_url}) do
|
||||
Actions.Create.create(:todo_list, object_data, false, %{
|
||||
"actor" => actor_url
|
||||
}) do
|
||||
{:ok, activity, todo_list}
|
||||
else
|
||||
{:error, :group_not_found} -> :error
|
||||
@@ -252,7 +255,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object_data <-
|
||||
object |> Converter.Todo.as_to_model_data(),
|
||||
{:ok, %Activity{} = activity, %Todo{} = todo} <-
|
||||
ActivityPub.create(:todo, object_data, false) do
|
||||
Actions.Create.create(:todo, object_data, false) do
|
||||
{:ok, activity, todo}
|
||||
else
|
||||
{:existing_todo, %Todo{} = todo} -> {:ok, nil, todo}
|
||||
@@ -277,7 +280,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)},
|
||||
{:ok, %Activity{} = activity, %Resource{} = resource} <-
|
||||
ActivityPub.create(:resource, object_data, false) do
|
||||
Actions.Create.create(:resource, object_data, false) do
|
||||
{:ok, activity, resource}
|
||||
else
|
||||
{:existing_resource, %Resource{} = resource} ->
|
||||
@@ -388,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object_data <-
|
||||
object |> Converter.Actor.as_to_model_data(),
|
||||
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
|
||||
ActivityPub.update(old_actor, object_data, false) do
|
||||
Actions.Update.update(old_actor, object_data, false) do
|
||||
{:ok, activity, new_actor}
|
||||
else
|
||||
e ->
|
||||
@@ -416,7 +419,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
Utils.origin_check?(actor_url, update_data) ||
|
||||
Permission.can_update_group_object?(actor, old_event)},
|
||||
{:ok, %Activity{} = activity, %Event{} = new_event} <-
|
||||
ActivityPub.update(old_event, object_data, false) do
|
||||
Actions.Update.update(old_event, object_data, false) do
|
||||
{:ok, activity, new_event}
|
||||
else
|
||||
_e ->
|
||||
@@ -438,7 +441,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
|
||||
object_data <- transform_object_data_for_discussion(object_data),
|
||||
{:ok, %Activity{} = activity, new_entity} <-
|
||||
ActivityPub.update(old_entity, object_data, false) do
|
||||
Actions.Update.update(old_entity, object_data, false) do
|
||||
{:ok, activity, new_entity}
|
||||
else
|
||||
_e ->
|
||||
@@ -461,7 +464,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
Utils.origin_check?(actor_url, update_data["object"]) ||
|
||||
Permission.can_update_group_object?(actor, old_post)},
|
||||
{:ok, %Activity{} = activity, %Post{} = new_post} <-
|
||||
ActivityPub.update(old_post, object_data, false) do
|
||||
Actions.Update.update(old_post, object_data, false) do
|
||||
{:ok, activity, new_post}
|
||||
else
|
||||
{:origin_check, _} ->
|
||||
@@ -489,7 +492,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
Utils.origin_check?(actor_url, update_data) ||
|
||||
Permission.can_update_group_object?(actor, old_resource)},
|
||||
{:ok, %Activity{} = activity, %Resource{} = new_resource} <-
|
||||
ActivityPub.update(old_resource, object_data, false) do
|
||||
Actions.Update.update(old_resource, object_data, false) do
|
||||
{:ok, activity, new_resource}
|
||||
else
|
||||
_e ->
|
||||
@@ -510,7 +513,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object_data <- Converter.Member.as_to_model_data(object),
|
||||
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
|
||||
{:ok, %Activity{} = activity, new_entity} <-
|
||||
ActivityPub.update(old_entity, object_data, false, %{moderator: actor}) do
|
||||
Actions.Update.update(old_entity, object_data, false, %{moderator: actor}) do
|
||||
{:ok, activity, new_entity}
|
||||
else
|
||||
_e ->
|
||||
@@ -527,7 +530,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
|
||||
with object_url <- Utils.get_url(object),
|
||||
{:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do
|
||||
ActivityPub.delete(entity, Relay.get_actor(), false)
|
||||
Actions.Delete.delete(entity, Relay.get_actor(), false)
|
||||
else
|
||||
{:ok, %Tombstone{} = tombstone} ->
|
||||
{:ok, nil, tombstone}
|
||||
@@ -550,7 +553,13 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor),
|
||||
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
|
||||
Actions.Announce.unannounce(
|
||||
actor,
|
||||
object,
|
||||
id,
|
||||
cancelled_activity_id,
|
||||
false
|
||||
) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
_e -> :error
|
||||
@@ -568,7 +577,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
with {:ok, %Actor{domain: nil} = followed} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(followed),
|
||||
{:ok, %Actor{} = follower} <- ActivityPubActor.get_or_fetch_actor_by_url(follower),
|
||||
{:ok, activity, object} <- ActivityPub.unfollow(follower, followed, id, false) do
|
||||
{:ok, activity, object} <-
|
||||
Actions.Follow.unfollow(follower, followed, id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
@@ -593,7 +603,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:origin_check,
|
||||
Utils.origin_check_from_id?(actor_url, object_id) ||
|
||||
Permission.can_delete_group_object?(actor, object)},
|
||||
{:ok, activity, object} <- ActivityPub.delete(object, actor, false) do
|
||||
{:ok, activity, object} <- Actions.Delete.delete(object, actor, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
@@ -637,7 +647,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:origin_check,
|
||||
Utils.origin_check?(actor_url, data) ||
|
||||
Permission.can_update_group_object?(actor, old_resource)},
|
||||
{:ok, activity, new_resource} <- ActivityPub.move(:resource, old_resource, object_data) do
|
||||
{:ok, activity, new_resource} <-
|
||||
Actions.Move.move(:resource, old_resource, object_data) do
|
||||
{:ok, activity, new_resource}
|
||||
else
|
||||
e ->
|
||||
@@ -665,7 +676,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.join(object, actor, false, %{
|
||||
Actions.Join.join(object, actor, false, %{
|
||||
url: id,
|
||||
metadata: %{message: Map.get(data, "participationMessage")}
|
||||
}) do
|
||||
@@ -682,7 +693,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor),
|
||||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <- ActivityPub.leave(object, actor, false) do
|
||||
{:ok, activity, object} <- Actions.Leave.leave(object, actor, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:only_organizer, true} ->
|
||||
@@ -714,7 +725,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:ok, %Actor{} = target} <-
|
||||
target |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(),
|
||||
{:ok, activity, %Member{} = member} <-
|
||||
ActivityPub.invite(object, actor, target, false, %{url: id}) do
|
||||
Actions.Invite.invite(object, actor, target, false, %{url: id}) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
@@ -734,7 +745,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:is_admin, Actors.get_member(moderator_id, group_id)},
|
||||
{:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <-
|
||||
{:is_member, Actors.get_member(person_id, group_id)} do
|
||||
ActivityPub.remove(member, group, moderator, false)
|
||||
Actions.Remove.remove(member, group, moderator, false)
|
||||
else
|
||||
{:is_admin, {:ok, %Member{}}} ->
|
||||
Logger.warn(
|
||||
@@ -786,7 +797,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:follow, get_follow(follow_object)},
|
||||
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
|
||||
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:follow,
|
||||
follow,
|
||||
false
|
||||
@@ -824,7 +835,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:follow, get_follow(follow_object)},
|
||||
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
|
||||
{:ok, activity, _} <-
|
||||
ActivityPub.reject(:follow, follow) do
|
||||
Actions.Reject.reject(:follow, follow) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:follow, _err} ->
|
||||
@@ -879,7 +890,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:can_accept_event_join, true} <-
|
||||
{:can_accept_event_join, can_manage_event?(actor_accepting, event)},
|
||||
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:join,
|
||||
participant,
|
||||
false
|
||||
@@ -911,7 +922,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do
|
||||
# Or maybe for groups it's the group that sends the Accept activity
|
||||
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
type,
|
||||
member,
|
||||
false
|
||||
@@ -929,7 +940,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:can_accept_event_reject, true} <-
|
||||
{:can_accept_event_reject, can_manage_event?(actor_accepting, event)},
|
||||
{:ok, activity, participant} <-
|
||||
ActivityPub.reject(:join, participant, false),
|
||||
Actions.Reject.reject(:join, participant, false),
|
||||
:ok <- Participation.send_emails_to_local_user(participant) do
|
||||
{:ok, activity, participant}
|
||||
else
|
||||
@@ -960,7 +971,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:invite, get_member(invite_object)},
|
||||
{:same_actor, true} <- {:same_actor, actor_rejecting.id == actor_id},
|
||||
{:ok, activity, member} <-
|
||||
ActivityPub.reject(:invite, member, false) do
|
||||
Actions.Reject.reject(:invite, member, false) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
@@ -1139,7 +1150,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
# Before 1.0.4 the object of a "Remove" activity was an actor's URL
|
||||
# instead of the member's URL.
|
||||
# TODO: Remove in 1.2
|
||||
@spec get_remove_object(map() | String.t()) :: {:ok, String.t() | integer()}
|
||||
@spec get_remove_object(map() | String.t()) :: {:ok, integer()}
|
||||
defp get_remove_object(object) do
|
||||
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
|
||||
{:ok, %Member{actor: %Actor{id: person_id}}} -> {:ok, person_id}
|
||||
@@ -1162,7 +1173,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
organizer_actor_id == actor_id
|
||||
end
|
||||
|
||||
defp can_manage_event?(_actor, _event) do
|
||||
defp can_manage_event?(%Actor{} = _actor, %Event{} = _event) do
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,8 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
@moduledoc false
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Permission, Relay}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
@@ -68,7 +67,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Actor.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Actor.t()}
|
||||
{:ok, ActivityStream.t(), Actor.t(), Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%Actor{
|
||||
followers_url: followers_url,
|
||||
@@ -245,7 +244,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
Mobilizon.Actors.get_default_member_role(group) == :member &&
|
||||
role == :member ->
|
||||
{:accept,
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:join,
|
||||
member,
|
||||
true,
|
||||
@@ -282,7 +281,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
Logger.debug("Target doesn't manually approves followers, we can accept right away")
|
||||
|
||||
{:accept,
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:follow,
|
||||
follower,
|
||||
true,
|
||||
|
||||
@@ -70,7 +70,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Comment.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Comment.t()} | {:error, Ecto.Changeset.t()}
|
||||
{:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Comment.t()}
|
||||
def delete(
|
||||
%Comment{url: url, id: comment_id},
|
||||
%Actor{} = actor,
|
||||
@@ -208,7 +208,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
||||
end
|
||||
end
|
||||
|
||||
@spec event_allows_commenting?(%{actor_id: String.t() | integer, event: Event.t()}) :: boolean
|
||||
@spec event_allows_commenting?(%{
|
||||
required(:actor_id) => String.t() | integer,
|
||||
required(:event) => Event.t() | nil,
|
||||
optional(atom) => any()
|
||||
}) :: boolean
|
||||
defp event_allows_commenting?(%{
|
||||
actor_id: actor_id,
|
||||
event: %Event{
|
||||
|
||||
@@ -17,94 +17,110 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Discussion.t(), ActivityStream.t()}
|
||||
| {:error, :discussion_not_found | :last_comment_not_found | Ecto.Changeset.t()}
|
||||
def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do
|
||||
with args <- prepare_args(args),
|
||||
%Discussion{} = discussion <- Discussions.get_discussion(discussion_id),
|
||||
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <-
|
||||
Discussions.reply_to_discussion(discussion, args),
|
||||
{:ok, _} <-
|
||||
DiscussionActivity.insert_activity(discussion,
|
||||
subject: "discussion_replied",
|
||||
actor_id: Map.get(args, :creator_id, args.actor_id)
|
||||
),
|
||||
%Comment{} = last_comment <- Discussions.get_comment_with_preload(last_comment_id),
|
||||
:ok <- maybe_publish_graphql_subscription(discussion),
|
||||
comment_as_data <- Convertible.model_to_as(last_comment),
|
||||
audience <-
|
||||
Audience.get_audience(discussion),
|
||||
create_data <-
|
||||
make_create_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, discussion, create_data}
|
||||
args = prepare_args(args)
|
||||
|
||||
case Discussions.get_discussion(discussion_id) do
|
||||
%Discussion{} = discussion ->
|
||||
case Discussions.reply_to_discussion(discussion, args) do
|
||||
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} ->
|
||||
DiscussionActivity.insert_activity(discussion,
|
||||
subject: "discussion_replied",
|
||||
actor_id: Map.get(args, :creator_id, args.actor_id)
|
||||
)
|
||||
|
||||
case Discussions.get_comment_with_preload(last_comment_id) do
|
||||
%Comment{} = last_comment ->
|
||||
maybe_publish_graphql_subscription(discussion)
|
||||
comment_as_data = Convertible.model_to_as(last_comment)
|
||||
audience = Audience.get_audience(discussion)
|
||||
create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
|
||||
{:ok, discussion, create_data}
|
||||
|
||||
nil ->
|
||||
{:error, :last_comment_not_found}
|
||||
end
|
||||
|
||||
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :discussion_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args(args),
|
||||
{:ok, %Discussion{} = discussion} <-
|
||||
Discussions.create_discussion(args),
|
||||
{:ok, _} <-
|
||||
DiscussionActivity.insert_activity(discussion, subject: "discussion_created"),
|
||||
discussion_as_data <- Convertible.model_to_as(discussion),
|
||||
audience <-
|
||||
Audience.get_audience(discussion),
|
||||
create_data <-
|
||||
make_create_data(discussion_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, discussion, create_data}
|
||||
args = prepare_args(args)
|
||||
|
||||
case Discussions.create_discussion(args) do
|
||||
{:ok, %Discussion{} = discussion} ->
|
||||
DiscussionActivity.insert_activity(discussion, subject: "discussion_created")
|
||||
discussion_as_data = Convertible.model_to_as(discussion)
|
||||
audience = Audience.get_audience(discussion)
|
||||
create_data = make_create_data(discussion_as_data, Map.merge(audience, additional))
|
||||
{:ok, discussion, create_data}
|
||||
|
||||
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()}
|
||||
@spec update(Discussion.t(), map(), map()) ::
|
||||
{:ok, Discussion.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Discussion{} = old_discussion, args, additional) do
|
||||
with {:ok, %Discussion{} = new_discussion} <-
|
||||
Discussions.update_discussion(old_discussion, args),
|
||||
{:ok, _} <-
|
||||
DiscussionActivity.insert_activity(new_discussion,
|
||||
subject: "discussion_renamed",
|
||||
old_discussion: old_discussion
|
||||
),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"),
|
||||
discussion_as_data <- Convertible.model_to_as(new_discussion),
|
||||
audience <-
|
||||
Audience.get_audience(new_discussion),
|
||||
update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_discussion, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
case Discussions.update_discussion(old_discussion, args) do
|
||||
{:ok, %Discussion{} = new_discussion} ->
|
||||
DiscussionActivity.insert_activity(new_discussion,
|
||||
subject: "discussion_renamed",
|
||||
old_discussion: old_discussion
|
||||
)
|
||||
|
||||
Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}")
|
||||
discussion_as_data = Convertible.model_to_as(new_discussion)
|
||||
audience = Audience.get_audience(new_discussion)
|
||||
update_data = make_update_data(discussion_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_discussion, update_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Discussion.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Discussion.t()}
|
||||
{:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Discussion.t()}
|
||||
def delete(
|
||||
%Discussion{actor: group, url: url} = discussion,
|
||||
%Actor{} = actor,
|
||||
_local,
|
||||
_additionnal
|
||||
) do
|
||||
with {:ok, _} <- Discussions.delete_discussion(discussion),
|
||||
{:ok, _} <-
|
||||
DiscussionActivity.insert_activity(discussion,
|
||||
subject: "discussion_deleted",
|
||||
moderator: actor
|
||||
) do
|
||||
# This is just fake
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => Convertible.model_to_as(discussion),
|
||||
"id" => url <> "/delete",
|
||||
"to" => [group.members_url]
|
||||
}
|
||||
case Discussions.delete_discussion(discussion) do
|
||||
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
|
||||
{:ok, activity_data, actor, discussion}
|
||||
{:ok, %{comments: {_, _}}} ->
|
||||
DiscussionActivity.insert_activity(discussion,
|
||||
subject: "discussion_deleted",
|
||||
moderator: actor
|
||||
)
|
||||
|
||||
# This is just fake
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => Convertible.model_to_as(discussion),
|
||||
"id" => url <> "/delete",
|
||||
"to" => [group.members_url]
|
||||
}
|
||||
|
||||
{:ok, activity_data, actor, discussion}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ alias Mobilizon.Federation.ActivityPub.Types.{
|
||||
}
|
||||
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Federation.ActivityPub.Permission
|
||||
alias Mobilizon.Posts.Post
|
||||
@@ -28,27 +28,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
|
||||
@moduledoc """
|
||||
ActivityPub entity behaviour
|
||||
"""
|
||||
@type t :: %{id: String.t(), url: String.t()}
|
||||
|
||||
@type entities ::
|
||||
Actor.t()
|
||||
| Member.t()
|
||||
| Event.t()
|
||||
| Participant.t()
|
||||
| Comment.t()
|
||||
| Discussion.t()
|
||||
| Post.t()
|
||||
| Resource.t()
|
||||
| Todo.t()
|
||||
| TodoList.t()
|
||||
@type t :: %{required(:id) => any(), optional(:url) => String.t(), optional(atom()) => any()}
|
||||
|
||||
@callback create(data :: any(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()} | {:error, any()}
|
||||
|
||||
@callback update(struct :: t(), attrs :: map(), additionnal :: map()) ::
|
||||
@callback update(structure :: t(), attrs :: map(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()} | {:error, any()}
|
||||
|
||||
@callback delete(struct :: t(), Actor.t(), local :: boolean(), map()) ::
|
||||
@callback delete(structure :: t(), actor :: Actor.t(), local :: boolean(), additionnal :: map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), t()} | {:error, any()}
|
||||
end
|
||||
|
||||
@@ -57,47 +45,61 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
|
||||
ActivityPub entity Managable protocol.
|
||||
"""
|
||||
|
||||
@spec update(Entity.t(), map(), map()) ::
|
||||
{:ok, Entity.t(), ActivityStream.t()} | {:error, any()}
|
||||
@doc """
|
||||
Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it
|
||||
"""
|
||||
@spec update(Entity.t(), map(), map()) ::
|
||||
{:ok, Entity.t(), ActivityStream.t()} | {:error, any()}
|
||||
def update(entity, attrs, additionnal)
|
||||
|
||||
@doc "Deletes an entity and returns the activitystream representation for it"
|
||||
@spec delete(Entity.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Entity.t()} | {:error, any()}
|
||||
@doc "Deletes an entity and returns the activitystream representation for it"
|
||||
def delete(entity, actor, local, additionnal)
|
||||
end
|
||||
|
||||
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
|
||||
@type group_role :: :member | :moderator | :administrator | nil
|
||||
|
||||
@spec group_actor(Entity.t()) :: Actor.t() | nil
|
||||
@doc "Returns an eventual group for the entity"
|
||||
@spec group_actor(Entity.t()) :: Actor.t() | nil
|
||||
def group_actor(entity)
|
||||
|
||||
@spec actor(Entity.t()) :: Actor.t() | nil
|
||||
@doc "Returns the actor for the entity"
|
||||
@spec actor(Entity.t()) :: Actor.t() | nil
|
||||
def actor(entity)
|
||||
|
||||
@doc """
|
||||
Returns the list of permissions for an entity
|
||||
"""
|
||||
@spec permissions(Entity.t()) :: Permission.t()
|
||||
def permissions(entity)
|
||||
end
|
||||
|
||||
defimpl Managable, for: Event do
|
||||
@spec update(Event.t(), map, map) ::
|
||||
{:error, atom() | Ecto.Changeset.t()} | {:ok, Event.t(), ActivityStream.t()}
|
||||
defdelegate update(entity, attrs, additionnal), to: Events
|
||||
|
||||
@spec delete(entity :: Event.t(), actor :: Actor.t(), local :: boolean(), additionnal :: map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Event.t()} | {:error, atom() | Ecto.Changeset.t()}
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Events
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Event do
|
||||
@spec group_actor(Event.t()) :: Actor.t() | nil
|
||||
defdelegate group_actor(entity), to: Events
|
||||
@spec actor(Event.t()) :: Actor.t() | nil
|
||||
defdelegate actor(entity), to: Events
|
||||
@spec permissions(Event.t()) :: Permission.t()
|
||||
defdelegate permissions(entity), to: Events
|
||||
end
|
||||
|
||||
defimpl Managable, for: Comment do
|
||||
@spec update(Comment.t(), map, map) ::
|
||||
{:error, Ecto.Changeset.t()} | {:ok, Comment.t(), ActivityStream.t()}
|
||||
defdelegate update(entity, attrs, additionnal), to: Comments
|
||||
|
||||
@spec delete(Comment.t(), Actor.t(), boolean, map) ::
|
||||
{:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Comment.t()}
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Comments
|
||||
end
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events, as: EventsManager
|
||||
alias Mobilizon.Events.{Event, Participant, ParticipantRole}
|
||||
alias Mobilizon.Federation.{ActivityPub, ActivityStream}
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
@@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
{:error, _step, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
@@ -89,11 +89,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
Share.delete_all_by_uri(event.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, event}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
@@ -166,11 +166,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
|
||||
@spec check_attendee_capacity?(Event.t()) :: boolean
|
||||
defp check_attendee_capacity?(%Event{options: options} = event) do
|
||||
with maximum_attendee_capacity <-
|
||||
Map.get(options, :maximum_attendee_capacity) || 0 do
|
||||
maximum_attendee_capacity == 0 ||
|
||||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
|
||||
end
|
||||
maximum_attendee_capacity = Map.get(options, :maximum_attendee_capacity) || 0
|
||||
|
||||
maximum_attendee_capacity == 0 ||
|
||||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
|
||||
end
|
||||
|
||||
# Set the participant to approved if the default role for new participants is :participant
|
||||
@@ -211,7 +210,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
Mobilizon.Events.get_default_participant_role(event) == :participant &&
|
||||
role == :participant ->
|
||||
{:accept,
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:join,
|
||||
participant,
|
||||
true,
|
||||
|
||||
@@ -2,7 +2,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
||||
@moduledoc false
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member, MemberRole}
|
||||
alias Mobilizon.Federation.{ActivityPub, ActivityStream}
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Service.Activity.Member, as: MemberActivity
|
||||
alias Mobilizon.Web.Endpoint
|
||||
@@ -74,7 +75,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
||||
_additionnal
|
||||
) do
|
||||
Logger.debug("Deleting a member")
|
||||
ActivityPub.leave(group, actor, local, %{force_member_removal: true})
|
||||
Actions.Leave.leave(group, actor, local, %{force_member_removal: true})
|
||||
end
|
||||
|
||||
@spec actor(Member.t()) :: Actor.t() | nil
|
||||
|
||||
@@ -1,46 +1,53 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Discussions, Reports}
|
||||
alias Mobilizon.{Actors, Discussions, Events, Reports}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Reports.Report
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
require Logger
|
||||
|
||||
@spec flag(map(), boolean(), map()) :: {Report.t(), ActivityStream.t()}
|
||||
@spec flag(map(), boolean(), map()) ::
|
||||
{:ok, Report.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def flag(args, local \\ false, _additional \\ %{}) do
|
||||
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
|
||||
{:create_report, {:ok, %Report{} = report}} <-
|
||||
{:create_report, Reports.create_report(args)},
|
||||
report_as_data <- Convertible.model_to_as(report),
|
||||
cc <- if(local, do: [report.reported.url], else: []),
|
||||
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do
|
||||
{report, report_as_data}
|
||||
with {:ok, %Report{} = report} <- args |> prepare_args_for_report() |> Reports.create_report() do
|
||||
report_as_data = Convertible.model_to_as(report)
|
||||
cc = if(local, do: [report.reported.url], else: [])
|
||||
report_as_data = Map.merge(report_as_data, %{"to" => [], "cc" => cc})
|
||||
{:ok, report, report_as_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec prepare_args_for_report(map()) :: map()
|
||||
defp prepare_args_for_report(args) do
|
||||
with {:reporter, %Actor{} = reporter_actor} <-
|
||||
{:reporter, Actors.get_actor!(args.reporter_id)},
|
||||
{:reported, %Actor{} = reported_actor} <-
|
||||
{:reported, Actors.get_actor!(args.reported_id)},
|
||||
content <- HTML.strip_tags(args.content),
|
||||
event <- Discussions.get_comment(Map.get(args, :event_id)),
|
||||
{:get_report_comments, comments} <-
|
||||
{:get_report_comments,
|
||||
Discussions.list_comments_by_actor_and_ids(
|
||||
reported_actor.id,
|
||||
Map.get(args, :comments_ids, [])
|
||||
)} do
|
||||
Map.merge(args, %{
|
||||
reporter: reporter_actor,
|
||||
reported: reported_actor,
|
||||
content: content,
|
||||
event: event,
|
||||
comments: comments
|
||||
})
|
||||
end
|
||||
%Actor{} = reporter_actor = Actors.get_actor!(args.reporter_id)
|
||||
%Actor{} = reported_actor = Actors.get_actor!(args.reported_id)
|
||||
content = HTML.strip_tags(args.content)
|
||||
|
||||
event_id = Map.get(args, :event_id)
|
||||
|
||||
event =
|
||||
if is_nil(event_id) do
|
||||
nil
|
||||
else
|
||||
{:ok, %Event{} = event} = Events.get_event(event_id)
|
||||
event
|
||||
end
|
||||
|
||||
comments =
|
||||
Discussions.list_comments_by_actor_and_ids(
|
||||
reported_actor.id,
|
||||
Map.get(args, :comments_ids, [])
|
||||
)
|
||||
|
||||
Map.merge(args, %{
|
||||
reporter: reporter_actor,
|
||||
reported: reported_actor,
|
||||
content: content,
|
||||
event: event,
|
||||
comments: comments
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, Resource.t(), ActivityStream.t()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Resource.t(), ActivityStream.t()}
|
||||
| {:error, Ecto.Changeset.t() | :creator_not_found | :group_not_found}
|
||||
def create(%{type: type} = args, additional) do
|
||||
args =
|
||||
case type do
|
||||
@@ -37,17 +39,18 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
with {:ok,
|
||||
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
|
||||
Resources.create_resource(args),
|
||||
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_created"),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group, creator: creator}),
|
||||
audience <- %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
} do
|
||||
{:ok, %Actor{} = group, %Actor{url: creator_url} = creator} <-
|
||||
group_and_creator(group_id, creator_id) do
|
||||
ResourceActivity.insert_activity(resource, subject: "resource_created")
|
||||
resource_as_data = Convertible.model_to_as(%{resource | actor: group, creator: creator})
|
||||
|
||||
audience = %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
}
|
||||
|
||||
create_data =
|
||||
case parent_id do
|
||||
nil ->
|
||||
@@ -60,15 +63,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
end
|
||||
|
||||
{:ok, resource, create_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Resource.t(), map(), map()) :: {:ok, Resource.t(), ActivityStream.t()}
|
||||
@spec update(Resource.t(), map(), map()) ::
|
||||
{:ok, Resource.t(), ActivityStream.t()}
|
||||
| {:error, Ecto.Changeset.t() | :creator_not_found | :group_not_found}
|
||||
def update(
|
||||
%Resource{parent_id: old_parent_id} = old_resource,
|
||||
%{parent_id: parent_id} = args,
|
||||
@@ -82,32 +83,35 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
def update(%Resource{} = old_resource, %{title: title} = _args, additional) do
|
||||
with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
|
||||
Resources.update_resource(old_resource, %{title: title}),
|
||||
{:ok, _} <-
|
||||
ResourceActivity.insert_activity(resource,
|
||||
subject: "resource_renamed",
|
||||
old_resource: old_resource
|
||||
),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group}),
|
||||
audience <- %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
},
|
||||
update_data <-
|
||||
make_update_data(resource_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %Actor{} = group, %Actor{url: creator_url}} <-
|
||||
group_and_creator(group_id, creator_id) do
|
||||
ResourceActivity.insert_activity(resource,
|
||||
subject: "resource_renamed",
|
||||
old_resource: old_resource
|
||||
)
|
||||
|
||||
resource_as_data = Convertible.model_to_as(%{resource | actor: group})
|
||||
|
||||
audience = %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
}
|
||||
|
||||
update_data = make_update_data(resource_as_data, Map.merge(audience, additional))
|
||||
{:ok, resource, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec update(Resource.t(), map(), map()) :: {:ok, Resource.t(), ActivityStream.t()}
|
||||
@spec move(Resource.t(), map(), map()) ::
|
||||
{:ok, Resource.t(), ActivityStream.t()}
|
||||
| {:error,
|
||||
Ecto.Changeset.t()
|
||||
| :creator_not_found
|
||||
| :group_not_found
|
||||
| :new_parent_not_found
|
||||
| :old_parent_not_found}
|
||||
def move(
|
||||
%Resource{parent_id: old_parent_id} = old_resource,
|
||||
%{parent_id: _new_parent_id} = args,
|
||||
@@ -117,37 +121,34 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
|
||||
resource} <-
|
||||
Resources.update_resource(old_resource, args),
|
||||
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_moved"),
|
||||
old_parent <- Resources.get_resource(old_parent_id),
|
||||
new_parent <- Resources.get_resource(new_parent_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group}),
|
||||
audience <- %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
},
|
||||
move_data <-
|
||||
make_move_data(
|
||||
resource_as_data,
|
||||
old_parent,
|
||||
new_parent,
|
||||
Map.merge(audience, additional)
|
||||
) do
|
||||
{:ok, old_parent, new_parent} <- parents(old_parent_id, new_parent_id),
|
||||
{:ok, %Actor{} = group, %Actor{url: creator_url}} <-
|
||||
group_and_creator(group_id, creator_id) do
|
||||
ResourceActivity.insert_activity(resource, subject: "resource_moved")
|
||||
resource_as_data = Convertible.model_to_as(%{resource | actor: group})
|
||||
|
||||
audience = %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
}
|
||||
|
||||
move_data =
|
||||
make_move_data(
|
||||
resource_as_data,
|
||||
old_parent,
|
||||
new_parent,
|
||||
Map.merge(audience, additional)
|
||||
)
|
||||
|
||||
{:ok, resource, move_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Resource.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Resource.t()}
|
||||
{:ok, ActivityStream.t(), Actor.t(), Resource.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
|
||||
%Actor{url: actor_url} = actor,
|
||||
@@ -165,10 +166,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
"to" => [members_url]
|
||||
}
|
||||
|
||||
with {:ok, _resource} <- Resources.delete_resource(resource),
|
||||
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_deleted"),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do
|
||||
{:ok, activity_data, actor, resource}
|
||||
case Resources.delete_resource(resource) do
|
||||
{:ok, _resource} ->
|
||||
ResourceActivity.insert_activity(resource, subject: "resource_deleted")
|
||||
Cachex.del(:activity_pub, "resource_#{resource.id}")
|
||||
{:ok, activity_data, actor, resource}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -183,4 +188,28 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
def permissions(%Resource{}) do
|
||||
%Permission{access: :member, create: :member, update: :member, delete: :member}
|
||||
end
|
||||
|
||||
@spec group_and_creator(integer(), integer()) ::
|
||||
{:ok, Actor.t(), Actor.t()} | {:error, :creator_not_found | :group_not_found}
|
||||
defp group_and_creator(group_id, creator_id) do
|
||||
case Actors.get_group_by_actor_id(group_id) do
|
||||
{:ok, %Actor{} = group} ->
|
||||
case Actors.get_actor(creator_id) do
|
||||
%Actor{} = creator ->
|
||||
{:ok, group, creator}
|
||||
|
||||
nil ->
|
||||
{:error, :creator_not_found}
|
||||
end
|
||||
|
||||
{:error, :group_not_found} ->
|
||||
{:error, :group_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@spec parents(String.t(), String.t()) ::
|
||||
{:ok, Resource.t(), Resource.t()}
|
||||
defp parents(old_parent_id, new_parent_id) do
|
||||
{:ok, Resources.get_resource(old_parent_id), Resources.get_resource(new_parent_id)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,33 +18,32 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
|
||||
| {:error, :group_not_found | Ecto.Changeset.t()}
|
||||
def create(args, additional) do
|
||||
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
|
||||
todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group})
|
||||
audience = %{"to" => [group.members_url], "cc" => []}
|
||||
create_data = make_create_data(todo_list_as_data, Map.merge(audience, additional))
|
||||
{:ok, todo_list, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), ActivityStream.t()} | any
|
||||
@spec update(TodoList.t(), map, map) ::
|
||||
{:ok, TodoList.t(), ActivityStream.t()}
|
||||
| {:error, Ecto.Changeset.t() | :group_not_found}
|
||||
def update(%TodoList{} = old_todo_list, args, additional) do
|
||||
with {:ok, %TodoList{actor_id: group_id} = todo_list} <-
|
||||
Todos.update_todo_list(old_todo_list, args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_list_as_data <-
|
||||
Convertible.model_to_as(%{todo_list | actor: group}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
update_data <-
|
||||
make_update_data(todo_list_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
|
||||
todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group})
|
||||
audience = %{"to" => [group.members_url], "cc" => []}
|
||||
update_data = make_update_data(todo_list_as_data, Map.merge(audience, additional))
|
||||
{:ok, todo_list, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(TodoList.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), TodoList.t()}
|
||||
{:ok, ActivityStream.t(), Actor.t(), TodoList.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
|
||||
%Actor{url: actor_url} = actor,
|
||||
@@ -61,9 +60,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
|
||||
"to" => [group_url]
|
||||
}
|
||||
|
||||
with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do
|
||||
{:ok, activity_data, actor, todo_list}
|
||||
case Todos.delete_todo_list(todo_list) do
|
||||
{:ok, _todo_list} ->
|
||||
Cachex.del(:activity_pub, "todo_list_#{todo_list.id}")
|
||||
{:ok, activity_data, actor, todo_list}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
||||
@moduledoc false
|
||||
@moduledoc """
|
||||
ActivityPub type handler for Todos
|
||||
"""
|
||||
alias Mobilizon.{Actors, Todos}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Permission
|
||||
@@ -13,41 +15,75 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, Todo.t(), ActivityStream.t()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Todo.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
def create(args, additional) do
|
||||
with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
|
||||
Todos.create_todo(args),
|
||||
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
|
||||
%Actor{} = creator <- Actors.get_actor(creator_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator},
|
||||
todo_as_data <-
|
||||
Convertible.model_to_as(todo),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(todo_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %Actor{} = creator, %TodoList{} = todo_list, %Actor{} = group} <-
|
||||
creator_todo_list_and_group(creator_id, todo_list_id) do
|
||||
todo = %{todo | todo_list: %{todo_list | actor: group}, creator: creator}
|
||||
todo_as_data = Convertible.model_to_as(todo)
|
||||
audience = %{"to" => [group.members_url], "cc" => []}
|
||||
create_data = make_create_data(todo_as_data, Map.merge(audience, additional))
|
||||
{:ok, todo, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Todo.t(), map, map) :: {:ok, Todo.t(), ActivityStream.t()}
|
||||
@spec update(Todo.t(), map, map) ::
|
||||
{:ok, Todo.t(), ActivityStream.t()}
|
||||
| {:error, atom() | Ecto.Changeset.t()}
|
||||
def update(%Todo{} = old_todo, args, additional) do
|
||||
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
|
||||
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_as_data <-
|
||||
Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
update_data <-
|
||||
make_update_data(todo_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %TodoList{} = todo_list, %Actor{} = group} <- todo_list_and_group(todo_list_id) do
|
||||
todo_as_data = Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}})
|
||||
audience = %{"to" => [group.members_url], "cc" => []}
|
||||
update_data = make_update_data(todo_as_data, Map.merge(audience, additional))
|
||||
{:ok, todo, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec creator_todo_list_and_group(integer(), String.t()) ::
|
||||
{:ok, Actor.t(), TodoList.t(), Actor.t()}
|
||||
| {:error, :creator_not_found | :group_not_found | :todo_list_not_found}
|
||||
defp creator_todo_list_and_group(creator_id, todo_list_id) do
|
||||
case Actors.get_actor(creator_id) do
|
||||
%Actor{} = creator ->
|
||||
case todo_list_and_group(todo_list_id) do
|
||||
{:ok, %TodoList{} = todo_list, %Actor{} = group} ->
|
||||
{:ok, creator, todo_list, group}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :creator_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@spec todo_list_and_group(String.t()) ::
|
||||
{:ok, TodoList.t(), Actor.t()} | {:error, :group_not_found | :todo_list_not_found}
|
||||
defp todo_list_and_group(todo_list_id) do
|
||||
case Todos.get_todo_list(todo_list_id) do
|
||||
%TodoList{actor_id: group_id} = todo_list ->
|
||||
case Actors.get_group_by_actor_id(group_id) do
|
||||
{:ok, %Actor{} = group} ->
|
||||
{:ok, todo_list, group}
|
||||
|
||||
{:error, :group_not_found} ->
|
||||
{:error, :group_not_found}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :todo_list_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Todo.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Todo.t()}
|
||||
@spec delete(Todo.t(), Actor.t(), any(), any()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Todo.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%Todo{url: url, creator: %Actor{url: group_url}} = todo,
|
||||
%Actor{url: actor_url} = actor,
|
||||
@@ -60,13 +96,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
||||
"actor" => actor_url,
|
||||
"type" => "Delete",
|
||||
"object" => Convertible.model_to_as(url),
|
||||
"id" => url <> "/delete",
|
||||
"id" => "#{url}/delete",
|
||||
"to" => [group_url]
|
||||
}
|
||||
|
||||
with {:ok, _todo} <- Todos.delete_todo(todo),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do
|
||||
{:ok, activity_data, actor, todo}
|
||||
case Todos.delete_todo(todo) do
|
||||
{:ok, _todo} ->
|
||||
Cachex.del(:activity_pub, "todo_#{todo.id}")
|
||||
{:ok, activity_data, actor, todo}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,7 +124,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
||||
end
|
||||
end
|
||||
|
||||
@spec permissions(TodoList.t()) :: Permission.t()
|
||||
@spec permissions(Todo.t()) :: Permission.t()
|
||||
def permissions(%Todo{}) do
|
||||
%Permission{access: :member, create: :member, update: :member, delete: :member}
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Permission
|
||||
|
||||
@spec actor(Tombstone.t()) :: Actor.t() | nil
|
||||
def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id)
|
||||
|
||||
def actor(%Tombstone{actor_id: actor_id}) when not is_nil(actor_id),
|
||||
@@ -11,8 +12,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
|
||||
|
||||
def actor(_), do: nil
|
||||
|
||||
@spec group_actor(any()) :: nil
|
||||
def group_actor(_), do: nil
|
||||
|
||||
@spec permissions(any()) :: Permission.t()
|
||||
def permissions(_) do
|
||||
%Permission{access: nil, create: nil, update: nil, delete: nil}
|
||||
end
|
||||
|
||||
@@ -12,8 +12,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Medias.Media
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Federator, Relay}
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityStream.Converter
|
||||
alias Mobilizon.Federation.HTTPSignatures
|
||||
@@ -23,6 +22,26 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
|
||||
@actor_types ["Group", "Person", "Application"]
|
||||
|
||||
# Wraps an object into an activity
|
||||
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
|
||||
def create_activity(map, local) when is_map(map) do
|
||||
with map <- lazy_put_activity_defaults(map) do
|
||||
{:ok,
|
||||
%Activity{
|
||||
data: map,
|
||||
local: local,
|
||||
actor: map["actor"],
|
||||
recipients: get_recipients(map)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
# Get recipients for an activity or object
|
||||
@spec get_recipients(map()) :: list()
|
||||
defp get_recipients(data) do
|
||||
Map.get(data, "to", []) ++ Map.get(data, "cc", [])
|
||||
end
|
||||
|
||||
# Some implementations send the actor URI as the actor field, others send the entire actor object,
|
||||
# so figure out what the actor's URI is based on what we have.
|
||||
@spec get_url(map() | String.t() | list(String.t()) | any()) :: String.t() | nil
|
||||
@@ -149,7 +168,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
%Activity{data: %{"object" => object}},
|
||||
%Actor{url: attributed_to_url}
|
||||
)
|
||||
when is_binary(object) do
|
||||
when is_binary(object) and is_binary(attributed_to_url) do
|
||||
do_maybe_relay_if_group_activity(object, attributed_to_url)
|
||||
end
|
||||
|
||||
@@ -166,7 +185,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
|
||||
case Actors.get_local_group_by_url(attributed_to) do
|
||||
%Actor{} = group ->
|
||||
case ActivityPub.announce(group, object, id, true, false) do
|
||||
case Actions.Announce.announce(group, object, id, true, false) do
|
||||
{:ok, _activity, _object} ->
|
||||
Logger.info("Forwarded activity to external members of the group")
|
||||
:ok
|
||||
@@ -564,6 +583,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
@doc """
|
||||
Converts PEM encoded keys to a public key representation
|
||||
"""
|
||||
@spec pem_to_public_key(String.t()) :: {:RSAPublicKey, any(), any()}
|
||||
def pem_to_public_key(pem) do
|
||||
[key_code] = :public_key.pem_decode(pem)
|
||||
key = :public_key.pem_entry_decode(key_code)
|
||||
@@ -577,6 +597,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
end
|
||||
end
|
||||
|
||||
@spec pem_to_public_key_pem(String.t()) :: String.t()
|
||||
def pem_to_public_key_pem(pem) do
|
||||
public_key = pem_to_public_key(pem)
|
||||
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
|
||||
|
||||
@@ -3,5 +3,5 @@ defmodule Mobilizon.Federation.ActivityStream do
|
||||
The ActivityStream Type
|
||||
"""
|
||||
|
||||
@type t :: map()
|
||||
@type t :: %{String.t() => String.t() | list(String.t()) | map() | nil}
|
||||
end
|
||||
|
||||
@@ -46,7 +46,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
|
||||
end
|
||||
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: map() | {:error, any()}
|
||||
@spec as_to_model_data(map) :: map() | {:error, atom()}
|
||||
def as_to_model_data(%{"type" => "Note", "name" => name} = object) when is_valid_string(name) do
|
||||
case extract_actors(object) do
|
||||
%{actor_id: actor_id, creator_id: creator_id} ->
|
||||
@@ -57,7 +57,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
|
||||
end
|
||||
end
|
||||
|
||||
@spec extract_actors(map()) :: %{actor_id: String.t(), creator_id: String.t()} | {:error, any()}
|
||||
@spec extract_actors(map()) ::
|
||||
%{actor_id: String.t(), creator_id: String.t()} | {:error, atom()}
|
||||
defp extract_actors(%{"actor" => creator_url, "attributedTo" => actor_url} = _object)
|
||||
when is_valid_string(creator_url) and is_valid_string(actor_url) do
|
||||
with {:ok, %Actor{id: creator_id, suspended: false}} <-
|
||||
|
||||
@@ -45,50 +45,51 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: map() | {:error, any()} | :error
|
||||
@spec as_to_model_data(map) :: map() | {:error, atom()}
|
||||
def as_to_model_data(object) do
|
||||
with {:ok, %Actor{id: actor_id}, attributed_to} <-
|
||||
maybe_fetch_actor_and_attributed_to_id(object),
|
||||
{:address, address_id} <-
|
||||
{:address, get_address(object["location"])},
|
||||
{:tags, tags} <- {:tags, fetch_tags(object["tag"])},
|
||||
{:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])},
|
||||
{:visibility, visibility} <- {:visibility, get_visibility(object)},
|
||||
{:options, options} <- {:options, get_options(object)},
|
||||
{:metadata, metadata} <- {:metadata, get_metdata(object)},
|
||||
[description: description, picture_id: picture_id, medias: medias] <-
|
||||
process_pictures(object, actor_id) do
|
||||
%{
|
||||
title: object["name"],
|
||||
description: description,
|
||||
organizer_actor_id: actor_id,
|
||||
attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
|
||||
picture_id: picture_id,
|
||||
medias: medias,
|
||||
begins_on: object["startTime"],
|
||||
ends_on: object["endTime"],
|
||||
category: object["category"],
|
||||
visibility: visibility,
|
||||
join_options: Map.get(object, "joinMode", "free"),
|
||||
local: is_local(object["id"]),
|
||||
options: options,
|
||||
metadata: metadata,
|
||||
status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(),
|
||||
online_address: object |> Map.get("attachment", []) |> get_online_address(),
|
||||
phone_address: object["phoneAddress"],
|
||||
draft: object["draft"] == true,
|
||||
url: object["id"],
|
||||
uuid: object["uuid"],
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
physical_address_id: address_id,
|
||||
updated_at: object["updated"],
|
||||
publish_at: object["published"],
|
||||
language: object["inLanguage"]
|
||||
}
|
||||
else
|
||||
{:error, _err} ->
|
||||
:error
|
||||
case maybe_fetch_actor_and_attributed_to_id(object) do
|
||||
{:ok, %Actor{id: actor_id}, attributed_to} ->
|
||||
address_id = get_address(object["location"])
|
||||
tags = fetch_tags(object["tag"])
|
||||
mentions = fetch_mentions(object["tag"])
|
||||
visibility = get_visibility(object)
|
||||
options = get_options(object)
|
||||
metadata = get_metdata(object)
|
||||
|
||||
[description: description, picture_id: picture_id, medias: medias] =
|
||||
process_pictures(object, actor_id)
|
||||
|
||||
%{
|
||||
title: object["name"],
|
||||
description: description,
|
||||
organizer_actor_id: actor_id,
|
||||
attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
|
||||
picture_id: picture_id,
|
||||
medias: medias,
|
||||
begins_on: object["startTime"],
|
||||
ends_on: object["endTime"],
|
||||
category: object["category"],
|
||||
visibility: visibility,
|
||||
join_options: Map.get(object, "joinMode", "free"),
|
||||
local: is_local(object["id"]),
|
||||
options: options,
|
||||
metadata: metadata,
|
||||
status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(),
|
||||
online_address: object |> Map.get("attachment", []) |> get_online_address(),
|
||||
phone_address: object["phoneAddress"],
|
||||
draft: object["draft"] == true,
|
||||
url: object["id"],
|
||||
uuid: object["uuid"],
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
physical_address_id: address_id,
|
||||
updated_at: object["updated"],
|
||||
publish_at: object["published"],
|
||||
language: object["inLanguage"]
|
||||
}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Events.EventMetadata
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
|
||||
@property_value "PropertyValue"
|
||||
|
||||
@spec metadata_to_as(EventMetadata.t()) :: map()
|
||||
def metadata_to_as(%EventMetadata{type: :boolean, value: value, key: key})
|
||||
when value in ["true", "false"] do
|
||||
%{
|
||||
@@ -47,6 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
|
||||
)
|
||||
end
|
||||
|
||||
@spec as_to_metadata(ActivityStream.t()) :: map()
|
||||
def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value})
|
||||
when is_boolean(value) do
|
||||
%{type: :boolean, key: key, value: to_string(value)}
|
||||
|
||||
@@ -66,6 +66,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
|
||||
|
||||
nil ->
|
||||
case ActivityPub.fetch_object_from_url(todo_list_url) do
|
||||
{:ok, _, %TodoList{}} ->
|
||||
as_to_model_data(object)
|
||||
|
||||
{:ok, %TodoList{}} ->
|
||||
as_to_model_data(object)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user