Browse Source

Refactor Mobilizon.Federation.ActivityPub and add typespecs

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
tags/2.0.0-beta.1
Thomas Citharel 1 year ago
parent
commit
b5d9b82bdd
No known key found for this signature in database GPG Key ID: A061B9DDE0CA0773
100 changed files with 2336 additions and 1625 deletions
  1. +4
    -4
      .doctor.exs
  2. +2
    -0
      lib/config_provider.ex
  3. +169
    -0
      lib/federation/activity_pub/actions/accept.ex
  4. +60
    -0
      lib/federation/activity_pub/actions/announce.ex
  5. +71
    -0
      lib/federation/activity_pub/actions/create.ex
  6. +33
    -0
      lib/federation/activity_pub/actions/delete.ex
  7. +31
    -0
      lib/federation/activity_pub/actions/flag.ex
  8. +74
    -0
      lib/federation/activity_pub/actions/follow.ex
  9. +86
    -0
      lib/federation/activity_pub/actions/invite.ex
  10. +51
    -0
      lib/federation/activity_pub/actions/join.ex
  11. +104
    -0
      lib/federation/activity_pub/actions/leave.ex
  12. +30
    -0
      lib/federation/activity_pub/actions/move.ex
  13. +127
    -0
      lib/federation/activity_pub/actions/reject.ex
  14. +56
    -0
      lib/federation/activity_pub/actions/remove.ex
  15. +44
    -0
      lib/federation/activity_pub/actions/update.ex
  16. +31
    -877
      lib/federation/activity_pub/activity_pub.ex
  17. +21
    -24
      lib/federation/activity_pub/audience.ex
  18. +12
    -2
      lib/federation/activity_pub/federator.ex
  19. +49
    -43
      lib/federation/activity_pub/fetcher.ex
  20. +128
    -0
      lib/federation/activity_pub/publisher.ex
  21. +3
    -4
      lib/federation/activity_pub/relay.ex
  22. +46
    -35
      lib/federation/activity_pub/transmogrifier.ex
  23. +4
    -5
      lib/federation/activity_pub/types/actors.ex
  24. +6
    -2
      lib/federation/activity_pub/types/comments.ex
  25. +82
    -66
      lib/federation/activity_pub/types/discussions.ex
  26. +25
    -23
      lib/federation/activity_pub/types/entity.ex
  27. +10
    -11
      lib/federation/activity_pub/types/events.ex
  28. +3
    -2
      lib/federation/activity_pub/types/members.ex
  29. +36
    -29
      lib/federation/activity_pub/types/reports.ex
  30. +97
    -68
      lib/federation/activity_pub/types/resources.ex
  31. +19
    -16
      lib/federation/activity_pub/types/todo_lists.ex
  32. +66
    -26
      lib/federation/activity_pub/types/todos.ex
  33. +3
    -0
      lib/federation/activity_pub/types/tombstones.ex
  34. +25
    -4
      lib/federation/activity_pub/utils.ex
  35. +1
    -1
      lib/federation/activity_stream.ex
  36. +3
    -2
      lib/federation/activity_stream/converter/discussion.ex
  37. +44
    -43
      lib/federation/activity_stream/converter/event.ex
  38. +3
    -0
      lib/federation/activity_stream/converter/event_metadata.ex
  39. +3
    -0
      lib/federation/activity_stream/converter/todo.ex
  40. +5
    -6
      lib/graphql/api/comments.ex
  41. +4
    -5
      lib/graphql/api/events.ex
  42. +9
    -9
      lib/graphql/api/follows.ex
  43. +19
    -23
      lib/graphql/api/groups.ex
  44. +16
    -14
      lib/graphql/api/participations.ex
  45. +39
    -43
      lib/graphql/api/reports.ex
  46. +3
    -0
      lib/graphql/resolvers/activity.ex
  47. +26
    -20
      lib/graphql/resolvers/actor.ex
  48. +32
    -9
      lib/graphql/resolvers/admin.ex
  49. +50
    -26
      lib/graphql/resolvers/comment.ex
  50. +6
    -1
      lib/graphql/resolvers/config.ex
  51. +47
    -18
      lib/graphql/resolvers/discussion.ex
  52. +22
    -0
      lib/graphql/resolvers/event.ex
  53. +3
    -3
      lib/graphql/resolvers/followers.ex
  54. +73
    -48
      lib/graphql/resolvers/group.ex
  55. +22
    -9
      lib/graphql/resolvers/member.ex
  56. +6
    -0
      lib/graphql/resolvers/participant.ex
  57. +44
    -28
      lib/graphql/resolvers/person.ex
  58. +19
    -13
      lib/graphql/resolvers/post.ex
  59. +4
    -0
      lib/graphql/resolvers/push_subscription.ex
  60. +10
    -0
      lib/graphql/resolvers/report.ex
  61. +37
    -14
      lib/graphql/resolvers/resource.ex
  62. +10
    -1
      lib/graphql/resolvers/search.ex
  63. +1
    -0
      lib/graphql/resolvers/statistics.ex
  64. +6
    -3
      lib/graphql/resolvers/tag.ex
  65. +30
    -7
      lib/graphql/resolvers/todos.ex
  66. +15
    -5
      lib/graphql/resolvers/user.ex
  67. +5
    -1
      lib/graphql/resolvers/users/activity_settings.ex
  68. +1
    -0
      lib/graphql/schema.ex
  69. +11
    -1
      lib/mix/tasks/mobilizon/common.ex
  70. +2
    -1
      lib/mix/tasks/mobilizon/create_bot.ex
  71. +1
    -0
      lib/mix/tasks/mobilizon/site_map.ex
  72. +6
    -0
      lib/mobilizon/activities/activities.ex
  73. +1
    -0
      lib/mobilizon/actors/actor.ex
  74. +2
    -1
      lib/mobilizon/actors/actors.ex
  75. +2
    -0
      lib/mobilizon/actors/member.ex
  76. +2
    -0
      lib/mobilizon/addresses/address.ex
  77. +5
    -2
      lib/mobilizon/cli.ex
  78. +2
    -2
      lib/mobilizon/config.ex
  79. +1
    -0
      lib/mobilizon/discussions/comment.ex
  80. +2
    -0
      lib/mobilizon/discussions/discussion.ex
  81. +22
    -14
      lib/mobilizon/discussions/discussions.ex
  82. +5
    -5
      lib/mobilizon/events/event.ex
  83. +3
    -2
      lib/mobilizon/events/events.ex
  84. +1
    -0
      lib/mobilizon/events/participant.ex
  85. +2
    -0
      lib/mobilizon/posts/post.ex
  86. +1
    -0
      lib/mobilizon/reports/report.ex
  87. +1
    -0
      lib/mobilizon/resources/resource.ex
  88. +1
    -0
      lib/mobilizon/storage/repo.ex
  89. +2
    -0
      lib/mobilizon/todos/todo.ex
  90. +2
    -0
      lib/mobilizon/todos/todo_list.ex
  91. +2
    -2
      lib/mobilizon/users/users.ex
  92. +1
    -1
      lib/service/activity/member.ex
  93. +2
    -0
      lib/service/error_page.ex
  94. +10
    -0
      lib/service/formatter/formatter.ex
  95. +3
    -0
      lib/service/http/activity_pub.ex
  96. +5
    -1
      lib/service/notifications/scheduler.ex
  97. +1
    -0
      lib/service/notifier/notifier.ex
  98. +6
    -0
      lib/service/rich_media/parsers/meta_tags_parser.ex
  99. +1
    -0
      lib/service/rich_media/parsers/oembed_parser.ex
  100. +2
    -0
      lib/service/rich_media/parsers/ogp.ex

+ 4
- 4
.doctor.exs View File

@@ -1,12 +1,12 @@
%Doctor.Config{
exception_moduledoc_required: true,
failed: false,
ignore_modules: [],
ignore_modules: [Mobilizon.Web, Mobilizon.GraphQL.Schema, Mobilizon.Service.Activity.Renderer, Mobilizon.Service.Workers.Helper],
ignore_paths: [],
min_module_doc_coverage: 70,
min_module_doc_coverage: 100,
min_module_spec_coverage: 50,
min_overall_doc_coverage: 90,
min_overall_spec_coverage: 30,
min_overall_doc_coverage: 100,
min_overall_spec_coverage: 90,
moduledoc_required: true,
raise: false,
reporter: Doctor.Reporters.Full,


+ 2
- 0
lib/config_provider.ex View File

@@ -4,8 +4,10 @@ defmodule Mobilizon.ConfigProvider do
"""
@behaviour Config.Provider

@spec init(String.t()) :: String.t()
def init(path) when is_binary(path), do: path

@spec load(Keyword.t(), String.t()) :: Keyword.t()
def load(config, path) do
config_path = System.get_env("MOBILIZON_CONFIG_PATH") || path



+ 169
- 0
lib/federation/activity_pub/actions/accept.ex View 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
- 0
lib/federation/activity_pub/actions/announce.ex View 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
- 0
lib/federation/activity_pub/actions/create.ex View 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
- 0
lib/federation/activity_pub/actions/delete.ex View 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
- 0
lib/federation/activity_pub/actions/flag.ex View 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
- 0
lib/federation/activity_pub/actions/follow.ex View 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
- 0
lib/federation/activity_pub/actions/invite.ex View 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
- 0
lib/federation/activity_pub/actions/join.ex View 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
- 0
lib/federation/activity_pub/actions/leave.ex View 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
- 0
lib/federation/activity_pub/actions/move.ex View 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
- 0
lib/federation/activity_pub/actions/reject.ex View 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
- 0
lib/federation/activity_pub/actions/remove.ex View 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
- 0
lib/federation/activity_pub/actions/update.ex View 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

+ 31
- 877
lib/federation/activity_pub/activity_pub.ex View File

@@ -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)
else
{:existing, entity} ->
handle_existing_entity(url, entity, options)

{:error, e} ->
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
{:error, e}
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)

e ->
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
{:error, e}
{:error, e} ->
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
{:error, e}
end
else
{: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,607 +118,12 @@ 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"}
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)
{:ok, entity}
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 """
@@ -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

+ 21
- 24
lib/federation/activity_pub/audience.ex View File

@@ -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