Refactor Mobilizon.Federation.ActivityPub and add typespecs

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-09-28 19:40:37 +02:00
parent 41f086e2c9
commit b5d9b82bdd
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
125 changed files with 2497 additions and 1673 deletions

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,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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
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()

View File

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

View File

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

View 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

View File

@ -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)}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,8 +5,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Comment
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
alias Mobilizon.GraphQL.API.Utils
@doc """
@ -15,7 +14,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
@spec create_comment(map) :: {:ok, Activity.t(), Comment.t()} | any
def create_comment(args) do
args = extract_pictures_from_comment_body(args)
ActivityPub.create(:comment, args, true)
Actions.Create.create(:comment, args, true)
end
@doc """
@ -24,7 +23,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
@spec update_comment(Comment.t(), map()) :: {:ok, Activity.t(), Comment.t()} | any
def update_comment(%Comment{} = comment, args) do
args = extract_pictures_from_comment_body(args)
ActivityPub.update(comment, args, true)
Actions.Update.update(comment, args, true)
end
@doc """
@ -32,7 +31,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
"""
@spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any
def delete_comment(%Comment{} = comment, %Actor{} = actor) do
ActivityPub.delete(comment, actor, true)
Actions.Delete.delete(comment, actor, true)
end
@doc """
@ -42,7 +41,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
def create_discussion(args) do
args = extract_pictures_from_comment_body(args)
ActivityPub.create(
Actions.Create.create(
:discussion,
args,
true

View File

@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.API.Events do
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Utils}
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
@doc """
@ -16,7 +15,7 @@ defmodule Mobilizon.GraphQL.API.Events do
@spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any
def create_event(args) do
# For now we don't federate drafts but it will be needed if we want to edit them as groups
ActivityPub.create(:event, prepare_args(args), should_federate(args))
Actions.Create.create(:event, prepare_args(args), should_federate(args))
end
@doc """
@ -24,7 +23,7 @@ defmodule Mobilizon.GraphQL.API.Events do
"""
@spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any
def update_event(args, %Event{} = event) do
ActivityPub.update(event, prepare_args(args), should_federate(args))
Actions.Update.update(event, prepare_args(args), should_federate(args))
end
@doc """
@ -32,7 +31,7 @@ defmodule Mobilizon.GraphQL.API.Events do
"""
@spec delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, Activity.t(), Entity.t()} | any()
def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do
ActivityPub.delete(event, actor, federate)
Actions.Delete.delete(event, actor, federate)
end
@spec prepare_args(map) :: map

View File

@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.API.Follows do
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
require Logger
@ -14,27 +14,27 @@ defmodule Mobilizon.GraphQL.API.Follows do
Make an actor (`follower`) follow another (`followed`).
"""
@spec follow(follower :: Actor.t(), followed :: Actor.t()) ::
{:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()}
{:ok, Activity.t(), Mobilizon.Actors.Follower.t()}
| {:error, String.t()}
def follow(%Actor{} = follower, %Actor{} = followed) do
ActivityPub.follow(follower, followed)
Actions.Follow.follow(follower, followed)
end
@doc """
Make an actor (`follower`) unfollow another (`followed`).
"""
@spec unfollow(follower :: Actor.t(), followed :: Actor.t()) ::
{:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()}
{:ok, Activity.t(), Mobilizon.Actors.Follower.t()}
| {:error, String.t()}
def unfollow(%Actor{} = follower, %Actor{} = followed) do
ActivityPub.unfollow(follower, followed)
Actions.Follow.unfollow(follower, followed)
end
@doc """
Make an actor (`followed`) accept the follow from another (`follower`).
"""
@spec accept(follower :: Actor.t(), followed :: Actor.t()) ::
{:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()}
{:ok, Activity.t(), Mobilizon.Actors.Follower.t()}
| {:error, String.t()}
def accept(%Actor{url: follower_url} = follower, %Actor{url: followed_url} = followed) do
Logger.debug(
@ -43,7 +43,7 @@ defmodule Mobilizon.GraphQL.API.Follows do
case Actors.is_following(follower, followed) do
%Follower{approved: false} = follow ->
ActivityPub.accept(
Actions.Accept.accept(
:follow,
follow,
true
@ -61,7 +61,7 @@ defmodule Mobilizon.GraphQL.API.Follows do
Make an actor (`followed`) reject the follow from another (`follower`).
"""
@spec reject(follower :: Actor.t(), followed :: Actor.t()) ::
{:ok, Mobilizon.Federation.ActivityPub.Activity.t(), Mobilizon.Actors.Follower.t()}
{:ok, Activity.t(), Mobilizon.Actors.Follower.t()}
| {:error, String.t()}
def reject(%Actor{url: follower_url} = follower, %Actor{url: followed_url} = followed) do
Logger.debug(
@ -73,7 +73,7 @@ defmodule Mobilizon.GraphQL.API.Follows do
{:error, "Follow already accepted"}
%Follower{} = follow ->
ActivityPub.reject(
Actions.Reject.reject(
:follow,
follow,
true

View File

@ -6,39 +6,35 @@ defmodule Mobilizon.GraphQL.API.Groups do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
alias Mobilizon.Service.Formatter.HTML
@doc """
Create a group
"""
@spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any
@spec create_group(map) ::
{:ok, Activity.t(), Actor.t()}
| {:error, String.t() | Ecto.Changeset.t()}
def create_group(args) do
with preferred_username <-
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
{:existing_group, nil} <-
{:existing_group, Actors.get_local_actor_by_name(preferred_username)},
args <- args |> Map.put(:type, :Group),
{:ok, %Activity{} = activity, %Actor{} = group} <-
ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do
{:ok, activity, group}
else
{:existing_group, _} ->
{:error, "A group with this name already exists"}
preferred_username =
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim()
args = args |> Map.put(:type, :Group)
case Actors.get_local_actor_by_name(preferred_username) do
nil ->
Actions.Create.create(:actor, args, true, %{"actor" => args.creator_actor.url})
%Actor{} ->
{:error, "A profile or group with that name already exists"}
end
end
@spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any
@spec update_group(map) ::
{:ok, Activity.t(), Actor.t()} | {:error, :group_not_found | Ecto.Changeset.t()}
def update_group(%{id: id} = args) do
with {:existing_group, {:ok, %Actor{type: :Group} = group}} <-
{:existing_group, Actors.get_group_by_actor_id(id)},
{:ok, %Activity{} = activity, %Actor{} = group} <-
ActivityPub.update(group, args, true, %{"actor" => args.updater_actor.url}) do
{:ok, activity, group}
else
{:existing_group, _} ->
{:error, "A group with this name already exists"}
with {:ok, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(id) do
Actions.Update.update(group, args, true, %{"actor" => args.updater_actor.url})
end
end
end

View File

@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.API.Participations do
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
alias Mobilizon.Service.Notifications.Scheduler
alias Mobilizon.Web.Email.Participation
@ -19,7 +18,7 @@ defmodule Mobilizon.GraphQL.API.Participations do
{:error, :already_participant}
{:error, :participant_not_found} ->
ActivityPub.join(event, actor, Map.get(args, :local, true), %{metadata: args})
Actions.Join.join(event, actor, Map.get(args, :local, true), %{metadata: args})
end
end
@ -27,7 +26,7 @@ defmodule Mobilizon.GraphQL.API.Participations do
{:ok, Activity.t(), Participant.t()}
| {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()}
def leave(%Event{} = event, %Actor{} = actor, args \\ %{}),
do: ActivityPub.leave(event, actor, Map.get(args, :local, true), %{metadata: args})
do: Actions.Leave.leave(event, actor, Map.get(args, :local, true), %{metadata: args})
@doc """
Update participation status
@ -52,15 +51,18 @@ defmodule Mobilizon.GraphQL.API.Participations do
%Participant{} = participation,
%Actor{} = moderator
) do
with {:ok, activity, %Participant{role: :participant} = participation} <-
ActivityPub.accept(
:join,
participation,
true,
%{"actor" => moderator.url}
),
:ok <- Participation.send_emails_to_local_user(participation) do
{:ok, activity, participation}
case Actions.Accept.accept(
:join,
participation,
true,
%{"actor" => moderator.url}
) do
{:ok, activity, %Participant{role: :participant} = participation} ->
Participation.send_emails_to_local_user(participation)
{:ok, activity, participation}
{:error, err} ->
{:error, err}
end
end
@ -70,7 +72,7 @@ defmodule Mobilizon.GraphQL.API.Participations do
%Actor{} = moderator
) do
with {:ok, activity, %Participant{role: :rejected} = participation} <-
ActivityPub.reject(
Actions.Reject.reject(
:join,
participation,
true,

View File

@ -9,36 +9,29 @@ defmodule Mobilizon.GraphQL.API.Reports do
alias Mobilizon.Reports.{Note, Report, ReportStatus}
alias Mobilizon.Users.User
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
@doc """
Create a report/flag on an actor, and optionally on an event or on comments.
"""
@spec report(map()) :: {:ok, Activity.t(), Report.t()} | {:error, any()}
@spec report(map()) :: {:ok, Activity.t(), Report.t()} | {:error, Ecto.Changeset.t()}
def report(args) do
case ActivityPub.flag(args, Map.get(args, :forward, false) == true) do
{:ok, %Activity{} = activity, %Report{} = report} ->
{:ok, activity, report}
err ->
{:error, err}
end
Actions.Flag.flag(args, Map.get(args, :forward, false) == true)
end
@doc """
Update the state of a report
"""
@spec update_report_status(Actor.t(), Report.t(), ReportStatus.t()) ::
{:ok, Report.t()} | {:error, String.t()}
{:ok, Report.t()} | {:error, Ecto.Changeset.t() | String.t()}
def update_report_status(%Actor{} = actor, %Report{} = report, state) do
with {:valid_state, true} <-
{:valid_state, ReportStatus.valid_value?(state)},
{:ok, %Report{} = report} <- ReportsAction.update_report(report, %{"status" => state}),
{:ok, _} <- Admin.log_action(actor, "update", report) do
{:ok, report}
if ReportStatus.valid_value?(state) do
with {:ok, %Report{} = report} <- ReportsAction.update_report(report, %{"status" => state}) do
Admin.log_action(actor, "update", report)
{:ok, report}
end
else
{:valid_state, false} -> {:error, "Unsupported state"}
{:error, "Unsupported state"}
end
end
@ -46,49 +39,52 @@ defmodule Mobilizon.GraphQL.API.Reports do
Create a note on a report
"""
@spec create_report_note(Report.t(), Actor.t(), String.t()) ::
{:ok, Note.t()} | {:error, String.t()}
{:ok, Note.t()} | {:error, String.t() | Ecto.Changeset.t()}
def create_report_note(
%Report{id: report_id},
%Actor{id: moderator_id, user_id: user_id} = moderator,
content
) do
with %User{role: role} <- Users.get_user!(user_id),
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <-
Mobilizon.Reports.create_note(%{
"report_id" => report_id,
"moderator_id" => moderator_id,
"content" => content
}),
{:ok, _} <- Admin.log_action(moderator, "create", note) do
{:ok, note}
%User{role: role} = Users.get_user!(user_id)
if role in [:administrator, :moderator] do
with {:ok, %Note{} = note} <-
Mobilizon.Reports.create_note(%{
"report_id" => report_id,
"moderator_id" => moderator_id,
"content" => content
}),
{:ok, _} <- Admin.log_action(moderator, "create", note) do
{:ok, note}
end
else
{:role, false} ->
{:error, "You need to be a moderator or an administrator to create a note on a report"}
{:error, "You need to be a moderator or an administrator to create a note on a report"}
end
end
@doc """
Delete a report note
"""
@spec delete_report_note(Note.t(), Actor.t()) :: {:ok, Note.t()} | {:error, String.t()}
@spec delete_report_note(Note.t(), Actor.t()) ::
{:ok, Note.t()} | {:error, Ecto.Changeset.t() | String.t()}
def delete_report_note(
%Note{moderator_id: note_moderator_id} = note,
%Actor{id: moderator_id, user_id: user_id} = moderator
) do
with {:same_actor, true} <- {:same_actor, note_moderator_id == moderator_id},
%User{role: role} <- Users.get_user!(user_id),
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <-
Mobilizon.Reports.delete_note(note),
{:ok, _} <- Admin.log_action(moderator, "delete", note) do
{:ok, note}
else
{:role, false} ->
{:error, "You need to be a moderator or an administrator to create a note on a report"}
if note_moderator_id == moderator_id do
%User{role: role} = Users.get_user!(user_id)
{:same_actor, false} ->
{:error, "You can only remove your own notes"}
if role in [:administrator, :moderator] do
with {:ok, %Note{} = note} <-
Mobilizon.Reports.delete_note(note),
{:ok, _} <- Admin.log_action(moderator, "delete", note) do
{:ok, note}
end
else
{:error, "You need to be a moderator or an administrator to create a note on a report"}
end
else
{:error, "You can only remove your own notes"}
end
end
end

View File

@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
import Mobilizon.Users.Guards
alias Mobilizon.{Activities, Actors}
alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Utils
alias Mobilizon.Storage.Page
@ -12,6 +13,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
require Logger
@spec group_activity(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Activity.t())} | {:error, :unauthorized | :unauthenticated}
def group_activity(%Actor{type: :Group, id: group_id}, %{page: page, limit: limit} = args, %{
context: %{current_user: %User{role: role}, current_actor: %Actor{id: actor_id}}
}) do

View File

@ -6,13 +6,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Admin}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Service.Workers.Background
alias Mobilizon.Users.User
import Mobilizon.Web.Gettext, only: [dgettext: 2]
require Logger
@spec refresh_profile(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def refresh_profile(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
case Actors.get_actor(id) do
@ -31,6 +33,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
end
end
@spec suspend_profile(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def suspend_profile(_parent, %{id: id}, %{
context: %{
current_user: %User{role: role},
@ -39,28 +43,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
})
when is_moderator(role) do
case Actors.get_actor_with_preload(id) do
%Actor{suspended: false} = actor ->
case actor do
# Suspend a group on this instance
%Actor{type: :Group, domain: nil} ->
Logger.debug("We're suspending a group on this very instance")
ActivityPub.delete(actor, moderator_actor, true, %{suspension: true})
Admin.log_action(moderator_actor, "suspend", actor)
{:ok, actor}
# Suspend a group on this instance
%Actor{suspended: false, type: :Group, domain: nil} = actor ->
Logger.debug("We're suspending a group on this very instance")
Actions.Delete.delete(actor, moderator_actor, true, %{suspension: true})
Admin.log_action(moderator_actor, "suspend", actor)
{:ok, actor}
# Delete a remote actor
%Actor{domain: domain} when not is_nil(domain) ->
Logger.debug("We're just deleting a remote instance")
Actors.delete_actor(actor, suspension: true)
Admin.log_action(moderator_actor, "suspend", actor)
{:ok, actor}
# Delete a remote actor
%Actor{suspended: false, domain: domain} = actor when not is_nil(domain) ->
Logger.debug("We're just deleting a remote instance")
Actors.delete_actor(actor, suspension: true)
Admin.log_action(moderator_actor, "suspend", actor)
{:ok, actor}
%Actor{domain: nil} ->
{:error, dgettext("errors", "No remote profile found with this ID")}
end
%Actor{suspended: false, domain: nil} ->
{:error, dgettext("errors", "No remote profile found with this ID")}
%Actor{suspended: true} ->
{:error, dgettext("errors", "Profile already suspended")}
nil ->
{:error, dgettext("errors", "Profile not found")}
end
end
@ -68,6 +72,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
{:error, dgettext("errors", "Only moderators and administrators can suspend a profile")}
end
@spec unsuspend_profile(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def unsuspend_profile(_parent, %{id: id}, %{
context: %{
current_user: %User{role: role},

View File

@ -6,14 +6,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Admin, Config, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Admin.{ActionLog, Setting}
alias Mobilizon.Cldr.Language
alias Mobilizon.Config
alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Federation.ActivityPub.{Actions, Relay}
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Service.Statistics
alias Mobilizon.Storage.Page
@ -21,6 +20,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
import Mobilizon.Web.Gettext
require Logger
@spec list_action_logs(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(ActionLog.t())} | {:error, String.t()}
def list_action_logs(
_parent,
%{page: page, limit: limit},
@ -38,10 +39,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
id: id,
inserted_at: inserted_at
} = action_log ->
with data when is_map(data) <-
transform_action_log(String.to_existing_atom(target_type), action, action_log) do
Map.merge(data, %{actor: actor, id: id, inserted_at: inserted_at})
end
target_type
|> String.to_existing_atom()
|> transform_action_log(action, action_log)
|> Map.merge(%{actor: actor, id: id, inserted_at: inserted_at})
end)
|> Enum.filter(& &1)
@ -53,6 +54,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, dgettext("errors", "You need to be logged-in and a moderator to list action logs")}
end
@spec transform_action_log(module(), atom(), ActionLog.t()) :: map()
defp transform_action_log(
Report,
:update,
@ -123,6 +125,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
@spec convert_changes_to_struct(module(), map()) :: struct()
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
with data <- for({key, val} <- changes, into: %{}, do: {String.to_existing_atom(key), val}),
data <- Map.put(data, :report, Mobilizon.Reports.get_report(data.report_id)) do
@ -143,6 +146,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
# datetimes are not unserialized as DateTime/NaiveDateTime so we do it manually with changeset data
@spec process_eventual_type(Ecto.Changeset.t(), String.t(), String.t() | nil) ::
DateTime.t() | NaiveDateTime.t() | any()
defp process_eventual_type(changeset, key, val) do
cond do
changeset[String.to_existing_atom(key)] == :utc_datetime and not is_nil(val) ->
@ -158,6 +163,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
end
@spec get_list_of_languages(any(), any(), any()) :: {:ok, String.t()} | {:error, any()}
def get_list_of_languages(_parent, %{codes: codes}, _resolution) when is_list(codes) do
locale = Gettext.get_locale()
locale = if Cldr.known_locale_name?(locale), do: locale, else: "en"
@ -187,6 +193,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
end
@spec get_dashboard(any(), any(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, String.t()}
def get_dashboard(_parent, _args, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
last_public_event_published =
@ -225,6 +233,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
)}
end
@spec get_settings(any(), any(), Absinthe.Resolution.t()) :: {:ok, map()} | {:error, String.t()}
def get_settings(_parent, _args, %{
context: %{current_user: %User{role: role}}
})
@ -237,6 +246,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
dgettext("errors", "You need to be logged-in and an administrator to access admin settings")}
end
@spec save_settings(any(), map(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, String.t()}
def save_settings(_parent, args, %{
context: %{current_user: %User{role: role}}
})
@ -261,6 +272,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
dgettext("errors", "You need to be logged-in and an administrator to save admin settings")}
end
@spec list_relay_followers(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Follower.t())} | {:error, :unauthorized | :unauthenticated}
def list_relay_followers(
_parent,
%{page: page, limit: limit},
@ -283,6 +296,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, :unauthenticated}
end
@spec list_relay_followings(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Follower.t())} | {:error, :unauthorized | :unauthenticated}
def list_relay_followings(
_parent,
%{page: page, limit: limit},
@ -305,6 +320,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, :unauthenticated}
end
@spec create_relay(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Follower.t()} | {:error, any()}
def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
case Relay.follow(address) do
@ -316,6 +333,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
end
@spec remove_relay(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Follower.t()} | {:error, any()}
def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do
case Relay.unfollow(address) do
@ -327,6 +346,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
end
@spec accept_subscription(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Follower.t()} | {:error, any()}
def accept_subscription(
_parent,
%{address: address},
@ -342,6 +363,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
end
@spec reject_subscription(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Follower.t()} | {:error, any()}
def reject_subscription(
_parent,
%{address: address},
@ -357,7 +380,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end
end
@spec eventually_update_instance_actor(map()) :: :ok
@spec eventually_update_instance_actor(map()) :: :ok | {:error, :instance_actor_update_failure}
defp eventually_update_instance_actor(admin_setting_args) do
args = %{}
new_instance_description = Map.get(admin_setting_args, :instance_description)
@ -382,7 +405,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
if args != %{} do
%Actor{} = instance_actor = Relay.get_actor()
case ActivityPub.update(instance_actor, args, true) do
case Actions.Update.update(instance_actor, args, true) do
{:ok, _activity, _actor} ->
:ok

View File

@ -14,10 +14,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
require Logger
@spec get_thread(any(), map(), Absinthe.Resolution.t()) :: {:ok, [CommentModel.t()]}
def get_thread(_parent, %{id: thread_id}, _context) do
{:ok, Discussions.get_thread_replies(thread_id)}
end
@spec create_comment(any(), map(), Absinthe.Resolution.t()) ::
{:ok, CommentModel.t()} | {:error, :unauthorized | :not_found | any() | String.t()}
def create_comment(
_parent,
%{event_id: event_id} = args,
@ -27,25 +30,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
}
}
) do
with {:find_event,
{:ok,
%Event{
options: %EventOptions{comment_moderation: comment_moderation},
organizer_actor_id: organizer_actor_id
}}} <-
{:find_event, Events.get_event(event_id)},
{:allowed, true} <-
{:allowed, comment_moderation != :closed || actor_id == organizer_actor_id},
args <- Map.put(args, :actor_id, actor_id),
{:ok, _, %CommentModel{} = comment} <-
Comments.create_comment(args) do
{:ok, comment}
else
{:error, err} ->
{:error, err}
case Events.get_event(event_id) do
{:ok,
%Event{
options: %EventOptions{comment_moderation: comment_moderation},
organizer_actor_id: organizer_actor_id
}} ->
if comment_moderation != :closed || actor_id == organizer_actor_id do
args = Map.put(args, :actor_id, actor_id)
{:allowed, false} ->
{:error, :unauthorized}
case Comments.create_comment(args) do
{:ok, _, %CommentModel{} = comment} ->
{:ok, comment}
{:error, err} ->
{:error, err}
end
else
{:error, :unauthorized}
end
{:error, :event_not_found} ->
{:error, :not_found}
end
end
@ -53,6 +59,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
{:error, dgettext("errors", "You are not allowed to create a comment if not connected")}
end
@spec update_comment(any(), map(), Absinthe.Resolution.t()) ::
{:ok, CommentModel.t()} | {:error, :unauthorized | :not_found | any() | String.t()}
def update_comment(
_parent,
%{text: text, comment_id: comment_id},
@ -62,11 +70,22 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
}
}
) do
with %CommentModel{actor_id: comment_actor_id} = comment <-
Mobilizon.Discussions.get_comment_with_preload(comment_id),
true <- actor_id == comment_actor_id,
{:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do
{:ok, comment}
case Mobilizon.Discussions.get_comment_with_preload(comment_id) do
%CommentModel{actor_id: comment_actor_id} = comment ->
if actor_id == comment_actor_id do
case Comments.update_comment(comment, %{text: text}) do
{:ok, _, %CommentModel{} = comment} ->
{:ok, comment}
{:error, err} ->
{:error, err}
end
else
{:error, dgettext("errors", "You are not the comment creator")}
end
nil ->
{:error, :not_found}
end
end
@ -114,10 +133,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
{:error, dgettext("errors", "You are not allowed to delete a comment if not connected")}
end
@spec do_delete_comment(CommentModel.t(), Actor.t()) ::
{:ok, CommentModel.t()} | {:error, any()}
defp do_delete_comment(%CommentModel{} = comment, %Actor{} = actor) do
with {:ok, _, %CommentModel{} = comment} <-
Comments.delete_comment(comment, actor) do
{:ok, comment}
case Comments.delete_comment(comment, actor) do
{:ok, _, %CommentModel{} = comment} ->
{:ok, comment}
{:error, err} ->
{:error, err}
end
end
end

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
@doc """
Gets config.
"""
@spec get_config(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def get_config(_parent, _params, %{context: %{ip: ip}}) do
geolix = Geolix.lookup(ip)
@ -28,6 +29,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
{:ok, data}
end
@spec terms(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def terms(_parent, %{locale: locale}, _resolution) do
type = Config.instance_terms_type()
@ -41,6 +43,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
{:ok, %{body_html: body_html, type: type, url: url}}
end
@spec privacy(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def privacy(_parent, %{locale: locale}, _resolution) do
type = Config.instance_privacy_type()
@ -54,6 +57,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
{:ok, %{body_html: body_html, type: type, url: url}}
end
@spec config_cache :: map()
defp config_cache do
case Cachex.fetch(:config, "full_config", fn _key ->
case build_config_cache() do
@ -62,10 +66,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
end
end) do
{status, value} when status in [:ok, :commit] -> value
_err -> nil
_err -> %{}
end
end
@spec build_config_cache :: map()
defp build_config_cache do
%{
name: Config.instance_name(),

View File

@ -6,12 +6,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
alias Mobilizon.{Actors, Discussions}
alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.GraphQL.API.Comments
alias Mobilizon.Storage.Page
alias Mobilizon.Users.User
import Mobilizon.Web.Gettext
@spec find_discussions_for_actor(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Discussion.t())} | {:error, :unauthenticated}
def find_discussions_for_actor(
%Actor{id: group_id},
%{page: page, limit: limit},
@ -30,19 +32,33 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
end
end
def find_discussions_for_actor(%Actor{}, _args, _resolution) do
def find_discussions_for_actor(%Actor{}, _args, %{
context: %{
current_user: %User{}
}
}) do
{:ok, %Page{total: 0, elements: []}}
end
def find_discussions_for_actor(%Actor{}, _args, _resolution), do: {:error, :unauthenticated}
@spec get_discussion(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Discussion.t()} | {:error, :unauthorized | :discussion_not_found | String.t()}
def get_discussion(_parent, %{id: id}, %{
context: %{
current_actor: %Actor{id: creator_id}
}
}) do
with %Discussion{actor_id: actor_id} = discussion <-
Discussions.get_discussion(id),
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
{:ok, discussion}
case Discussions.get_discussion(id) do
%Discussion{actor_id: actor_id} = discussion ->
if Actors.is_member?(creator_id, actor_id) do
{:ok, discussion}
else
{:error, :unauthorized}
end
nil ->
{:error, :discussion_not_found}
end
end
@ -73,6 +89,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
def get_discussion(_parent, _args, _resolution),
do: {:error, dgettext("errors", "You need to be logged-in to access discussions")}
@spec get_comments_for_discussion(Discussion.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Discussion.t())}
def get_comments_for_discussion(
%Discussion{id: discussion_id},
%{page: page, limit: limit},
@ -81,6 +99,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
{:ok, Discussions.get_comments_for_discussion(discussion_id, page, limit)}
end
@spec create_discussion(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Discussion.t()}
| {:error, Ecto.Changeset.t() | String.t() | :unauthorized | :unauthenticated}
def create_discussion(
_parent,
%{title: title, text: text, actor_id: group_id},
@ -90,27 +111,32 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
}
}
) do
with {:member, true} <- {:member, Actors.is_member?(creator_id, group_id)},
{:ok, _activity, %Discussion{} = discussion} <-
Comments.create_discussion(%{
if Actors.is_member?(creator_id, group_id) do
case Comments.create_discussion(%{
title: title,
text: text,
actor_id: group_id,
creator_id: creator_id,
attributed_to_id: group_id
}) do
{:ok, discussion}
else
{:error, type, err, _} when type in [:discussion, :comment] ->
{:error, err}
{:ok, _activity, %Discussion{} = discussion} ->
{:ok, discussion}
{:member, false} ->
{:error, :unauthorized}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
{:error, _err} ->
{:error, dgettext("errors", "Error while creating a discussion")}
end
else
{:error, :unauthorized}
end
end
def create_discussion(_, _, _), do: {:error, :unauthenticated}
@spec reply_to_discussion(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Discussion.t()} | {:error, :discussion_not_found | :unauthenticated}
def reply_to_discussion(
_parent,
%{text: text, discussion_id: discussion_id},
@ -150,7 +176,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
def reply_to_discussion(_, _, _), do: {:error, :unauthenticated}
@spec update_discussion(map(), map(), map()) :: {:ok, Discussion.t()}
@spec update_discussion(map(), map(), map()) ::
{:ok, Discussion.t()} | {:error, :unauthorized | :unauthenticated}
def update_discussion(
_parent,
%{title: title, discussion_id: discussion_id},
@ -164,7 +191,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
{:no_discussion, Discussions.get_discussion(discussion_id)},
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
{:ok, _activity, %Discussion{} = discussion} <-
ActivityPub.update(
Actions.Update.update(
discussion,
%{
title: title
@ -179,6 +206,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
def update_discussion(_, _, _), do: {:error, :unauthenticated}
@spec delete_discussion(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Discussion.t()} | {:error, String.t() | :unauthorized | :unauthenticated}
def delete_discussion(_parent, %{discussion_id: discussion_id}, %{
context: %{
current_user: %User{},
@ -189,7 +218,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
{:no_discussion, Discussions.get_discussion(discussion_id)},
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
{:ok, _activity, %Discussion{} = discussion} <-
ActivityPub.delete(discussion, actor) do
Actions.Delete.delete(discussion, actor) do
{:ok, discussion}
else
{:no_discussion, _} ->

View File

@ -21,6 +21,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@event_max_limit 100
@number_of_related_events 3
@spec organizer_for_event(Event.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t() | nil} | {:error, String.t()}
def organizer_for_event(
%Event{attributed_to_id: attributed_to_id, organizer_actor_id: organizer_actor_id},
_args,
@ -62,6 +64,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
end
end
@spec list_events(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Event.t())} | {:error, :events_max_limit_reached}
def list_events(
_parent,
%{page: page, limit: limit, order_by: order_by, direction: direction},
@ -75,6 +79,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:error, :events_max_limit_reached}
end
@spec find_private_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, :event_not_found}
defp find_private_event(
_parent,
%{uuid: uuid},
@ -106,6 +112,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:error, :event_not_found}
end
@spec find_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, :event_not_found}
def find_event(parent, %{uuid: uuid} = args, %{context: context} = resolution) do
with {:has_event, %Event{} = event} <-
{:has_event, Events.get_public_event_by_uuid_with_preload(uuid)},
@ -132,6 +140,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@doc """
List participants for event (through an event request)
"""
@spec list_participants_for_event(Event.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Participant.t())} | {:error, String.t()}
def list_participants_for_event(
%Event{id: event_id} = event,
%{page: page, limit: limit, roles: roles},
@ -166,6 +176,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:ok, %{total: 0, elements: []}}
end
@spec stats_participants(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def stats_participants(
%Event{participant_stats: %EventParticipantStats{} = stats, id: event_id} = _event,
_args,
@ -198,6 +209,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@doc """
List related events
"""
@spec list_related_events(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Event.t())}
def list_related_events(
%Event{tags: tags, organizer_actor: organizer_actor, uuid: uuid},
_args,
@ -239,11 +251,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:ok, events}
end
@spec uniq_events(list(Event.t())) :: list(Event.t())
defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end)
@doc """
Create an event
"""
@spec create_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
def create_event(
_parent,
%{organizer_actor_id: organizer_actor_id} = args,
@ -283,6 +298,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@doc """
Update an event
"""
@spec update_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
def update_event(
_parent,
%{event_id: event_id} = args,
@ -327,6 +344,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@doc """
Delete an event
"""
@spec delete_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
def delete_event(
_parent,
%{event_id: event_id},
@ -365,6 +384,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:error, dgettext("errors", "You need to be logged-in to delete an event")}
end
@spec do_delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, map()}
defp do_delete_event(%Event{} = event, %Actor{} = actor, federate \\ true)
when is_boolean(federate) do
with {:ok, _activity, event} <- API.Events.delete_event(event, actor) do
@ -372,6 +392,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
end
end
@spec is_organizer_group_member?(map()) :: boolean()
defp is_organizer_group_member?(%{
attributed_to_id: attributed_to_id,
organizer_actor_id: organizer_actor_id
@ -383,6 +404,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
defp is_organizer_group_member?(_), do: true
@spec verify_profile_change(map(), Event.t(), User.t(), Actor.t()) :: {:ok, map()}
defp verify_profile_change(
args,
%Event{attributed_to: %Actor{}},

View File

@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
import Mobilizon.Users.Guards
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Storage.Page
alias Mobilizon.Users.User
@ -43,9 +43,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
{:member, Actors.is_moderator?(actor_id, group_id)},
{:ok, _activity, %Follower{} = follower} <-
(if approved do
ActivityPub.accept(:follow, follower)
Actions.Accept.accept(:follow, follower)
else
ActivityPub.reject(:follow, follower)
Actions.Reject.reject(:follow, follower)
end) do
{:ok, follower}
else

View File

@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Events}
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.GraphQL.API
alias Mobilizon.Users.User
@ -15,6 +15,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
require Logger
@spec find_group(
any,
%{:preferred_username => binary, optional(any) => any},
Absinthe.Resolution.t()
) ::
{:error, :group_not_found} | {:ok, Actor.t()}
@doc """
Find a group
"""
@ -27,29 +33,26 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
}
}
) do
with {:group, {:ok, %Actor{id: group_id, suspended: false} = group}} <-
{:group, ActivityPubActor.find_or_make_group_from_nickname(name)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
{:ok, group}
else
{:member, false} ->
find_group(parent, args, nil)
case ActivityPubActor.find_or_make_group_from_nickname(name) do
{:ok, %Actor{id: group_id, suspended: false} = group} ->
if Actors.is_member?(actor_id, group_id) do
{:ok, group}
else
find_group(parent, args, nil)
end
{:group, _} ->
{:error, _err} ->
{:error, :group_not_found}
_ ->
{:error, :unknown}
end
end
def find_group(_parent, %{preferred_username: name}, _resolution) do
with {:ok, %Actor{suspended: false} = actor} <-
ActivityPubActor.find_or_make_group_from_nickname(name),
%Actor{} = actor <- restrict_fields_for_non_member_request(actor) do
{:ok, actor}
else
_ ->
case ActivityPubActor.find_or_make_group_from_nickname(name) do
{:ok, %Actor{suspended: false} = actor} ->
%Actor{} = actor = restrict_fields_for_non_member_request(actor)
{:ok, actor}
{:error, _err} ->
{:error, :group_not_found}
end
end
@ -57,13 +60,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """
Get a group
"""
@spec get_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def get_group(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
with %Actor{type: :Group, suspended: suspended} = actor <-
Actors.get_actor_with_preload(id, true),
true <- suspended == false or is_moderator(role) do
{:ok, actor}
else
_ ->
case Actors.get_actor_with_preload(id, true) do
%Actor{type: :Group, suspended: suspended} = actor ->
if suspended == false or is_moderator(role) do
{:ok, actor}
else
{:error, dgettext("errors", "Group with ID %{id} not found", id: id)}
end
nil ->
{:error, dgettext("errors", "Group with ID %{id} not found", id: id)}
end
end
@ -71,6 +79,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """
Lists all groups
"""
@spec list_groups(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Actor.t())} | {:error, String.t()}
def list_groups(
_parent,
%{
@ -95,6 +105,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
do: {:error, dgettext("errors", "You may not list groups unless moderator.")}
# TODO Move me to somewhere cleaner
@spec save_attached_pictures(map()) :: map()
defp save_attached_pictures(args) do
Enum.reduce([:avatar, :banner], args, fn key, args ->
if is_map(args) && Map.has_key?(args, key) && !is_nil(args[key][:media]) do
@ -113,6 +124,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """
Create a new group. The creator is automatically added as admin
"""
@spec create_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def create_group(
_parent,
args,
@ -145,6 +158,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """
Update a group. The creator is automatically added as admin
"""
@spec update_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def update_group(
_parent,
%{id: group_id} = args,
@ -154,22 +169,24 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
}
}
) do
with {:administrator, true} <-
{:administrator, Actors.is_administrator?(updater_actor.id, group_id)},
args when is_map(args) <- Map.put(args, :updater_actor, updater_actor),
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
{:ok, _activity, %Actor{type: :Group} = group} <-
API.Groups.update_group(args) do
{:ok, group}
if Actors.is_administrator?(updater_actor.id, group_id) do
args = Map.put(args, :updater_actor, updater_actor)
case save_attached_pictures(args) do
{:error, :file_too_large} ->
{:error, dgettext("errors", "The provided picture is too heavy")}
map when is_map(map) ->
case API.Groups.update_group(args) do
{:ok, _activity, %Actor{type: :Group} = group} ->
{:ok, group}
{:error, _err} ->
{:error, dgettext("errors", "Failed to update the group")}
end
end
else
{:picture, {:error, :file_too_large}} ->
{:error, dgettext("errors", "The provided picture is too heavy")}
{:error, err} when is_binary(err) ->
{:error, err}
{:administrator, false} ->
{:error, dgettext("errors", "Profile is not administrator for the group")}
{:error, dgettext("errors", "Profile is not administrator for the group")}
end
end
@ -180,6 +197,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """
Delete an existing group
"""
@spec delete_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, %{id: integer()}} | {:error, String.t()}
def delete_group(
_parent,
%{group_id: group_id},
@ -192,7 +211,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- {:is_admin, Member.is_administrator(member)},
{:ok, _activity, group} <- ActivityPub.delete(group, actor, true) do
{:ok, _activity, group} <- Actions.Delete.delete(group, actor, true) do
{:ok, %{id: group.id}}
else
{:error, :group_not_found} ->
@ -214,6 +233,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """
Join an existing group
"""
@spec join_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def join_group(_parent, %{group_id: group_id} = args, %{
context: %{current_actor: %Actor{} = actor}
}) do
@ -222,7 +243,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
{:ok, _activity, %Member{} = member} <-
ActivityPub.join(group, actor, true, args) do
Actions.Join.join(group, actor, true, args) do
{:ok, member}
else
{:error, :group_not_found} ->
@ -243,6 +264,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """
Leave a existing group
"""
@spec leave_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def leave_group(
_parent,
%{group_id: group_id},
@ -253,7 +276,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
}
) do
with {:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)},
{:ok, _activity, %Member{} = member} <- ActivityPub.leave(group, actor, true) do
{:ok, _activity, %Member{} = member} <-
Actions.Leave.leave(group, actor, true) do
{:ok, member}
else
{:error, :member_not_found} ->
@ -262,7 +286,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:group, nil} ->
{:error, dgettext("errors", "Group not found")}
{:is_not_only_admin, false} ->
{:error, :is_not_only_admin} ->
{:error,
dgettext("errors", "You can't leave this group because you are the only administrator")}
end
@ -272,6 +296,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:error, dgettext("errors", "You need to be logged-in to leave a group")}
end
@spec find_events_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Event.t())}
def find_events_for_group(
%Actor{id: group_id} = group,
%{
@ -320,16 +346,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
)}
end
@spec restrict_fields_for_non_member_request(Actor.t()) :: Actor.t()
defp restrict_fields_for_non_member_request(%Actor{} = group) do
Map.merge(
group,
%{
followers: [],
%Actor{
group
| followers: [],
followings: [],
organized_events: [],
comments: [],
feed_tokens: []
}
)
}
end
end

View File

@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
import Mobilizon.Users.Guards
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Storage.Page
alias Mobilizon.Users.User
@ -17,6 +17,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
If actor requesting is not part of the group, we only return the number of members, not members
"""
@spec find_members_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Member.t())}
def find_members_for_group(
%Actor{id: group_id} = group,
%{page: page, limit: limit, roles: roles},
@ -47,11 +49,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
end
def find_members_for_group(%Actor{} = group, _args, _resolution) do
with %Page{} = page <- Actors.list_members_for_group(group) do
{:ok, %Page{page | elements: []}}
end
%Page{} = page = Actors.list_members_for_group(group)
{:ok, %Page{page | elements: []}}
end
@spec invite_member(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def invite_member(
_parent,
%{group_id: group_id, target_actor_username: target_actor_username},
@ -68,7 +71,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
ActivityPubActor.find_or_make_actor_from_nickname(target_actor_username)},
{:existant, true} <-
{:existant, check_member_not_existant_or_rejected(target_actor_id, group.id)},
{:ok, _activity, %Member{} = member} <- ActivityPub.invite(group, actor, target_actor) do
{:ok, _activity, %Member{} = member} <-
Actions.Invite.invite(group, actor, target_actor) do
{:ok, member}
else
{:error, :group_not_found} ->
@ -92,6 +96,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
end
end
@spec accept_invitation(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def accept_invitation(_parent, %{id: member_id}, %{
context: %{current_actor: %Actor{id: actor_id}}
}) do
@ -99,7 +105,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
Actors.get_member(member_id),
{:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id},
{:ok, _activity, %Member{} = member} <-
ActivityPub.accept(
Actions.Accept.accept(
:invite,
member,
true
@ -111,6 +117,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
end
end
@spec reject_invitation(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def reject_invitation(_parent, %{id: member_id}, %{
context: %{current_actor: %Actor{id: actor_id}}
}) do
@ -118,7 +126,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
{:invitation_exists, Actors.get_member(member_id)},
{:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id},
{:ok, _activity, %Member{} = member} <-
ActivityPub.reject(
Actions.Reject.reject(
:invite,
member,
true
@ -133,12 +141,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
end
end
@spec update_member(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def update_member(_parent, %{member_id: member_id, role: role}, %{
context: %{current_actor: %Actor{} = moderator}
}) do
with %Member{} = member <- Actors.get_member(member_id),
{:ok, _activity, %Member{} = member} <-
ActivityPub.update(member, %{role: role}, true, %{moderator: moderator}) do
Actions.Update.update(member, %{role: role}, true, %{moderator: moderator}) do
{:ok, member}
else
{:error, :member_not_found} ->
@ -156,6 +166,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
def update_member(_parent, _args, _resolution),
do: {:error, "You must be logged-in to update a member"}
@spec remove_member(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def remove_member(_parent, %{member_id: member_id, group_id: group_id}, %{
context: %{current_actor: %Actor{id: moderator_id} = moderator}
}) do
@ -164,7 +176,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
{:has_rights_to_remove, {:ok, %Member{role: role}}}
when role in [:moderator, :administrator, :creator] <-
{:has_rights_to_remove, Actors.get_member(moderator_id, group_id)},
{:ok, _activity, %Member{}} <- ActivityPub.remove(member, group, moderator, true) do
{:ok, _activity, %Member{}} <-
Actions.Remove.remove(member, group, moderator, true) do
{:ok, member}
else
%Member{role: :rejected} ->

View File

@ -16,6 +16,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
@doc """
Join an event for an regular or anonymous actor
"""
@spec actor_join_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Participant.t()} | {:error, String.t()}
def actor_join_event(
_parent,
%{actor_id: actor_id, event_id: event_id} = args,
@ -157,6 +159,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
@doc """
Leave an event for an anonymous actor
"""
@spec actor_leave_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, String.t()}
def actor_leave_event(
_parent,
%{actor_id: actor_id, event_id: event_id, token: token},
@ -220,6 +224,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
{:error, dgettext("errors", "You need to be logged-in to leave an event")}
end
@spec update_participation(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Participation.t()} | {:error, String.t()}
def update_participation(
_parent,
%{id: participation_id, role: new_role},

View File

@ -12,7 +12,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
alias Mobilizon.Users.User
import Mobilizon.Web.Gettext
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
require Logger
@ -21,6 +21,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """
Get a person
"""
@spec get_person(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t() | :unauthorized}
def get_person(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
with %Actor{suspended: suspended} = actor <- Actors.get_actor_with_preload(id, true),
true <- suspended == false or is_moderator(role) do
@ -36,6 +38,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """
Find a person
"""
@spec fetch_person(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t() | :unauthorized | :unauthenticated}
def fetch_person(_parent, %{preferred_username: preferred_username}, %{
context: %{current_user: %User{} = user}
}) do
@ -57,6 +61,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
def fetch_person(_parent, _args, _resolution), do: {:error, :unauthenticated}
@spec list_persons(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Actor.t())} | {:error, :unauthorized | :unauthenticated}
def list_persons(
_parent,
%{
@ -92,7 +98,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
Returns the current actor for the currently logged-in user
"""
@spec get_current_person(any, any, Absinthe.Resolution.t()) ::
{:error, :unauthenticated} | {:ok, Actor.t()}
{:error, :unauthenticated | :no_current_person} | {:ok, Actor.t()}
def get_current_person(_parent, _args, %{context: %{current_actor: %Actor{} = actor}}) do
{:ok, actor}
end
@ -121,6 +127,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """
This function is used to create more identities from an existing user
"""
@spec create_person(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t() | :unauthenticated}
def create_person(
_parent,
%{preferred_username: _preferred_username} = args,
@ -148,6 +156,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """
This function is used to update an existing identity
"""
@spec update_person(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t() | :unauthenticated}
def update_person(
_parent,
%{id: id} = args,
@ -160,7 +170,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
{:ok, %Actor{} = actor} ->
case save_attached_pictures(args) do
args when is_map(args) ->
case ActivityPub.update(actor, args, true) do
case Actions.Update.update(actor, args, true) do
{:ok, _activity, %Actor{} = actor} ->
{:ok, actor}
@ -184,6 +194,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """
This function is used to delete an existing identity
"""
@spec delete_person(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t() | :unauthenticated}
def delete_person(
_parent,
%{id: id} = _args,
@ -225,6 +237,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
end
end
@spec last_identity?(User.t()) :: boolean
defp last_identity?(user) do
length(Users.get_actors_for_user(user)) <= 1
end
@ -275,6 +288,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """
This function is used to register a person afterwards the user has been created (but not activated)
"""
@spec register_person(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def register_person(_parent, args, _resolution) do
# When registering, email is assumed confirmed (unlike changing email)
case Users.get_user_by_email(args.email, unconfirmed: false) do
@ -311,6 +326,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """
Returns the participations, optionally restricted to an event
"""
@spec person_participations(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Participant.t())} | {:error, :unauthorized | String.t()}
def person_participations(
%Actor{id: actor_id} = person,
%{event_id: event_id},
@ -329,13 +346,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
def person_participations(%Actor{} = person, %{page: page, limit: limit}, %{
context: %{current_user: %User{} = user}
}) do
with {:can_get_participations, true} <-
{:can_get_participations, user_can_access_person_details?(person, user)},
%Page{} = page <- Events.list_event_participations_for_actor(person, page, limit) do
if user_can_access_person_details?(person, user) do
%Page{} = page = Events.list_event_participations_for_actor(person, page, limit)
{:ok, page}
else
{:can_get_participations, false} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
end
end
@ -346,24 +361,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
def person_memberships(%Actor{id: actor_id} = person, %{group: group}, %{
context: %{current_user: %User{} = user}
}) do
with {:can_get_memberships, true} <-
{:can_get_memberships, user_can_access_person_details?(person, user)},
{:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)},
{:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id),
memberships <- %Page{
if user_can_access_person_details?(person, user) do
with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)},
{:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id) do
{:ok,
%Page{
total: 1,
elements: [Repo.preload(membership, [:actor, :parent, :invited_by])]
} do
{:ok, memberships}
}}
else
{:error, :member_not_found} ->
{:ok, %Page{total: 0, elements: []}}
{:group, nil} ->
{:error, :group_not_found}
end
else
{:error, :member_not_found} ->
{:ok, %Page{total: 0, elements: []}}
{:group, nil} ->
{:error, :group_not_found}
{:can_get_memberships, _} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
end
end
@ -384,6 +398,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
end
end
@spec user_for_person(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, User.t() | nil} | {:error, String.t() | nil}
def user_for_person(%Actor{type: :Person, user_id: user_id}, _args, %{
context: %{current_user: %User{role: role}}
})
@ -402,6 +418,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
def user_for_person(_, _args, _resolution), do: {:error, nil}
@spec organized_events_for_person(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Event.t())} | {:error, :unauthorized}
def organized_events_for_person(
%Actor{} = person,
%{page: page, limit: limit},
@ -409,13 +427,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
context: %{current_user: %User{} = user}
}
) do
with {:can_get_events, true} <-
{:can_get_events, user_can_access_person_details?(person, user)},
%Page{} = page <- Events.list_organized_events_for_actor(person, page, limit) do
if user_can_access_person_details?(person, user) do
%Page{} = page = Events.list_organized_events_for_actor(person, page, limit)
{:ok, page}
else
{:can_get_events, false} ->
{:error, :unauthorized}
{:error, :unauthorized}
end
end

View File

@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Posts}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Permission, Utils}
alias Mobilizon.Federation.ActivityPub.{Actions, Permission, Utils}
alias Mobilizon.Posts.Post
alias Mobilizon.Storage.Page
alias Mobilizon.Users.User
@ -22,6 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
Returns only if actor requesting is a member of the group
"""
@spec find_posts_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Post.t())}
def find_posts_for_group(
%Actor{id: group_id} = group,
%{page: page, limit: limit} = args,
@ -32,13 +32,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
}
} = _resolution
) do
with {:member, true} <-
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)},
%Page{} = page <- Posts.get_posts_for_group(group, page, limit) do
if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
%Page{} = page = Posts.get_posts_for_group(group, page, limit)
{:ok, page}
else
{:member, _} ->
find_posts_for_group(group, args, nil)
find_posts_for_group(group, args, nil)
end
end
@ -47,9 +45,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
%{page: page, limit: limit},
_resolution
) do
with %Page{} = page <- Posts.get_public_posts_for_group(group, page, limit) do
{:ok, page}
end
%Page{} = page = Posts.get_public_posts_for_group(group, page, limit)
{:ok, page}
end
def find_posts_for_group(
@ -60,6 +57,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:ok, %Page{total: 0, elements: []}}
end
@spec get_post(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Post.t()} | {:error, :post_not_found}
def get_post(
parent,
%{slug: slug},
@ -101,6 +100,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:error, :post_not_found}
end
@spec create_post(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Post.t()} | {:error, String.t()}
def create_post(
_parent,
%{attributed_to_id: group_id} = args,
@ -118,7 +119,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
end),
args <- extract_pictures_from_post_body(args, actor_id),
{:ok, _, %Post{} = post} <-
ActivityPub.create(
Actions.Create.create(
:post,
args
|> Map.put(:author_id, actor_id)
@ -140,6 +141,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:error, dgettext("errors", "You need to be logged-in to create posts")}
end
@spec update_post(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Post.t()} | {:error, String.t()}
def update_post(
_parent,
%{id: id} = args,
@ -159,7 +162,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
args <- extract_pictures_from_post_body(args, actor_id),
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Post{} = post} <-
ActivityPub.update(post, args, true, %{"actor" => actor_url}) do
Actions.Update.update(post, args, true, %{"actor" => actor_url}) do
{:ok, post}
else
{:uuid, :error} ->
@ -177,6 +180,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:error, dgettext("errors", "You need to be logged-in to update posts")}
end
@spec delete_post(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Post.t()} | {:error, String.t()}
def delete_post(
_parent,
%{id: post_id},
@ -191,7 +196,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:post, Posts.get_post_with_preloads(post_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Post{} = post} <-
ActivityPub.delete(post, actor) do
Actions.Delete.delete(post, actor) do
{:ok, post}
else
{:uuid, :error} ->
@ -209,6 +214,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:error, dgettext("errors", "You need to be logged-in to delete posts")}
end
@spec process_picture(map() | nil, Actor.t()) :: nil | map()
defp process_picture(nil, _), do: nil
defp process_picture(%{media_id: _picture_id} = args, _), do: args

View File

@ -10,6 +10,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
@doc """
List all of an user's registered push subscriptions
"""
@spec list_user_push_subscriptions(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(PushSubscription.t())} | {:error, :unauthenticated}
def list_user_push_subscriptions(_parent, %{page: page, limit: limit}, %{
context: %{current_user: %User{id: user_id}}
}) do
@ -22,6 +24,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
@doc """
Register a push subscription
"""
@spec register_push_subscription(any(), map(), Absinthe.Resolution.t()) ::
{:ok, String.t()} | {:error, String.t()}
def register_push_subscription(_parent, args, %{
context: %{current_user: %User{id: user_id}}
}) do

View File

@ -13,6 +13,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
alias Mobilizon.GraphQL.API
@spec list_reports(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Report.t())} | {:error, String.t()}
def list_reports(
_parent,
%{page: page, limit: limit, status: status},
@ -26,6 +28,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
{:error, dgettext("errors", "You need to be logged-in and a moderator to list reports")}
end
@spec get_report(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Report.t()} | {:error, String.t()}
def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do
case Mobilizon.Reports.get_report(id) do
@ -44,6 +48,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
@doc """
Create a report, either logged-in or anonymously
"""
@spec create_report(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Report.t()} | {:error, String.t()}
def create_report(
_parent,
args,
@ -80,6 +86,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
@doc """
Update a report's status
"""
@spec update_report(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Report.t()} | {:error, String.t()}
def update_report(
_parent,
%{report_id: report_id, status: status},
@ -99,6 +107,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
{:error, dgettext("errors", "You need to be logged-in and a moderator to update a report")}
end
@spec create_report_note(any(), map(), Absinthe.Resolution.t()) :: {:ok, Note.t()}
def create_report_note(
_parent,
%{report_id: report_id, content: content},
@ -112,6 +121,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
end
end
@spec delete_report_note(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def delete_report_note(
_parent,
%{note_id: note_id},

View File

@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
alias Mobilizon.{Actors, Resources}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Resources.Resource
alias Mobilizon.Resources.Resource.Metadata
alias Mobilizon.Service.RichMedia.Parser
@ -21,6 +21,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
Returns only if actor requesting is a member of the group
"""
@spec find_resources_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Resource.t())}
def find_resources_for_group(
%Actor{id: group_id} = group,
%{page: page, limit: limit},
@ -47,6 +49,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:ok, %Page{total: 0, elements: []}}
end
@spec find_resources_for_parent(Resource.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Resource.t())}
def find_resources_for_parent(
%Resource{actor_id: group_id} = parent,
%{page: page, limit: limit},
@ -65,6 +69,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
def find_resources_for_parent(_parent, _args, _resolution),
do: {:ok, %Page{total: 0, elements: []}}
@spec get_resource(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Resource.t()} | {:error, :group_not_found | :resource_not_found | String.t()}
def get_resource(
_parent,
%{path: path, username: username},
@ -90,6 +96,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:error, dgettext("errors", "You need to be logged-in to access resources")}
end
@spec create_resource(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Resource.t()} | {:error, String.t()}
def create_resource(
_parent,
%{actor_id: group_id} = args,
@ -103,7 +111,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
parent <- get_eventual_parent(args),
{:own_check, true} <- {:own_check, check_resource_owned_by_group(parent, group_id)},
{:ok, _, %Resource{} = resource} <-
ActivityPub.create(
Actions.Create.create(
:resource,
args
|> Map.put(:actor_id, group_id)
@ -128,6 +136,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:error, dgettext("errors", "You need to be logged-in to create resources")}
end
@spec update_resource(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Resource.t()} | {:error, String.t()}
def update_resource(
_parent,
%{id: resource_id} = args,
@ -137,18 +147,25 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
}
} = _resolution
) do
with {:resource, %Resource{actor_id: group_id} = resource} <-
{:resource, Resources.get_resource_with_preloads(resource_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Resource{} = resource} <-
ActivityPub.update(resource, args, true, %{"actor" => actor_url}) do
{:ok, resource}
else
{:resource, _} ->
{:error, dgettext("errors", "Resource doesn't exist")}
case Resources.get_resource_with_preloads(resource_id) do
%Resource{actor_id: group_id} = resource ->
if Actors.is_member?(actor_id, group_id) do
case Actions.Update.update(resource, args, true, %{"actor" => actor_url}) do
{:ok, _, %Resource{} = resource} ->
{:ok, resource}
{:member, _} ->
{:error, dgettext("errors", "Profile is not member of group")}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
{:error, err} when is_atom(err) ->
{:error, dgettext("errors", "Unknown error while updating resource")}
end
else
{:error, dgettext("errors", "Profile is not member of group")}
end
nil ->
{:error, dgettext("errors", "Resource doesn't exist")}
end
end
@ -156,6 +173,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:error, dgettext("errors", "You need to be logged-in to update resources")}
end
@spec delete_resource(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Resource.t()} | {:error, String.t()}
def delete_resource(
_parent,
%{id: resource_id},
@ -169,7 +188,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:resource, Resources.get_resource_with_preloads(resource_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Resource{} = resource} <-
ActivityPub.delete(resource, actor) do
Actions.Delete.delete(resource, actor) do
{:ok, resource}
else
{:resource, _} ->
@ -184,6 +203,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:error, dgettext("errors", "You need to be logged-in to delete resources")}
end
@spec preview_resource_link(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Metadata.t()} | {:error, String.t() | :unknown_resource}
def preview_resource_link(
_parent,
%{resource_url: resource_url},
@ -211,6 +232,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:error, dgettext("errors", "You need to be logged-in to view a resource preview")}
end
@spec proxyify_pictures(Metadata.t(), map(), Absinthe.Resolution.t()) ::
{:ok, String.t() | nil} | {:error, String.t()}
def proxyify_pictures(%Metadata{} = metadata, _args, %{
definition: %{schema_node: %{name: name}}
}) do

View File

@ -2,12 +2,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
@moduledoc """
Handles the event-related GraphQL calls
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.GraphQL.API.Search
alias Mobilizon.Storage.Page
@doc """
Search persons
"""
@spec search_persons(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Actor.t())} | {:error, String.t()}
def search_persons(_parent, %{page: page, limit: limit} = args, _resolution) do
Search.search_actors(Map.put(args, :minimum_visibility, :private), page, limit, :Person)
end
@ -15,6 +19,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
@doc """
Search groups
"""
@spec search_groups(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Actor.t())} | {:error, String.t()}
def search_groups(_parent, %{page: page, limit: limit} = args, _resolution) do
Search.search_actors(args, page, limit, :Group)
end
@ -22,10 +28,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
@doc """
Search events
"""
@spec search_events(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Event.t())} | {:error, String.t()}
def search_events(_parent, %{page: page, limit: limit} = args, _resolution) do
Search.search_events(args, page, limit)
end
@spec interact(any(), map(), Absinthe.Resolution.t()) :: {:ok, struct} | {:error, :not_found}
def interact(_parent, %{uri: uri}, _resolution) do
Search.interact(uri)
end

View File

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Statistics do
@doc """
Gets config.
"""
@spec get_statistics(any(), any(), any()) :: {:ok, map()}
def get_statistics(_parent, _params, _context) do
{:ok,
%{

View File

@ -6,7 +6,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
alias Mobilizon.{Events, Posts}
alias Mobilizon.Events.{Event, Tag}
alias Mobilizon.Posts.Post
alias Mobilizon.Storage.Page
@spec list_tags(any(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Tag.t())}
def list_tags(_parent, %{page: page, limit: limit} = args, _resolution) do
filter = Map.get(args, :filter)
tags = Mobilizon.Events.list_tags(filter, page, limit)
@ -19,6 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
From an event or a struct with an url
"""
@spec list_tags_for_event(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())}
def list_tags_for_event(%Event{id: id}, _args, _resolution) do
{:ok, Events.list_tags_for_event(id)}
end
@ -33,6 +36,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
@doc """
Retrieve the list of tags for a post
"""
@spec list_tags_for_post(Post.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())}
def list_tags_for_post(%Post{id: id}, _args, _resolution) do
{:ok, Posts.list_tags_for_post(id)}
end
@ -50,9 +54,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
@doc """
Retrieve the list of related tags for a parent tag
"""
@spec list_tags_for_post(Tag.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())}
def get_related_tags(%Tag{} = tag, _args, _resolution) do
with tags <- Events.list_tag_neighbors(tag) do
{:ok, tags}
end
{:ok, Events.list_tag_neighbors(tag)}
end
end

View File

@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
alias Mobilizon.{Actors, Todos}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Storage.Page
alias Mobilizon.Todos.{Todo, TodoList}
import Mobilizon.Web.Gettext
@ -17,6 +17,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
Returns only if actor requesting is a member of the group
"""
@spec find_todo_lists_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(TodoList.t())}
def find_todo_lists_for_group(
%Actor{id: group_id} = group,
_args,
@ -39,6 +41,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
{:ok, %Page{total: 0, elements: []}}
end
@spec find_todo_lists_for_group(TodoList.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Todo.t())} | {:error, String.t()}
def find_todos_for_todo_list(
%TodoList{actor_id: group_id} = todo_list,
_args,
@ -55,6 +59,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
end
end
@spec get_todo_list(any(), map(), Absinthe.Resolution.t()) ::
{:ok, TodoList.t()} | {:error, String.t()}
def get_todo_list(
_parent,
%{id: todo_list_id},
@ -78,6 +84,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
end
end
@spec create_todo_list(any(), map(), Absinthe.Resolution.t()) ::
{:ok, TodoList.t()} | {:error, String.t()}
def create_todo_list(
_parent,
%{group_id: group_id} = args,
@ -87,7 +95,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
) do
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %TodoList{} = todo_list} <-
ActivityPub.create(:todo_list, Map.put(args, :actor_id, group_id), true, %{}) do
Actions.Create.create(
:todo_list,
Map.put(args, :actor_id, group_id),
true,
%{}
) do
{:ok, todo_list}
else
{:actor, nil} ->
@ -110,7 +123,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:ok, _, %TodoList{} = todo} <-
# ActivityPub.update_todo_list(todo_list, actor, true, %{}) do
# Actions.Update.update_todo_list(todo_list, actor, true, %{}) do
# {:ok, todo}
# else
# {:todo_list, _} ->
@ -133,7 +146,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:ok, _, %TodoList{} = todo} <-
# ActivityPub.delete_todo_list(todo_list, actor, true, %{}) do
# Actions.Delete.delete_todo_list(todo_list, actor, true, %{}) do
# {:ok, todo}
# else
# {:todo_list, _} ->
@ -144,6 +157,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# end
# end
@spec get_todo(any(), map(), Absinthe.Resolution.t()) :: {:ok, Todo.t()} | {:error, String.t()}
def get_todo(
_parent,
%{id: todo_id},
@ -169,6 +183,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
end
end
@spec create_todo(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Todo.t()} | {:error, String.t()}
def create_todo(
_parent,
%{todo_list_id: todo_list_id} = args,
@ -180,7 +196,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
{:todo_list, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Todo{} = todo} <-
ActivityPub.create(:todo, Map.put(args, :creator_id, actor_id), true, %{}) do
Actions.Create.create(
:todo,
Map.put(args, :creator_id, actor_id),
true,
%{}
) do
{:ok, todo}
else
{:actor, nil} ->
@ -194,6 +215,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
end
end
@spec update_todo(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Todo.t()} | {:error, String.t()}
def update_todo(
_parent,
%{id: todo_id} = args,
@ -207,7 +230,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
{:todo_list, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Todo{} = todo} <-
ActivityPub.update(todo, args, true, %{}) do
Actions.Update.update(todo, args, true, %{}) do
{:ok, todo}
else
{:actor, nil} ->
@ -238,7 +261,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:ok, _, %Todo{} = todo} <-
# ActivityPub.delete_todo(todo, actor, true, %{}) do
# Actions.Delete.delete_todo(todo, actor, true, %{}) do
# {:ok, todo}
# else
# {:todo_list, _} ->

View File

@ -7,8 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
alias Mobilizon.{Actors, Admin, Config, Events, Users}
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Federation.ActivityPub.{Actions, Relay}
alias Mobilizon.Service.Auth.Authenticator
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.{Setting, User}
@ -21,6 +20,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """
Find an user by its ID
"""
@spec find_user(any(), map(), Absinthe.Resolution.t()) :: {:ok, User.t()} | {:error, String.t()}
def find_user(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do
with {:ok, %User{} = user} <- Users.get_user_with_actors(id) do
@ -44,6 +44,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """
List instance users
"""
@spec list_users(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(User.t())} | {:error, :unauthorized}
def list_users(
_parent,
%{email: email, page: page, limit: limit, sort: sort, direction: direction},
@ -60,6 +62,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """
Login an user. Returns a token and the user
"""
@spec login_user(any(), map(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, :user_not_found | String.t()}
def login_user(_parent, %{email: email, password: password}, %{context: context}) do
with {:ok,
%{
@ -88,6 +92,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """
Refresh a token
"""
@spec refresh_token(any(), map(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, String.t()}
def refresh_token(_parent, %{refresh_token: refresh_token}, _resolution) do
with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token),
{:ok, _old, {exchanged_token, _claims}} <-
@ -106,6 +112,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
{:error, dgettext("errors", "You need to have an existing token to get a refresh token")}
end
@spec logout(any(), map(), Absinthe.Resolution.t()) ::
{:ok, String.t()}
| {:error, :token_not_found | :unable_to_logout | :unauthenticated | :invalid_argument}
def logout(_parent, %{refresh_token: refresh_token}, %{context: %{current_user: %User{}}}) do
with {:ok, _claims} <- Auth.Guardian.decode_and_verify(refresh_token, %{"typ" => "refresh"}),
{:ok, _claims} <- Auth.Guardian.revoke(refresh_token) do
@ -134,7 +143,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
- create the user
- send a validation email to the user
"""
@spec create_user(any, %{email: String.t()}, any) :: tuple
@spec create_user(any, %{email: String.t()}, any) :: {:ok, User.t()} | {:error, String.t()}
def create_user(_parent, %{email: email} = args, _resolution) do
with :registration_ok <- check_registration_config(email),
:not_deny_listed <- check_registration_denylist(email),
@ -161,7 +170,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
end
end
@spec check_registration_config(String.t()) :: atom
@spec check_registration_config(String.t()) ::
:registration_ok | :registration_closed | :not_allowlisted
defp check_registration_config(email) do
cond do
Config.instance_registrations_open?() ->
@ -523,7 +533,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
:ok <-
Enum.each(actors, fn actor ->
actor_performing = Keyword.get(options, :actor_performing, actor)
ActivityPub.delete(actor, actor_performing, true)
Actions.Delete.delete(actor, actor_performing, true)
end),
# Delete user
{:ok, user} <-

View File

@ -4,10 +4,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
"""
alias Mobilizon.Users
alias Mobilizon.Users.User
alias Mobilizon.Users.{ActivitySetting, User}
require Logger
@spec user_activity_settings(any(), map(), Absinthe.Resolution.t()) ::
{:ok, list(ActivitySetting.t())} | {:error, :unauthenticated}
def user_activity_settings(_parent, _args, %{context: %{current_user: %User{} = user}}) do
{:ok, Users.activity_settings_for_user(user)}
end
@ -16,6 +18,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
{:error, :unauthenticated}
end
@spec upsert_user_activity_setting(any(), map(), Absinthe.Resolution.t()) ::
{:ok, ActivitySetting.t()} | {:error, :unauthenticated}
def upsert_user_activity_setting(_parent, args, %{context: %{current_user: %User{id: user_id}}}) do
Users.create_activity_setting(Map.put(args, :user_id, user_id))
end

View File

@ -194,6 +194,7 @@ defmodule Mobilizon.GraphQL.Schema do
import_fields(:discussion_subscriptions)
end
@spec middleware(list(module()), any(), map()) :: list(module())
def middleware(middleware, _field, %{identifier: type}) when type in [:query, :mutation] do
[CurrentActorProvider] ++ middleware ++ [ErrorHandler]
end

View File

@ -9,6 +9,7 @@ defmodule Mix.Tasks.Mobilizon.Common do
"""
require Logger
@spec start_mobilizon :: any()
def start_mobilizon do
if mix_task?(), do: Mix.Task.run("app.config")
@ -21,10 +22,12 @@ defmodule Mix.Tasks.Mobilizon.Common do
{:ok, _} = Application.ensure_all_started(:mobilizon)
end
@spec get_option(Keyword.t(), atom(), String.t(), String.t() | nil, String.t() | nil) :: any()
def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do
Keyword.get(options, opt) || shell_prompt(prompt, defval, defname)
end
@spec shell_prompt(String.t(), String.t() | nil, String.t() | nil) :: String.t()
def shell_prompt(prompt, defval \\ nil, defname \\ nil) do
prompt_message = "#{prompt} [#{defname || defval}] "
@ -48,6 +51,7 @@ defmodule Mix.Tasks.Mobilizon.Common do
end
end
@spec shell_yes?(String.t()) :: boolean()
def shell_yes?(message) do
if mix_shell?(),
do: Mix.shell().yes?("Continue?"),
@ -75,10 +79,13 @@ defmodule Mix.Tasks.Mobilizon.Common do
end
@doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)"
@spec mix_shell? :: boolean
def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0)
@spec mix_task? :: boolean
def mix_task?, do: :erlang.function_exported(Mix.Task, :run, 1)
@spec escape_sh_path(String.t()) :: String.t()
def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end
@ -97,6 +104,7 @@ defmodule Mix.Tasks.Mobilizon.Common do
end
end
@spec show_subtasks_for_module(module()) :: :ok
def show_subtasks_for_module(module_name) do
tasks = list_subtasks_for_module(module_name)
@ -107,7 +115,7 @@ defmodule Mix.Tasks.Mobilizon.Common do
end)
end
@spec list_subtasks_for_module(atom()) :: list({String.t(), String.t()})
@spec list_subtasks_for_module(module()) :: list({String.t(), String.t()})
def list_subtasks_for_module(module_name) do
Application.load(:mobilizon)
{:ok, modules} = :application.get_key(:mobilizon, :modules)
@ -121,10 +129,12 @@ defmodule Mix.Tasks.Mobilizon.Common do
|> Enum.map(&format_module/1)
end
@spec format_module(module()) :: {String.t(), String.t() | nil}
defp format_module(module) do
{format_name(to_string(module)), shortdoc(module)}
end
@spec format_name(String.t()) :: String.t()
defp format_name("Elixir.Mix.Tasks.Mobilizon." <> task_name) do
String.downcase(task_name)
end

View File

@ -13,11 +13,12 @@ defmodule Mix.Tasks.Mobilizon.CreateBot do
require Logger
@shortdoc "Create bot"
@spec run(list(String.t())) :: Bot.t() | :ok
def run([email, name, summary, type, url]) do
start_mobilizon()
with {:ok, %User{} = user} <- Users.get_user_by_email(email, activated: true),
actor <- Actors.register_bot(%{name: name, summary: summary}),
{:ok, actor} <- Actors.register_bot(%{name: name, summary: summary}),
{:ok, %Bot{} = bot} <-
Actors.create_bot(%{
"type" => type,

View File

@ -11,6 +11,7 @@ defmodule Mix.Tasks.Mobilizon.SiteMap do
@preferred_cli_env "prod"
@shortdoc "Generates a new Sitemap"
@spec run(list(String.t())) :: :ok
def run(["generate"]) do
start_mobilizon()

View File

@ -70,6 +70,7 @@ defmodule Mobilizon.Activities do
[%Activity{}, ...]
"""
@spec list_activities :: list(Activity.t())
def list_activities do
Repo.all(Activity)
end
@ -161,6 +162,7 @@ defmodule Mobilizon.Activities do
** (Ecto.NoResultsError)
"""
@spec get_activity!(integer()) :: Activity.t()
def get_activity!(id), do: Repo.get!(Activity, id)
@doc """
@ -175,6 +177,7 @@ defmodule Mobilizon.Activities do
{:error, %Ecto.Changeset{}}
"""
@spec create_activity(map()) :: {:ok, Activity.t()} | {:error, Ecto.Changeset.t()}
def create_activity(attrs \\ %{}) do
%Activity{}
|> Activity.changeset(attrs)
@ -186,10 +189,13 @@ defmodule Mobilizon.Activities do
Repo.preload(activity, @activity_preloads)
end
@spec object_types :: list(String.t())
def object_types, do: @object_type
@spec subjects :: list(String.t())
def subjects, do: @subjects
@spec activity_types :: list(String.t())
def activity_types, do: @activity_types
@spec filter_object_type(Query.t(), atom() | nil) :: Query.t()

View File

@ -23,6 +23,7 @@ defmodule Mobilizon.Actors.Actor do
require Logger
@type t :: %__MODULE__{
id: integer(),
url: String.t(),
outbox_url: String.t(),
inbox_url: String.t(),

View File

@ -299,6 +299,7 @@ defmodule Mobilizon.Actors do
@delete_actor_default_options [reserve_username: true, suspension: false]
@spec delete_actor(Actor.t(), Keyword.t()) :: {:error, Ecto.Changeset.t()} | {:ok, Oban.Job.t()}
def delete_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
@ -533,7 +534,7 @@ defmodule Mobilizon.Actors do
|> Repo.one()
end
@spec get_actor_by_followers_url(String.t()) :: Actor.t()
@spec get_actor_by_followers_url(String.t()) :: Actor.t() | nil
def get_actor_by_followers_url(followers_url) do
Actor
|> where([q], q.followers_url == ^followers_url)

View File

@ -12,6 +12,8 @@ defmodule Mobilizon.Actors.Member do
alias Mobilizon.Web.Endpoint
@type t :: %__MODULE__{
id: String.t(),
url: String.t(),
role: MemberRole.t(),
parent: Actor.t(),
actor: Actor.t(),

View File

@ -73,6 +73,7 @@ defmodule Mobilizon.Addresses.Address do
put_change(changeset, :url, url)
end
@spec coords(nil | t) :: nil | {float, float}
def coords(nil), do: nil
def coords(%__MODULE__{} = address) do
@ -81,6 +82,7 @@ defmodule Mobilizon.Addresses.Address do
end
end
@spec representation(nil | t) :: nil | String.t()
def representation(nil), do: nil
def representation(%__MODULE__{} = address) do

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.CLI do
"""
alias Mix.Tasks.Mobilizon.Ecto.{Migrate, Rollback}
@spec run(String.t()) :: any()
def run(args) do
[task | args] = String.split(args)
@ -43,11 +44,13 @@ defmodule Mobilizon.CLI do
end
end
def migrate(args) do
@spec migrate(String.t()) :: any()
defp migrate(args) do
Migrate.run(args)
end
def rollback(args) do
@spec rollback(String.t()) :: any()
defp rollback(args) do
Rollback.run(args)
end
end

View File

@ -302,9 +302,9 @@ defmodule Mobilizon.Config do
def instance_event_creation_enabled?,
do: :mobilizon |> Application.get_env(:events) |> Keyword.get(:creation)
@spec anonymous_actor_id :: binary | integer
@spec anonymous_actor_id :: integer
def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id)
@spec relay_actor_id :: binary | integer
@spec relay_actor_id :: integer
def relay_actor_id, do: get_cached_value(:relay_actor_id)
@spec admin_settings :: map
def admin_settings, do: get_cached_value(:admin_config)

View File

@ -20,6 +20,7 @@ defmodule Mobilizon.Discussions.Comment do
@type t :: %__MODULE__{
text: String.t(),
url: String.t(),
id: integer(),
local: boolean,
visibility: CommentVisibility.t(),
uuid: Ecto.UUID.t(),

View File

@ -4,6 +4,7 @@ defmodule Mobilizon.Discussions.Discussion.TitleSlug do
"""
use EctoAutoslugField.Slug, from: [:title, :id], to: :slug
@spec build_slug([String.t()], Ecto.Changeset.t()) :: String.t()
def build_slug([title, id], %Ecto.Changeset{valid?: true}) do
[title, ShortUUID.encode!(id)]
|> Enum.join("-")
@ -31,6 +32,7 @@ defmodule Mobilizon.Discussions.Discussion do
import Mobilizon.Web.Gettext, only: [dgettext: 2]
@type t :: %__MODULE__{
id: String.t(),
creator: Actor.t(),
actor: Actor.t(),
title: String.t(),

View File

@ -377,8 +377,8 @@ defmodule Mobilizon.Discussions do
@doc """
Creates a discussion.
"""
@spec create_discussion(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def create_discussion(attrs \\ %{}) do
@spec create_discussion(map()) :: {:ok, Discussion.t()} | {:error, atom(), Changeset.t(), map()}
def create_discussion(attrs) do
with {:ok, %{comment: %Comment{} = _comment, discussion: %Discussion{} = discussion}} <-
Multi.new()
|> Multi.insert(
@ -412,19 +412,26 @@ defmodule Mobilizon.Discussions do
@doc """
Create a response to a discussion
"""
@spec reply_to_discussion(Discussion.t(), map()) :: {:ok, Discussion.t()}
@spec reply_to_discussion(Discussion.t(), map()) ::
{:ok, Discussion.t()} | {:error, atom(), Ecto.Changeset.t(), map()}
def reply_to_discussion(%Discussion{id: discussion_id} = discussion, attrs \\ %{}) do
attrs =
Map.merge(attrs, %{
discussion_id: discussion_id,
actor_id: Map.get(attrs, :creator_id, Map.get(attrs, :actor_id))
})
changeset =
Comment.changeset(
%Comment{},
attrs
)
with {:ok, %{comment: %Comment{} = comment, discussion: %Discussion{} = discussion}} <-
Multi.new()
|> Multi.insert(
:comment,
Comment.changeset(
%Comment{},
Map.merge(attrs, %{
discussion_id: discussion_id,
actor_id: Map.get(attrs, :creator_id, attrs.actor_id)
})
)
changeset
)
|> Multi.update(:discussion, fn %{comment: %Comment{id: comment_id}} ->
Discussion.changeset(
@ -435,7 +442,7 @@ defmodule Mobilizon.Discussions do
|> Repo.transaction(),
# Discussion is not updated
%Comment{} = comment <- Repo.preload(comment, @comment_preloads) do
{:ok, Map.put(discussion, :last_comment, comment)}
{:ok, %Discussion{discussion | last_comment: comment}}
end
end
@ -453,7 +460,8 @@ defmodule Mobilizon.Discussions do
@doc """
Delete a discussion.
"""
@spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()}
@spec delete_discussion(Discussion.t()) ::
{:ok, %{comments: {integer() | nil, any()}}} | {:error, :comments, Changeset.t(), map()}
def delete_discussion(%Discussion{id: discussion_id}) do
Multi.new()
|> Multi.delete_all(:comments, fn _ ->
@ -463,7 +471,7 @@ defmodule Mobilizon.Discussions do
|> Repo.transaction()
end
@spec public_comments_for_actor_query(String.t() | integer()) :: [Comment.t()]
@spec public_comments_for_actor_query(String.t() | integer()) :: Ecto.Query.t()
defp public_comments_for_actor_query(actor_id) do
Comment
|> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility)
@ -471,7 +479,7 @@ defmodule Mobilizon.Discussions do
|> preload_for_comment()
end
@spec public_replies_for_thread_query(String.t() | integer()) :: [Comment.t()]
@spec public_replies_for_thread_query(String.t() | integer()) :: Ecto.Query.t()
defp public_replies_for_thread_query(comment_id) do
Comment
|> where([c], c.origin_comment_id == ^comment_id and c.visibility in ^@public_visibility)

View File

@ -35,7 +35,7 @@ defmodule Mobilizon.Events.Event do
alias Mobilizon.Web.Router.Helpers, as: Routes
@type t :: %__MODULE__{
id: String.t(),
id: integer(),
url: String.t(),
local: boolean,
begins_on: DateTime.t(),
@ -47,16 +47,16 @@ defmodule Mobilizon.Events.Event do
draft: boolean,
visibility: EventVisibility.t(),
join_options: JoinOptions.t(),
publish_at: DateTime.t(),
publish_at: DateTime.t() | nil,
uuid: Ecto.UUID.t(),
online_address: String.t(),
online_address: String.t() | nil,
phone_address: String.t(),
category: String.t(),
options: EventOptions.t(),
organizer_actor: Actor.t(),
attributed_to: Actor.t() | nil,
physical_address: Address.t(),
picture: Media.t(),
physical_address: Address.t() | nil,
picture: Media.t() | nil,
media: [Media.t()],
tracks: [Track.t()],
sessions: [Session.t()],

View File

@ -282,7 +282,7 @@ defmodule Mobilizon.Events do
# We start by inserting the event and then insert a first participant if the event is not a draft
@spec do_create_event(map) ::
{:ok, Event.t()}
{:ok, %{insert: Event.t(), write: Participant.t() | nil}}
| {:error, Changeset.t()}
| {:error, :update | :write, Changeset.t(), map()}
defp do_create_event(attrs) do
@ -368,7 +368,7 @@ defmodule Mobilizon.Events do
Deletes an event.
Raises an exception if it fails.
"""
@spec delete_event(Event.t()) :: Event.t()
@spec delete_event!(Event.t()) :: Event.t()
def delete_event!(%Event{} = event), do: Repo.delete!(event)
@doc """
@ -457,6 +457,7 @@ defmodule Mobilizon.Events do
@spec list_organized_events_for_group(
Actor.t(),
EventVisibility.t(),
DateTime.t() | nil,
DateTime.t() | nil,
integer | nil,

View File

@ -15,6 +15,7 @@ defmodule Mobilizon.Events.Participant do
alias Mobilizon.Web.Endpoint
@type t :: %__MODULE__{
id: String.t(),
role: ParticipantRole.t(),
url: String.t(),
event: Event.t(),

View File

@ -4,6 +4,7 @@ defmodule Mobilizon.Posts.Post.TitleSlug do
"""
use EctoAutoslugField.Slug, from: [:title, :id], to: :slug
@spec build_slug([String.t()], any()) :: String.t() | nil
def build_slug([title, id], _changeset) do
[title, ShortUUID.encode!(id)]
|> Enum.join("-")
@ -31,6 +32,7 @@ defmodule Mobilizon.Posts.Post do
import Mobilizon.Web.Gettext
@type t :: %__MODULE__{
id: String.t(),
url: String.t(),
local: boolean,
slug: String.t(),

View File

@ -15,6 +15,7 @@ defmodule Mobilizon.Reports.Report do
alias Mobilizon.Web.Endpoint
@type t :: %__MODULE__{
id: integer(),
content: String.t(),
status: ReportStatus.t(),
url: String.t(),

View File

@ -13,6 +13,7 @@ defmodule Mobilizon.Resources.Resource do
alias Mobilizon.Resources.Resource.Metadata
@type t :: %__MODULE__{
id: String.t(),
title: String.t(),
summary: String.t(),
url: String.t(),

View File

@ -10,6 +10,7 @@ defmodule Mobilizon.Storage.Repo do
@doc """
Dynamically loads the repository url from the DATABASE_URL environment variable.
"""
@spec init(any(), any()) :: any()
def init(_, opts) do
{:ok, opts}
end

View File

@ -10,6 +10,8 @@ defmodule Mobilizon.Todos.Todo do
alias Mobilizon.Todos.TodoList
@type t :: %__MODULE__{
id: String.t(),
url: String.t(),
status: boolean(),
title: String.t(),
due_date: DateTime.t(),

View File

@ -10,6 +10,8 @@ defmodule Mobilizon.Todos.TodoList do
alias Mobilizon.Todos.Todo
@type t :: %__MODULE__{
id: String.t(),
url: String.t(),
title: String.t(),
todos: [Todo.t()],
actor: Actor.t(),

View File

@ -107,7 +107,7 @@ defmodule Mobilizon.Users do
@doc """
Get an user by its activation token.
"""
@spec get_user_by_activation_token(String.t()) :: Actor.t() | nil
@spec get_user_by_activation_token(String.t()) :: User.t() | nil
def get_user_by_activation_token(token) do
token
|> user_by_activation_token_query()
@ -117,7 +117,7 @@ defmodule Mobilizon.Users do
@doc """
Get an user by its reset password token.
"""
@spec get_user_by_reset_password_token(String.t()) :: Actor.t() | nil
@spec get_user_by_reset_password_token(String.t()) :: User.t() | nil
def get_user_by_reset_password_token(token) do
token
|> user_by_reset_password_token_query()

View File

@ -40,7 +40,7 @@ defmodule Mobilizon.Service.Activity.Member do
Actors.get_member(member_id)
end
@spec get_author(Member.t(), Member.t() | nil) :: String.t() | integer()
@spec get_author(Member.t(), Member.t() | nil) :: integer()
defp get_author(%Member{actor_id: actor_id}, options) do
moderator = Keyword.get(options, :moderator)

View File

@ -3,10 +3,12 @@ defmodule Mobilizon.Service.ErrorPage do
Render an error page
"""
@spec init :: :ok | {:error, File.posix()}
def init do
render_error_page()
end
@spec render_error_page :: :ok | {:error, File.posix()}
defp render_error_page do
content =
Phoenix.View.render_to_string(Mobilizon.Web.ErrorView, "500.html", conn: %Plug.Conn{})

View File

@ -65,6 +65,7 @@ defmodule Mobilizon.Service.Formatter do
end
end
@spec hashtag_handler(String.t(), String.t(), any(), map()) :: {String.t(), map()}
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Endpoint.url()}/tag/#{tag}"
@ -100,6 +101,7 @@ defmodule Mobilizon.Service.Formatter do
@doc """
Escapes a special characters in mention names.
"""
@spec mentions_escape(String.t(), Keyword.t()) :: String.t()
def mentions_escape(text, options \\ []) do
options =
Keyword.merge(options,
@ -111,6 +113,11 @@ defmodule Mobilizon.Service.Formatter do
Linkify.link(text, options)
end
@spec html_escape(
{text :: String.t(), mentions :: list(), hashtags :: list()},
type :: String.t()
) :: {String.t(), list(), list()}
@spec html_escape(text :: String.t(), type :: String.t()) :: String.t()
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
@ -131,6 +138,7 @@ defmodule Mobilizon.Service.Formatter do
|> Enum.join("")
end
@spec truncate(String.t(), non_neg_integer(), String.t()) :: String.t()
def truncate(text, max_length \\ 200, omission \\ "...") do
# Remove trailing whitespace
text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
@ -143,6 +151,7 @@ defmodule Mobilizon.Service.Formatter do
end
end
@spec linkify_opts :: Keyword.t()
defp linkify_opts do
Mobilizon.Config.get(__MODULE__) ++
[
@ -186,5 +195,6 @@ defmodule Mobilizon.Service.Formatter do
|> (&" #{&1}").()
end
@spec tag_text_strip(String.t()) :: String.t()
defp tag_text_strip(tag), do: tag |> String.trim("#") |> String.downcase()
end

View File

@ -9,6 +9,7 @@ defmodule Mobilizon.Service.HTTP.ActivityPub do
recv_timeout: 20_000
]
@spec client(Keyword.t()) :: Tesla.Client.t()
def client(options \\ []) do
headers = Keyword.get(options, :headers, [])
adapter = Application.get_env(:tesla, __MODULE__, [])[:adapter] || Tesla.Adapter.Hackney
@ -27,10 +28,12 @@ defmodule Mobilizon.Service.HTTP.ActivityPub do
Tesla.client(middleware, {adapter, opts})
end
@spec get(Tesla.Client.t(), String.t()) :: Tesla.Env.t()
def get(client, url) do
Tesla.get(client, url)
end
@spec post(Tesla.Client.t(), String.t(), map() | String.t()) :: Tesla.Env.t()
def post(client, url, data) do
Tesla.post(client, url, data)
end

View File

@ -19,7 +19,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
require Logger
@spec trigger_notifications_for_participant(Participant.t()) :: {:ok, nil}
@spec trigger_notifications_for_participant(Participant.t()) :: {:ok, Oban.Job.t() | nil}
def trigger_notifications_for_participant(%Participant{} = participant) do
before_event_notification(participant)
on_day_notification(participant)
@ -27,6 +27,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
{:ok, nil}
end
@spec before_event_notification(Participant.t()) :: {:ok, nil}
def before_event_notification(%Participant{
id: participant_id,
event: %Event{begins_on: begins_on},
@ -46,6 +47,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
def before_event_notification(_), do: {:ok, nil}
@spec on_day_notification(Participant.t()) :: {:ok, Oban.Job.t() | nil | String.t()}
def on_day_notification(%Participant{
event: %Event{begins_on: begins_on},
actor: %Actor{user_id: user_id}
@ -90,6 +92,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
def on_day_notification(_), do: {:ok, nil}
@spec weekly_notification(Participant.t()) :: {:ok, Oban.Job.t() | nil | String.t()}
def weekly_notification(%Participant{
event: %Event{begins_on: begins_on},
actor: %Actor{user_id: user_id}
@ -144,6 +147,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
def weekly_notification(_), do: {:ok, nil}
@spec pending_participation_notification(Event.t(), Keyword.t()) :: {:ok, Oban.Job.t() | nil}
def pending_participation_notification(event, options \\ [])
def pending_participation_notification(

View File

@ -18,6 +18,7 @@ defmodule Mobilizon.Service.Notifier do
@callback send(User.t(), list(Activity.t()), Keyword.t()) :: {:ok, any()} | {:error, String.t()}
@spec notify(User.t(), Activity.t(), Keyword.t()) :: :ok
def notify(%User{} = user, %Activity{} = activity, opts \\ []) do
Enum.each(providers(opts), & &1.send(user, activity, opts))
end

View File

@ -8,6 +8,8 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do
Module to parse meta tags data in HTML pages
"""
@spec parse(String.t(), map(), String.t(), String.t(), atom(), atom(), list(atom())) ::
{:ok, map()} | {:error, String.t()}
def parse(
html,
data,
@ -35,10 +37,12 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do
end
end
@spec get_elements(String.t(), atom(), String.t()) :: Floki.html_tree()
defp get_elements(html, key_name, prefix) do
html |> Floki.parse_document!() |> Floki.find("meta[#{to_string(key_name)}^='#{prefix}:']")
end
@spec normalize_attributes(Floki.html_node(), String.t(), atom(), atom(), list(atom())) :: map()
defp normalize_attributes(html_node, prefix, key_name, value_name, allowed_attributes) do
{_tag, attributes, _children} = html_node
@ -55,6 +59,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do
end
end
@spec maybe_put_title(map(), String.t()) :: map()
defp maybe_put_title(%{title: _} = meta, _), do: meta
defp maybe_put_title(meta, html) when meta != %{} do
@ -66,6 +71,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.MetaTagsParser do
defp maybe_put_title(meta, _), do: meta
@spec maybe_put_description(map(), String.t()) :: map()
defp maybe_put_description(%{description: _} = meta, _), do: meta
defp maybe_put_description(meta, html) when meta != %{} do

View File

@ -15,6 +15,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do
ssl: [{:versions, [:"tlsv1.2"]}]
]
@spec parse(String.t(), map()) :: {:ok, map()} | {:error, String.t()}
def parse(html, _data) do
Logger.debug("Using OEmbed parser")

View File

@ -30,6 +30,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OGP do
:"image:alt"
]
@spec parse(String.t(), map()) :: {:ok, map()}
def parse(html, data) do
Logger.debug("Using OpenGraph card parser")
@ -49,6 +50,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OGP do
end
end
@spec transform_tags(map()) :: map()
defp transform_tags(data) do
data
|> Enum.reject(fn {_, v} -> is_nil(v) end)

Some files were not shown because too many files have changed in this diff Show More