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{ %Doctor.Config{
exception_moduledoc_required: true, exception_moduledoc_required: true,
failed: false, failed: false,
ignore_modules: [], ignore_modules: [Mobilizon.Web, Mobilizon.GraphQL.Schema, Mobilizon.Service.Activity.Renderer, Mobilizon.Service.Workers.Helper],
ignore_paths: [], ignore_paths: [],
min_module_doc_coverage: 70, min_module_doc_coverage: 100,
min_module_spec_coverage: 50, min_module_spec_coverage: 50,
min_overall_doc_coverage: 90, min_overall_doc_coverage: 100,
min_overall_spec_coverage: 30, min_overall_spec_coverage: 90,
moduledoc_required: true, moduledoc_required: true,
raise: false, raise: false,
reporter: Doctor.Reporters.Full, reporter: Doctor.Reporters.Full,

View File

@ -4,8 +4,10 @@ defmodule Mobilizon.ConfigProvider do
""" """
@behaviour Config.Provider @behaviour Config.Provider
@spec init(String.t()) :: String.t()
def init(path) when is_binary(path), do: path def init(path) when is_binary(path), do: path
@spec load(Keyword.t(), String.t()) :: Keyword.t()
def load(config, path) do def load(config, path) do
config_path = System.get_env("MOBILIZON_CONFIG_PATH") || path 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.{ alias Mobilizon.{
Actors, Actors,
Config,
Discussions, Discussions,
Events, Events,
Posts, Posts,
Resources, Resources
Share,
Users
} }
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.Event
alias Mobilizon.Tombstone alias Mobilizon.Tombstone
alias Mobilizon.Federation.ActivityPub.{ alias Mobilizon.Federation.ActivityPub.{
Activity, Activity,
Audience,
Federator,
Fetcher, Fetcher,
Preloader, Preloader,
Refresher, Relay
Relay,
Transmogrifier,
Types,
Visibility
} }
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.Types.{Entity, Managable, Ownable}
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Federation.HTTPSignatures.Signature
alias Mobilizon.Service.Notifications.Scheduler
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Email.{Admin, Group, Mailer}
require Logger require Logger
@public_ap_adress "https://www.w3.org/ns/activitystreams#Public" @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 """ @doc """
Fetch an object from an URL, from our local database of events and comments, then eventually remote Fetch an object from an URL, from our local database of events and comments, then eventually remote
""" """
@ -79,8 +49,8 @@ defmodule Mobilizon.Federation.ActivityPub do
def fetch_object_from_url(url, options \\ []) do def fetch_object_from_url(url, options \\ []) do
Logger.info("Fetching object from url #{url}") Logger.info("Fetching object from url #{url}")
with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")}, if String.starts_with?(url, "http") do
{:existing, nil} <- with {:existing, nil} <-
{:existing, Tombstone.find_tombstone(url)}, {:existing, Tombstone.find_tombstone(url)},
{:existing, nil} <- {:existing, Events.get_event_by_url(url)}, {:existing, nil} <- {:existing, Events.get_event_by_url(url)},
{:existing, nil} <- {:existing, nil} <-
@ -102,10 +72,9 @@ defmodule Mobilizon.Federation.ActivityPub do
{:error, e} -> {:error, e} ->
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}") Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
{:error, e} {:error, e}
end
e -> else
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}") {:error, :url_not_http}
{:error, e}
end end
end end
@ -132,7 +101,7 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
@spec refresh_entity(String.t(), struct(), Keyword.t()) :: @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 defp refresh_entity(url, entity, options) do
force_fetch = Keyword.get(options, :force, false) force_fetch = Keyword.get(options, :force, false)
@ -149,609 +118,14 @@ defmodule Mobilizon.Federation.ActivityPub do
{:error, :http_not_found} -> {:error, :http_not_found} ->
{:error, :http_not_found, entity} {:error, :http_not_found, entity}
{:error, "Object origin check failed"} -> {:error, err} ->
{:error, "Object origin check failed"} {:error, err}
end end
else else
{:ok, entity} {:ok, entity}
end end
end end
@doc """
Create an activity of type `Create`
* Creates the object, which returns AS data
* Wraps ActivityStreams data into a `Create` activity
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
* Federates (asynchronously) the activity
* Returns the activity
"""
@spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), Entity.entities()} | any()
def create(type, args, local \\ false, additional \\ %{}) do
Logger.debug("creating an activity")
Logger.debug(inspect(args))
with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)},
{:ok, entity, create_data} <-
(case type do
:event -> Types.Events.create(args, additional)
:comment -> Types.Comments.create(args, additional)
:discussion -> Types.Discussions.create(args, additional)
:actor -> Types.Actors.create(args, additional)
:todo_list -> Types.TodoLists.create(args, additional)
:todo -> Types.Todos.create(args, additional)
:resource -> Types.Resources.create(args, additional)
:post -> Types.Posts.create(args, additional)
end),
{:ok, activity} <- create_activity(create_data, local),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
@doc """
Create an activity of type `Update`
* Updates the object, which returns AS data
* Wraps ActivityStreams data into a `Update` activity
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
* Federates (asynchronously) the activity
* Returns the activity
"""
@spec update(Entity.entities(), map(), boolean, map()) ::
{:ok, Activity.t(), Entity.entities()} | {:error, any()}
def update(old_entity, args, local \\ false, additional \\ %{}) do
Logger.debug("updating an activity")
Logger.debug(inspect(args))
case Managable.update(old_entity, args, additional) do
{:ok, entity, update_data} ->
{:ok, activity} = create_activity(update_data, local)
maybe_federate(activity)
maybe_relay_if_group_activity(activity)
{:ok, activity, entity}
{:error, err} ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
{:error, err}
end
end
@type acceptable_types :: :join | :follow | :invite
@type acceptable_entities ::
accept_join_entities | accept_follow_entities | accept_invite_entities
@spec accept(acceptable_types, acceptable_entities, boolean, map) ::
{:ok, ActivityStream.t(), acceptable_entities}
def accept(type, entity, local \\ true, additional \\ %{}) do
Logger.debug("We're accepting something")
{:ok, entity, update_data} =
case type do
:join -> accept_join(entity, additional)
:follow -> accept_follow(entity, additional)
:invite -> accept_invite(entity, additional)
end
with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
@spec reject(acceptable_types, acceptable_entities, boolean, map) ::
{:ok, ActivityStream.t(), acceptable_entities}
def reject(type, entity, local \\ true, additional \\ %{}) do
{:ok, entity, update_data} =
case type do
:join -> reject_join(entity, additional)
:follow -> reject_follow(entity, additional)
:invite -> reject_invite(entity, additional)
end
with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
@spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) ::
{:ok, Activity.t(), ActivityStream.t()} | {:error, any()}
def announce(
%Actor{} = actor,
object,
activity_id \\ nil,
local \\ true,
public \\ true
) do
with {:ok, %Actor{id: object_owner_actor_id}} <-
ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]),
{:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id),
announce_data <- make_announce_data(actor, object, activity_id, public),
{:ok, activity} <- create_activity(announce_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
error ->
{:error, error}
end
end
@spec unannounce(Actor.t(), ActivityStream.t(), String.t() | nil, String.t() | nil, boolean) ::
{:ok, Activity.t(), ActivityStream.t()}
def unannounce(
%Actor{} = actor,
object,
activity_id \\ nil,
cancelled_activity_id \\ nil,
local \\ true
) do
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- create_activity(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity) do
{:ok, unannounce_activity, object}
else
_e -> {:ok, object}
end
end
@doc """
Make an actor follow another
"""
@spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) ::
{:ok, Activity.t(), Follower.t()} | {:error, atom | Ecto.Changeset.t() | String.t()}
def follow(
%Actor{} = follower,
%Actor{} = followed,
activity_id \\ nil,
local \\ true,
additional \\ %{}
) do
if followed.id != follower.id do
case Types.Actors.follow(
follower,
followed,
local,
Map.merge(additional, %{"activity_id" => activity_id})
) do
{:ok, activity_data, %Follower{} = follower} ->
{:ok, activity} = create_activity(activity_data, local)
maybe_federate(activity)
{:ok, activity, follower}
{:error, err} ->
{:error, err}
end
else
{:error, "Can't follow yourself"}
end
end
@doc """
Make an actor unfollow another
"""
@spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Follower.t()} | {:error, String.t()}
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower),
# We recreate the follow activity
follow_as_data <-
Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}),
{:ok, follow_activity} <- create_activity(follow_as_data, local),
activity_unfollow_id <-
activity_id || "#{Endpoint.url()}/unfollow/#{follow_id}/activity",
unfollow_data <-
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
{:ok, activity} <- create_activity(unfollow_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, follow}
else
err ->
Logger.debug("Error while unfollowing an actor #{inspect(err)}")
err
end
end
@spec delete(Entity.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Entity.t()}
def delete(object, actor, local \\ true, additional \\ %{}) do
with {:ok, activity_data, actor, object} <-
Managable.delete(object, actor, local, additional),
group <- Ownable.group_actor(object),
:ok <- check_for_actor_key_rotation(actor),
{:ok, activity} <- create_activity(activity_data, local),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity, group) do
{:ok, activity, object}
end
end
@spec join(Event.t(), Actor.t(), boolean, map) ::
{:ok, Activity.t(), Participant.t()} | {:error, :maximum_attendee_capacity}
@spec join(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
def join(entity_to_join, actor_joining, local \\ true, additional \\ %{})
def join(%Event{} = event, %Actor{} = actor, local, additional) do
case Types.Events.join(event, actor, local, additional) do
{:ok, activity_data, participant} ->
{:ok, activity} = create_activity(activity_data, local)
maybe_federate(activity)
{:ok, activity, participant}
{:error, :maximum_attendee_capacity_reached} ->
{:error, :maximum_attendee_capacity_reached}
{:accept, accept} ->
accept
end
end
def join(%Actor{type: :Group} = group, %Actor{} = actor, local, additional) do
with {:ok, activity_data, %Member{} = member} <-
Types.Actors.join(group, actor, local, additional),
{:ok, activity} <- create_activity(activity_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, member}
else
{:accept, accept} ->
accept
end
end
@spec leave(Event.t(), Actor.t(), boolean, map) ::
{:ok, Activity.t(), Participant.t()}
| {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()}
@spec leave(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
def leave(object, actor, local \\ true, additional \\ %{})
@doc """
Leave an event or a group
"""
def leave(
%Event{id: event_id, url: event_url} = _event,
%Actor{id: actor_id, url: actor_url} = _actor,
local,
additional
) do
if Participant.is_not_only_organizer(event_id, actor_id) do
{:error, :is_only_organizer}
else
case Mobilizon.Events.get_participant(
event_id,
actor_id,
Map.get(additional, :metadata, %{})
) do
{:ok, %Participant{} = participant} ->
case Events.delete_participant(participant) do
{:ok, %{participant: %Participant{} = participant}} ->
leave_data = %{
"type" => "Leave",
# If it's an exclusion it should be something else
"actor" => actor_url,
"object" => event_url,
"id" => "#{Endpoint.url()}/leave/event/#{participant.id}"
}
audience = Audience.get_audience(participant)
{:ok, activity} = create_activity(Map.merge(leave_data, audience), local)
maybe_federate(activity)
{:ok, activity, participant}
{:error, _type, %Ecto.Changeset{} = err, _} ->
{:error, err}
end
{:error, :participant_not_found} ->
{:error, :participant_not_found}
end
end
end
def leave(
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
%Actor{id: actor_id, url: actor_url},
local,
additional
) do
with {:member, {:ok, %Member{id: member_id} = member}} <-
{:member, Actors.get_member(actor_id, group_id)},
{:is_not_only_admin, true} <-
{:is_not_only_admin,
Map.get(additional, :force_member_removal, false) ||
!Actors.is_only_administrator?(member_id, group_id)},
{:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)},
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit"),
leave_data <- %{
"to" => [group_members_url],
"cc" => [group_url],
"attributedTo" => group_url,
"type" => "Leave",
"actor" => actor_url,
"object" => group_url
},
{:ok, activity} <- create_activity(leave_data, local),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, member}
end
end
@spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
def remove(
%Member{} = member,
%Actor{type: :Group, url: group_url, members_url: group_members_url},
%Actor{url: moderator_url} = moderator,
local,
_additional \\ %{}
) do
with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}),
%Member{} = member <- Actors.get_member(member_id),
{:ok, _} <-
Mobilizon.Service.Activity.Member.insert_activity(member,
moderator: moderator,
subject: "member_removed"
),
:ok <- Group.send_notification_to_removed_member(member),
remove_data <- %{
"to" => [group_members_url],
"type" => "Remove",
"actor" => moderator_url,
"object" => member.url,
"origin" => group_url
},
{:ok, activity} <- create_activity(remove_data, local),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity) do
{:ok, activity, member}
end
end
@spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) ::
{:ok, map(), Member.t()} | {:error, :member_not_found}
def invite(
%Actor{url: group_url, id: group_id, members_url: members_url} = group,
%Actor{url: actor_url, id: actor_id} = actor,
%Actor{url: target_actor_url, id: target_actor_id} = _target_actor,
local \\ true,
additional \\ %{}
) do
Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}")
with {:is_able_to_invite, true} <- {:is_able_to_invite, is_able_to_invite?(actor, group)},
{:ok, %Member{url: member_url} = member} <-
Actors.create_member(%{
parent_id: group_id,
actor_id: target_actor_id,
role: :invited,
invited_by_id: actor_id,
url: Map.get(additional, :url)
}),
{:ok, _} <-
Mobilizon.Service.Activity.Member.insert_activity(member,
moderator: actor,
subject: "member_invited"
),
invite_data <- %{
"type" => "Invite",
"attributedTo" => group_url,
"actor" => actor_url,
"object" => group_url,
"target" => target_actor_url,
"id" => member_url
},
{:ok, activity} <-
create_activity(
invite_data
|> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]})
|> Map.merge(additional),
local
),
:ok <- maybe_federate(activity),
:ok <- maybe_relay_if_group_activity(activity),
:ok <- Group.send_invite_to_user(member) do
{:ok, activity, member}
end
end
@spec is_able_to_invite?(Actor.t(), Actor.t()) :: boolean
defp is_able_to_invite?(%Actor{domain: actor_domain, id: actor_id}, %Actor{
domain: group_domain,
id: group_id
}) do
# If the actor comes from the same domain we trust it
if actor_domain == group_domain do
true
else
# If local group, we'll send the invite
case Actors.get_member(actor_id, group_id) do
{:ok, %Member{} = admin_member} ->
Member.is_administrator(admin_member)
_ ->
false
end
end
end
@spec move(:resource, Resource.t(), map, boolean, map) :: {:ok, Activity.t(), Resource.t()}
def move(type, old_entity, args, local \\ false, additional \\ %{}) do
Logger.debug("We're moving something")
Logger.debug(inspect(args))
with {:ok, entity, update_data} <-
(case type do
:resource -> Types.Resources.move(old_entity, args, additional)
end),
{:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating a Move activity")
Logger.debug(inspect(err))
err
end
end
@spec flag(map, boolean, map) :: {:ok, Activity.t(), Report.t()}
def flag(args, local \\ false, additional \\ %{}) do
with {report, report_as_data} <- Types.Reports.flag(args, local, additional),
{:ok, activity} <- create_activity(report_as_data, local),
:ok <- maybe_federate(activity) do
Enum.each(Users.list_moderators(), fn moderator ->
moderator
|> Admin.report(report)
|> Mailer.send_email_later()
end)
{:ok, activity, report}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
@spec is_create_activity?(Activity.t()) :: boolean
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
defp is_create_activity?(_), do: false
@spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
defp convert_members_in_recipients(recipients) do
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc ->
case Actors.get_group_by_members_url(recipient) do
# If the group is local just add external members
%Actor{domain: domain} = group when is_nil(domain) ->
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
member_actors ++ Actors.list_external_actors_members_for_group(group)}
# If it's remote add the remote group actor as well
%Actor{} = group ->
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]}
_ ->
acc
end
end)
end
@spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())}
defp convert_followers_in_recipients(recipients) do
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
case Actors.get_actor_by_followers_url(recipient) do
%Actor{} = group ->
{Enum.filter(recipients, fn recipient -> recipient != group.followers_url end),
follower_actors ++ Actors.list_external_followers_for_actor(group)}
_ ->
acc
end
end)
end
# @spec is_announce_activity?(Activity.t()) :: boolean
# defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
# defp is_announce_activity?(_), do: false
@doc """
Publish an activity to all appropriated audiences inboxes
"""
# credo:disable-for-lines:47
@spec publish(Actor.t(), Activity.t()) :: :ok
def publish(actor, %Activity{recipients: recipients} = activity) do
Logger.debug("Publishing an activity")
Logger.debug(inspect(activity, pretty: true))
public = Visibility.is_public?(activity)
Logger.debug("is public ? #{public}")
if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
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 """ @doc """
Return all public activities (events & comments) for an actor Return all public activities (events & comments) for an actor
""" """
@ -802,224 +176,4 @@ defmodule Mobilizon.Federation.ActivityPub do
local: local local: local
} }
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
@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 end

View File

@ -7,6 +7,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
@ -154,22 +155,20 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{mentions, []} {mentions, []}
end 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 defp maybe_add_group_members(collection, %Actor{type: :Group, members_url: members_url}) do
[members_url | collection] [members_url | collection]
end end
defp maybe_add_group_members(collection, %Actor{type: _}), do: collection 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 defp maybe_add_followers(collection, %Actor{type: :Group, followers_url: followers_url}) do
[followers_url | collection] [followers_url | collection]
end end
defp maybe_add_followers(collection, %Actor{type: _}), do: collection 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(%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(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url]
defp add_in_reply_to(_), do: [] 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()} @spec extract_actors_from_mentions(list(), Actor.t(), atom()) :: {list(), list()}
defp extract_actors_from_mentions(mentions, actor, visibility) do defp extract_actors_from_mentions(mentions, actor, visibility) do
with mentioned_actors <- Enum.map(mentions, &process_mention/1), get_to_and_cc(actor, Enum.map(mentions, &process_mention/1), visibility)
addressed_actors <- get_addressed_actors(mentioned_actors, nil) do
get_to_and_cc(actor, addressed_actors, visibility)
end
end end
@spec extract_actors_from_event(Event.t()) :: %{
String.t() => list(String.t())
}
defp extract_actors_from_event(%Event{} = event) do defp extract_actors_from_event(%Event{} = event) do
with {to, cc} <- {to, cc} =
extract_actors_from_mentions( extract_actors_from_mentions(
event.mentions, event.mentions,
group_or_organizer_event(event), group_or_organizer_event(event),
event.visibility event.visibility
), )
{to, cc} <-
{to, cc} =
{to, {to,
Enum.uniq( Enum.uniq(
cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url) cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url)
)} do )}
%{"to" => to, "cc" => cc} %{"to" => to, "cc" => cc}
else
_ ->
%{"to" => [], "cc" => []}
end
end end
@spec group_or_organizer_event(Event.t()) :: Actor.t() @spec group_or_organizer_event(Event.t()) :: Actor.t()

View File

@ -19,10 +19,12 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
@max_jobs 20 @max_jobs 20
@spec init(any()) :: {:ok, any()}
def init(args) do def init(args) do
{:ok, args} {:ok, args}
end end
@spec start_link(any) :: GenServer.on_start()
def start_link(_) do def start_link(_) do
spawn(fn -> spawn(fn ->
# 1 minute # 1 minute
@ -39,6 +41,8 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
) )
end 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 def handle(:publish, activity) do
Logger.debug(inspect(activity)) Logger.debug(inspect(activity))
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
@ -46,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
with {:ok, %Actor{} = actor} <- with {:ok, %Actor{} = actor} <-
ActivityPubActor.get_or_fetch_actor_by_url(activity.data["actor"]) do ActivityPubActor.get_or_fetch_actor_by_url(activity.data["actor"]) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end) Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
ActivityPub.publish(actor, activity) ActivityPub.Publisher.publish(actor, activity)
end end
end end
@ -67,7 +71,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
end end
def handle(:publish_single_ap, params) do def handle(:publish_single_ap, params) do
ActivityPub.publish_one(params) ActivityPub.Publisher.publish_one(params)
end end
def handle(type, _) do def handle(type, _) do
@ -75,6 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
{:error, "Don't know what to do with this"} {:error, "Don't know what to do with this"}
end end
@spec enqueue(atom(), map(), pos_integer()) :: :ok | {:ok, any()} | {:error, any()}
def enqueue(type, payload, priority \\ 1) do def enqueue(type, payload, priority \\ 1) do
Logger.debug("enqueue something with type #{inspect(type)}") Logger.debug("enqueue something with type #{inspect(type)}")
@ -85,6 +90,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
end end
end end
@spec maybe_start_job(any(), any()) :: {any(), any()}
def maybe_start_job(running_jobs, queue) do def maybe_start_job(running_jobs, queue) do
if :sets.size(running_jobs) < @max_jobs && queue != [] do if :sets.size(running_jobs) < @max_jobs && queue != [] do
{{type, payload}, queue} = queue_pop(queue) {{type, payload}, queue} = queue_pop(queue)
@ -96,6 +102,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
end end
end end
@spec handle_cast(any(), any()) :: {:noreply, any()}
def handle_cast({:enqueue, type, payload, _priority}, state) def handle_cast({:enqueue, type, payload, _priority}, state)
when type in [:incoming_doc, :incoming_ap_doc] do when type in [:incoming_doc, :incoming_ap_doc] do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state %{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} {:noreply, state}
end end
@spec handle_info({:DOWN, any(), :process, any, any()}, any) :: {:noreply, map()}
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_running_jobs = :sets.del_element(ref, i_running_jobs) 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}}} {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end end
@spec enqueue_sorted(any(), any(), pos_integer()) :: any()
def enqueue_sorted(queue, element, priority) do def enqueue_sorted(queue, element, priority) do
[%{item: element, priority: priority} | queue] [%{item: element, priority: priority} | queue]
|> Enum.sort_by(fn %{priority: priority} -> priority end) |> Enum.sort_by(fn %{priority: priority} -> priority end)
end end
@spec queue_pop(list(any())) :: {any(), list(any())}
def queue_pop([%{item: element} | queue]) do def queue_pop([%{item: element} | queue]) do
{element, queue} {element, queue}
end end

View File

@ -19,9 +19,8 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
@spec fetch(String.t(), Keyword.t()) :: @spec fetch(String.t(), Keyword.t()) ::
{:ok, map()} {:ok, map()}
| {:ok, Tesla.Env.t()} | {:error,
| {:error, any()} :invalid_url | :http_gone | :http_error | :http_not_found | :content_not_json}
| {:error, :invalid_url}
def fetch(url, options \\ []) do def fetch(url, options \\ []) do
on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor()) on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())
date = Signature.generate_date_header() date = Signature.generate_date_header()
@ -35,7 +34,7 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
if address_valid?(url) do if address_valid?(url) do
case ActivityPubClient.get(client, 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, data}
{:ok, %Tesla.Env{status: 410}} -> {:ok, %Tesla.Env{status: 410}} ->
@ -46,8 +45,12 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
Logger.debug("Resource at #{url} is 404 Gone") Logger.debug("Resource at #{url} is 404 Gone")
{:error, :http_not_found} {:error, :http_not_found}
{:ok, %Tesla.Env{body: data}} when is_binary(data) ->
{:error, :content_not_json}
{:ok, %Tesla.Env{} = res} -> {:ok, %Tesla.Env{} = res} ->
{:error, res} Logger.debug("Resource returned bad HTTP code inspect #{res}")
{:error, :http_error}
end end
else else
{:error, :invalid_url} {:error, :invalid_url}
@ -55,30 +58,32 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
end end
@spec fetch_and_create(String.t(), Keyword.t()) :: @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 def fetch_and_create(url, options \\ []) do
with {:ok, data} when is_map(data) <- fetch(url, options), case fetch(url, options) do
{:origin_check, true} <- {:origin_check, origin_check?(url, data)}, {:ok, data} when is_map(data) ->
params <- %{ if origin_check?(url, data) do
case Transmogrifier.handle_incoming(%{
"type" => "Create", "type" => "Create",
"to" => data["to"], "to" => data["to"],
"cc" => data["cc"], "cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"], "actor" => data["actor"] || data["attributedTo"],
"attributedTo" => data["attributedTo"] || data["actor"], "attributedTo" => data["attributedTo"] || data["actor"],
"object" => data "object" => data
} do }) do
Transmogrifier.handle_incoming(params) {:ok, entity, structure} ->
{:ok, entity, structure}
{:error, error} when is_atom(error) ->
{:error, error}
:error ->
{:error, :transmogrifier_error}
end
else else
{:origin_check, false} ->
Logger.warn("Object origin check failed") Logger.warn("Object origin check failed")
{:error, "Object origin check failed"} {:error, :object_origin_check_failed}
end
# Returned content is not JSON
{:ok, data} when is_binary(data) ->
{:error, "Failed to parse content as JSON"}
{:error, :invalid_url} ->
{:error, :invalid_url}
{:error, err} -> {:error, err} ->
{:error, err} {:error, err}
@ -86,22 +91,23 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
end end
@spec fetch_and_update(String.t(), Keyword.t()) :: @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 def fetch_and_update(url, options \\ []) do
with {:ok, data} when is_map(data) <- fetch(url, options), case fetch(url, options) do
{:origin_check, true} <- {:origin_check, origin_check(url, data)}, {:ok, data} when is_map(data) ->
params <- %{ if origin_check(url, data) do
Transmogrifier.handle_incoming(%{
"type" => "Update", "type" => "Update",
"to" => data["to"], "to" => data["to"],
"cc" => data["cc"], "cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"], "actor" => data["actor"] || data["attributedTo"],
"attributedTo" => data["attributedTo"] || data["actor"], "attributedTo" => data["attributedTo"] || data["actor"],
"object" => data "object" => data
} do })
Transmogrifier.handle_incoming(params)
else else
{:origin_check, false} -> Logger.warn("Object origin check failed")
{:error, "Object origin check failed"} {:error, :object_origin_check_failed}
end
{:error, err} -> {:error, err} ->
{: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
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.WebFinger alias Mobilizon.Federation.WebFinger
alias Mobilizon.Service.Workers.Background alias Mobilizon.Service.Workers.Background
@ -118,7 +117,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
{:ok, Oban.Job.t()} {:ok, Oban.Job.t()}
| {:error, Ecto.Changeset.t()} | {:error, Ecto.Changeset.t()}
| {:error, :bad_url} | {:error, :bad_url}
| {:error, Mobilizon.Federation.ActivityPub.Actor.make_actor_errors()} | {:error, ActivityPubActor.make_actor_errors()}
| {:error, :no_internal_relay_actor} | {:error, :no_internal_relay_actor}
| {:error, :url_nil} | {:error, :url_nil}
def refresh(address) do def refresh(address) do
@ -145,7 +144,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
{object, object_id} <- fetch_object(object), {object, object_id} <- fetch_object(object),
id <- "#{object_id}/announces/#{actor_id}" do id <- "#{object_id}/announces/#{actor_id}" do
Logger.info("Publishing activity #{id} to all relays") Logger.info("Publishing activity #{id} to all relays")
ActivityPub.announce(actor, object, id, true, false) Actions.Announce.announce(actor, object, id, true, false)
else else
e -> e ->
Logger.error("Error while getting local instance actor: #{inspect(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.Todos.{Todo, TodoList}
alias Mobilizon.Federation.ActivityPub 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.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.Types.Ownable alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
@ -50,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
local: false local: false
} }
ActivityPub.flag(params, false) Actions.Flag.flag(params, false)
end end
end end
@ -77,10 +77,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Activity{} = activity, entity} <- {:ok, %Activity{} = activity, entity} <-
(if is_data_for_comment_or_discussion?(object_data) do (if is_data_for_comment_or_discussion?(object_data) do
Logger.debug("Chosing to create a regular comment") Logger.debug("Chosing to create a regular comment")
ActivityPub.create(:comment, object_data, false) Actions.Create.create(:comment, object_data, false)
else else
Logger.debug("Chosing to initialize or add a comment to a conversation") 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 end) do
{:ok, activity, entity} {:ok, activity, entity}
else else
@ -110,7 +110,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object |> Converter.Event.as_to_model_data(), object |> Converter.Event.as_to_model_data(),
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)}, {:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Event{} = event} <- {:ok, %Activity{} = activity, %Event{} = event} <-
ActivityPub.create(:event, object_data, false) do Actions.Create.create(:event, object_data, false) do
{:ok, activity, event} {:ok, activity, event}
else else
{:existing_event, %Event{} = event} -> {:ok, nil, event} {: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{type: :Group} = group <- Actors.get_actor(object_data.parent_id),
%Actor{} = actor <- Actors.get_actor(object_data.actor_id), %Actor{} = actor <- Actors.get_actor(object_data.actor_id),
{:ok, %Activity{} = activity, %Member{} = member} <- {:ok, %Activity{} = activity, %Member{} = member} <-
ActivityPub.join(group, actor, false, %{ Actions.Join.join(group, actor, false, %{
url: object_data.url, url: object_data.url,
metadata: %{role: object_data.role} metadata: %{role: object_data.role}
}) do }) do
@ -173,7 +173,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:existing_post, nil} <- {:existing_post, nil} <-
{:existing_post, Posts.get_post_by_url(object_data.url)}, {:existing_post, Posts.get_post_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Post{} = post} <- {:ok, %Activity{} = activity, %Post{} = post} <-
ActivityPub.create(:post, object_data, false) do Actions.Create.create(:post, object_data, false) do
{:ok, activity, post} {:ok, activity, post}
else else
{:existing_post, %Post{} = post} -> {:existing_post, %Post{} = post} ->
@ -198,7 +198,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, nil, comment} {:ok, nil, comment}
{:ok, entity} -> {:ok, entity} ->
ActivityPub.delete(entity, Relay.get_actor(), false) Actions.Delete.delete(entity, Relay.get_actor(), false)
end end
end end
@ -207,7 +207,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
) do ) do
with {:ok, %Actor{} = followed} <- ActivityPubActor.get_or_fetch_actor_by_url(followed, true), 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, %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} {:ok, activity, object}
else else
{:error, :person_no_follow} -> {:error, :person_no_follow} ->
@ -233,7 +234,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data when is_map(object_data) <- object_data when is_map(object_data) <-
object |> Converter.TodoList.as_to_model_data(), object |> Converter.TodoList.as_to_model_data(),
{:ok, %Activity{} = activity, %TodoList{} = todo_list} <- {: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} {:ok, activity, todo_list}
else else
{:error, :group_not_found} -> :error {:error, :group_not_found} -> :error
@ -252,7 +255,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <- object_data <-
object |> Converter.Todo.as_to_model_data(), object |> Converter.Todo.as_to_model_data(),
{:ok, %Activity{} = activity, %Todo{} = todo} <- {:ok, %Activity{} = activity, %Todo{} = todo} <-
ActivityPub.create(:todo, object_data, false) do Actions.Create.create(:todo, object_data, false) do
{:ok, activity, todo} {:ok, activity, todo}
else else
{:existing_todo, %Todo{} = todo} -> {:ok, nil, todo} {:existing_todo, %Todo{} = todo} -> {:ok, nil, todo}
@ -277,7 +280,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:member, true} <- {:member, true} <-
{:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)}, {:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)},
{:ok, %Activity{} = activity, %Resource{} = resource} <- {:ok, %Activity{} = activity, %Resource{} = resource} <-
ActivityPub.create(:resource, object_data, false) do Actions.Create.create(:resource, object_data, false) do
{:ok, activity, resource} {:ok, activity, resource}
else else
{:existing_resource, %Resource{} = resource} -> {:existing_resource, %Resource{} = resource} ->
@ -388,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <- object_data <-
object |> Converter.Actor.as_to_model_data(), object |> Converter.Actor.as_to_model_data(),
{:ok, %Activity{} = activity, %Actor{} = new_actor} <- {: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} {:ok, activity, new_actor}
else else
e -> e ->
@ -416,7 +419,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Utils.origin_check?(actor_url, update_data) || Utils.origin_check?(actor_url, update_data) ||
Permission.can_update_group_object?(actor, old_event)}, Permission.can_update_group_object?(actor, old_event)},
{:ok, %Activity{} = activity, %Event{} = new_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} {:ok, activity, new_event}
else else
_e -> _e ->
@ -438,7 +441,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- transform_object_data_for_discussion(object_data), object_data <- transform_object_data_for_discussion(object_data),
{:ok, %Activity{} = activity, new_entity} <- {: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} {:ok, activity, new_entity}
else else
_e -> _e ->
@ -461,7 +464,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Utils.origin_check?(actor_url, update_data["object"]) || Utils.origin_check?(actor_url, update_data["object"]) ||
Permission.can_update_group_object?(actor, old_post)}, Permission.can_update_group_object?(actor, old_post)},
{:ok, %Activity{} = activity, %Post{} = new_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} {:ok, activity, new_post}
else else
{:origin_check, _} -> {:origin_check, _} ->
@ -489,7 +492,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Utils.origin_check?(actor_url, update_data) || Utils.origin_check?(actor_url, update_data) ||
Permission.can_update_group_object?(actor, old_resource)}, Permission.can_update_group_object?(actor, old_resource)},
{:ok, %Activity{} = activity, %Resource{} = new_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} {:ok, activity, new_resource}
else else
_e -> _e ->
@ -510,7 +513,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <- Converter.Member.as_to_model_data(object), object_data <- Converter.Member.as_to_model_data(object),
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
{:ok, %Activity{} = activity, new_entity} <- {: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} {:ok, activity, new_entity}
else else
_e -> _e ->
@ -527,7 +530,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with object_url <- Utils.get_url(object), with object_url <- Utils.get_url(object),
{:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do {: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 else
{:ok, %Tombstone{} = tombstone} -> {:ok, %Tombstone{} = tombstone} ->
{:ok, nil, 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, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id), {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
{:ok, activity, object} <- {: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} {:ok, activity, object}
else else
_e -> :error _e -> :error
@ -568,7 +577,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with {:ok, %Actor{domain: nil} = followed} <- with {:ok, %Actor{domain: nil} = followed} <-
ActivityPubActor.get_or_fetch_actor_by_url(followed), ActivityPubActor.get_or_fetch_actor_by_url(followed),
{:ok, %Actor{} = follower} <- ActivityPubActor.get_or_fetch_actor_by_url(follower), {: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} {:ok, activity, object}
else else
e -> e ->
@ -593,7 +603,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, {:origin_check,
Utils.origin_check_from_id?(actor_url, object_id) || Utils.origin_check_from_id?(actor_url, object_id) ||
Permission.can_delete_group_object?(actor, object)}, 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} {:ok, activity, object}
else else
{:origin_check, false} -> {:origin_check, false} ->
@ -637,7 +647,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, {:origin_check,
Utils.origin_check?(actor_url, data) || Utils.origin_check?(actor_url, data) ||
Permission.can_update_group_object?(actor, old_resource)}, 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} {:ok, activity, new_resource}
else else
e -> e ->
@ -665,7 +676,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object <- Utils.get_url(object), object <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_url(object), {:ok, object} <- ActivityPub.fetch_object_from_url(object),
{:ok, activity, object} <- {:ok, activity, object} <-
ActivityPub.join(object, actor, false, %{ Actions.Join.join(object, actor, false, %{
url: id, url: id,
metadata: %{message: Map.get(data, "participationMessage")} metadata: %{message: Map.get(data, "participationMessage")}
}) do }) do
@ -682,7 +693,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor), {:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor),
object <- Utils.get_url(object), object <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_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} {:ok, activity, object}
else else
{:only_organizer, true} -> {:only_organizer, true} ->
@ -714,7 +725,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Actor{} = target} <- {:ok, %Actor{} = target} <-
target |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(), target |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(),
{:ok, activity, %Member{} = member} <- {: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} {:ok, activity, member}
end end
end end
@ -734,7 +745,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:is_admin, Actors.get_member(moderator_id, group_id)}, {:is_admin, Actors.get_member(moderator_id, group_id)},
{:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <- {:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <-
{:is_member, Actors.get_member(person_id, group_id)} do {:is_member, Actors.get_member(person_id, group_id)} do
ActivityPub.remove(member, group, moderator, false) Actions.Remove.remove(member, group, moderator, false)
else else
{:is_admin, {:ok, %Member{}}} -> {:is_admin, {:ok, %Member{}}} ->
Logger.warn( Logger.warn(
@ -786,7 +797,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:follow, get_follow(follow_object)}, {:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id}, {:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <- {:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
ActivityPub.accept( Actions.Accept.accept(
:follow, :follow,
follow, follow,
false false
@ -824,7 +835,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:follow, get_follow(follow_object)}, {:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id}, {:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <- {:ok, activity, _} <-
ActivityPub.reject(:follow, follow) do Actions.Reject.reject(:follow, follow) do
{:ok, activity, follow} {:ok, activity, follow}
else else
{:follow, _err} -> {:follow, _err} ->
@ -879,7 +890,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:can_accept_event_join, true} <- {:can_accept_event_join, true} <-
{:can_accept_event_join, can_manage_event?(actor_accepting, event)}, {:can_accept_event_join, can_manage_event?(actor_accepting, event)},
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <- {:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
ActivityPub.accept( Actions.Accept.accept(
:join, :join,
participant, participant,
false false
@ -911,7 +922,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] 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 # Or maybe for groups it's the group that sends the Accept activity
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <- with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
ActivityPub.accept( Actions.Accept.accept(
type, type,
member, member,
false false
@ -929,7 +940,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:can_accept_event_reject, true} <- {:can_accept_event_reject, true} <-
{:can_accept_event_reject, can_manage_event?(actor_accepting, event)}, {:can_accept_event_reject, can_manage_event?(actor_accepting, event)},
{:ok, activity, participant} <- {:ok, activity, participant} <-
ActivityPub.reject(:join, participant, false), Actions.Reject.reject(:join, participant, false),
:ok <- Participation.send_emails_to_local_user(participant) do :ok <- Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant} {:ok, activity, participant}
else else
@ -960,7 +971,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:invite, get_member(invite_object)}, {:invite, get_member(invite_object)},
{:same_actor, true} <- {:same_actor, actor_rejecting.id == actor_id}, {:same_actor, true} <- {:same_actor, actor_rejecting.id == actor_id},
{:ok, activity, member} <- {:ok, activity, member} <-
ActivityPub.reject(:invite, member, false) do Actions.Reject.reject(:invite, member, false) do
{:ok, activity, member} {:ok, activity, member}
end end
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 # Before 1.0.4 the object of a "Remove" activity was an actor's URL
# instead of the member's URL. # instead of the member's URL.
# TODO: Remove in 1.2 # 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 defp get_remove_object(object) do
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
{:ok, %Member{actor: %Actor{id: person_id}}} -> {:ok, person_id} {: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 organizer_actor_id == actor_id
end end
defp can_manage_event?(_actor, _event) do defp can_manage_event?(%Actor{} = _actor, %Event{} = _event) do
false false
end end
end end

View File

@ -2,8 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole} alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay}
alias Mobilizon.Federation.ActivityPub.{Audience, Permission, Relay}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
@ -68,7 +67,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@impl Entity @impl Entity
@spec delete(Actor.t(), Actor.t(), boolean, map) :: @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( def delete(
%Actor{ %Actor{
followers_url: followers_url, followers_url: followers_url,
@ -245,7 +244,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
Mobilizon.Actors.get_default_member_role(group) == :member && Mobilizon.Actors.get_default_member_role(group) == :member &&
role == :member -> role == :member ->
{:accept, {:accept,
ActivityPub.accept( Actions.Accept.accept(
:join, :join,
member, member,
true, 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") Logger.debug("Target doesn't manually approves followers, we can accept right away")
{:accept, {:accept,
ActivityPub.accept( Actions.Accept.accept(
:follow, :follow,
follower, follower,
true, true,

View File

@ -70,7 +70,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
@impl Entity @impl Entity
@spec delete(Comment.t(), Actor.t(), boolean, map()) :: @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( def delete(
%Comment{url: url, id: comment_id}, %Comment{url: url, id: comment_id},
%Actor{} = actor, %Actor{} = actor,
@ -208,7 +208,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
end end
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?(%{ defp event_allows_commenting?(%{
actor_id: actor_id, actor_id: actor_id,
event: %Event{ event: %Event{

View File

@ -17,84 +17,100 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
@behaviour Entity @behaviour Entity
@impl 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 def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do
with args <- prepare_args(args), args = prepare_args(args)
%Discussion{} = discussion <- Discussions.get_discussion(discussion_id),
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <- case Discussions.get_discussion(discussion_id) do
Discussions.reply_to_discussion(discussion, args), %Discussion{} = discussion ->
{:ok, _} <- case Discussions.reply_to_discussion(discussion, args) do
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} ->
DiscussionActivity.insert_activity(discussion, DiscussionActivity.insert_activity(discussion,
subject: "discussion_replied", subject: "discussion_replied",
actor_id: Map.get(args, :creator_id, args.actor_id) 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), case Discussions.get_comment_with_preload(last_comment_id) do
comment_as_data <- Convertible.model_to_as(last_comment), %Comment{} = last_comment ->
audience <- maybe_publish_graphql_subscription(discussion)
Audience.get_audience(discussion), comment_as_data = Convertible.model_to_as(last_comment)
create_data <- audience = Audience.get_audience(discussion)
make_create_data(comment_as_data, Map.merge(audience, additional)) do create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
{:ok, discussion, create_data} {: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
end end
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()}
def create(args, additional) do def create(args, additional) do
with args <- prepare_args(args), args = prepare_args(args)
{:ok, %Discussion{} = discussion} <-
Discussions.create_discussion(args), case Discussions.create_discussion(args) do
{:ok, _} <- {:ok, %Discussion{} = discussion} ->
DiscussionActivity.insert_activity(discussion, subject: "discussion_created"), DiscussionActivity.insert_activity(discussion, subject: "discussion_created")
discussion_as_data <- Convertible.model_to_as(discussion), discussion_as_data = Convertible.model_to_as(discussion)
audience <- audience = Audience.get_audience(discussion)
Audience.get_audience(discussion), create_data = make_create_data(discussion_as_data, Map.merge(audience, additional))
create_data <-
make_create_data(discussion_as_data, Map.merge(audience, additional)) do
{:ok, discussion, create_data} {:ok, discussion, create_data}
{:error, _, %Ecto.Changeset{} = err, _} ->
{:error, err}
end end
end end
@impl Entity @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 def update(%Discussion{} = old_discussion, args, additional) do
with {:ok, %Discussion{} = new_discussion} <- case Discussions.update_discussion(old_discussion, args) do
Discussions.update_discussion(old_discussion, args), {:ok, %Discussion{} = new_discussion} ->
{:ok, _} <-
DiscussionActivity.insert_activity(new_discussion, DiscussionActivity.insert_activity(new_discussion,
subject: "discussion_renamed", subject: "discussion_renamed",
old_discussion: old_discussion old_discussion: old_discussion
), )
{:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"),
discussion_as_data <- Convertible.model_to_as(new_discussion), Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}")
audience <- discussion_as_data = Convertible.model_to_as(new_discussion)
Audience.get_audience(new_discussion), audience = Audience.get_audience(new_discussion)
update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do update_data = make_update_data(discussion_as_data, Map.merge(audience, additional))
{:ok, new_discussion, update_data} {:ok, new_discussion, update_data}
else
err -> {:error, %Ecto.Changeset{} = err} ->
Logger.error("Something went wrong while creating an update activity") {:error, err}
Logger.debug(inspect(err))
err
end end
end end
@impl Entity @impl Entity
@spec delete(Discussion.t(), Actor.t(), boolean, map()) :: @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( def delete(
%Discussion{actor: group, url: url} = discussion, %Discussion{actor: group, url: url} = discussion,
%Actor{} = actor, %Actor{} = actor,
_local, _local,
_additionnal _additionnal
) do ) do
with {:ok, _} <- Discussions.delete_discussion(discussion), case Discussions.delete_discussion(discussion) do
{:ok, _} <- {:error, _, %Ecto.Changeset{} = err, _} ->
{:error, err}
{:ok, %{comments: {_, _}}} ->
DiscussionActivity.insert_activity(discussion, DiscussionActivity.insert_activity(discussion,
subject: "discussion_deleted", subject: "discussion_deleted",
moderator: actor moderator: actor
) do )
# This is just fake # This is just fake
activity_data = %{ activity_data = %{
"type" => "Delete", "type" => "Delete",

View File

@ -15,7 +15,7 @@ alias Mobilizon.Federation.ActivityPub.Types.{
} }
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.Event
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
@ -28,27 +28,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
@moduledoc """ @moduledoc """
ActivityPub entity behaviour ActivityPub entity behaviour
""" """
@type t :: %{id: String.t(), url: String.t()} @type t :: %{required(:id) => any(), optional(:url) => String.t(), optional(atom()) => any()}
@type entities ::
Actor.t()
| Member.t()
| Event.t()
| Participant.t()
| Comment.t()
| Discussion.t()
| Post.t()
| Resource.t()
| Todo.t()
| TodoList.t()
@callback create(data :: any(), additionnal :: map()) :: @callback create(data :: any(), additionnal :: map()) ::
{:ok, t(), ActivityStream.t()} | {:error, any()} {: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()} {: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()} {:ok, ActivityStream.t(), Actor.t(), t()} | {:error, any()}
end end
@ -57,47 +45,61 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
ActivityPub entity Managable protocol. ActivityPub entity Managable protocol.
""" """
@spec update(Entity.t(), map(), map()) ::
{:ok, Entity.t(), ActivityStream.t()} | {:error, any()}
@doc """ @doc """
Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it 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) def update(entity, attrs, additionnal)
@doc "Deletes an entity and returns the activitystream representation for it"
@spec delete(Entity.t(), Actor.t(), boolean(), map()) :: @spec delete(Entity.t(), Actor.t(), boolean(), map()) ::
{:ok, ActivityStream.t(), Actor.t(), Entity.t()} | {:error, any()} {: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) def delete(entity, actor, local, additionnal)
end end
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do 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" @doc "Returns an eventual group for the entity"
@spec group_actor(Entity.t()) :: Actor.t() | nil
def group_actor(entity) def group_actor(entity)
@spec actor(Entity.t()) :: Actor.t() | nil
@doc "Returns the actor for the entity" @doc "Returns the actor for the entity"
@spec actor(Entity.t()) :: Actor.t() | nil
def actor(entity) def actor(entity)
@doc """
Returns the list of permissions for an entity
"""
@spec permissions(Entity.t()) :: Permission.t() @spec permissions(Entity.t()) :: Permission.t()
def permissions(entity) def permissions(entity)
end end
defimpl Managable, for: Event do 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 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 defdelegate delete(entity, actor, local, additionnal), to: Events
end end
defimpl Ownable, for: Event do defimpl Ownable, for: Event do
@spec group_actor(Event.t()) :: Actor.t() | nil
defdelegate group_actor(entity), to: Events defdelegate group_actor(entity), to: Events
@spec actor(Event.t()) :: Actor.t() | nil
defdelegate actor(entity), to: Events defdelegate actor(entity), to: Events
@spec permissions(Event.t()) :: Permission.t()
defdelegate permissions(entity), to: Events defdelegate permissions(entity), to: Events
end end
defimpl Managable, for: Comment do 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 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 defdelegate delete(entity, actor, local, additionnal), to: Comments
end end

View File

@ -4,9 +4,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events, as: EventsManager alias Mobilizon.Events, as: EventsManager
alias Mobilizon.Events.{Event, Participant, ParticipantRole} alias Mobilizon.Events.{Event, Participant, ParticipantRole}
alias Mobilizon.Federation.{ActivityPub, ActivityStream} alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission}
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
{:error, _step, %Ecto.Changeset{} = err, _} -> {:error, _step, %Ecto.Changeset{} = err, _} ->
{:error, err} {:error, err}
{:error, err} -> {:error, %Ecto.Changeset{} = err} ->
{:error, err} {:error, err}
end end
end end
@ -89,11 +89,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
Share.delete_all_by_uri(event.url) Share.delete_all_by_uri(event.url)
{:ok, Map.merge(activity_data, audience), actor, event} {:ok, Map.merge(activity_data, audience), actor, event}
{:error, err} -> {:error, %Ecto.Changeset{} = err} ->
{:error, err} {:error, err}
end end
{:error, err} -> {:error, %Ecto.Changeset{} = err} ->
{:error, err} {:error, err}
end end
end end
@ -166,12 +166,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
@spec check_attendee_capacity?(Event.t()) :: boolean @spec check_attendee_capacity?(Event.t()) :: boolean
defp check_attendee_capacity?(%Event{options: options} = event) do defp check_attendee_capacity?(%Event{options: options} = event) do
with maximum_attendee_capacity <- maximum_attendee_capacity = Map.get(options, :maximum_attendee_capacity) || 0
Map.get(options, :maximum_attendee_capacity) || 0 do
maximum_attendee_capacity == 0 || maximum_attendee_capacity == 0 ||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
end end
end
# Set the participant to approved if the default role for new participants is :participant # Set the participant to approved if the default role for new participants is :participant
@spec approve_if_default_role_is_participant( @spec approve_if_default_role_is_participant(
@ -211,7 +210,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
Mobilizon.Events.get_default_participant_role(event) == :participant && Mobilizon.Events.get_default_participant_role(event) == :participant &&
role == :participant -> role == :participant ->
{:accept, {:accept,
ActivityPub.accept( Actions.Accept.accept(
:join, :join,
participant, participant,
true, true,

View File

@ -2,7 +2,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member, MemberRole} 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.Federation.ActivityStream.Convertible
alias Mobilizon.Service.Activity.Member, as: MemberActivity alias Mobilizon.Service.Activity.Member, as: MemberActivity
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
@ -74,7 +75,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
_additionnal _additionnal
) do ) do
Logger.debug("Deleting a member") 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 end
@spec actor(Member.t()) :: Actor.t() | nil @spec actor(Member.t()) :: Actor.t() | nil

View File

@ -1,39 +1,47 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Reports do defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Discussions, Reports} alias Mobilizon.{Actors, Discussions, Events, Reports}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Reports.Report alias Mobilizon.Reports.Report
alias Mobilizon.Service.Formatter.HTML alias Mobilizon.Service.Formatter.HTML
require Logger 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 def flag(args, local \\ false, _additional \\ %{}) do
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)}, with {:ok, %Report{} = report} <- args |> prepare_args_for_report() |> Reports.create_report() do
{:create_report, {:ok, %Report{} = report}} <- report_as_data = Convertible.model_to_as(report)
{:create_report, Reports.create_report(args)}, cc = if(local, do: [report.reported.url], else: [])
report_as_data <- Convertible.model_to_as(report), report_as_data = Map.merge(report_as_data, %{"to" => [], "cc" => cc})
cc <- if(local, do: [report.reported.url], else: []), {:ok, report, report_as_data}
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do
{report, report_as_data}
end end
end end
@spec prepare_args_for_report(map()) :: map() @spec prepare_args_for_report(map()) :: map()
defp prepare_args_for_report(args) do defp prepare_args_for_report(args) do
with {:reporter, %Actor{} = reporter_actor} <- %Actor{} = reporter_actor = Actors.get_actor!(args.reporter_id)
{:reporter, Actors.get_actor!(args.reporter_id)}, %Actor{} = reported_actor = Actors.get_actor!(args.reported_id)
{:reported, %Actor{} = reported_actor} <- content = HTML.strip_tags(args.content)
{:reported, Actors.get_actor!(args.reported_id)},
content <- HTML.strip_tags(args.content), event_id = Map.get(args, :event_id)
event <- Discussions.get_comment(Map.get(args, :event_id)),
{:get_report_comments, comments} <- event =
{:get_report_comments, 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( Discussions.list_comments_by_actor_and_ids(
reported_actor.id, reported_actor.id,
Map.get(args, :comments_ids, []) Map.get(args, :comments_ids, [])
)} do )
Map.merge(args, %{ Map.merge(args, %{
reporter: reporter_actor, reporter: reporter_actor,
reported: reported_actor, reported: reported_actor,
@ -43,4 +51,3 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
}) })
end end
end end
end

View File

@ -17,7 +17,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
@behaviour Entity @behaviour Entity
@impl 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 def create(%{type: type} = args, additional) do
args = args =
case type do case type do
@ -37,17 +39,18 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
with {:ok, with {:ok,
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <- %Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
Resources.create_resource(args), Resources.create_resource(args),
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_created"), {:ok, %Actor{} = group, %Actor{url: creator_url} = creator} <-
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), group_and_creator(group_id, creator_id) do
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), ResourceActivity.insert_activity(resource, subject: "resource_created")
resource_as_data <- resource_as_data = Convertible.model_to_as(%{resource | actor: group, creator: creator})
Convertible.model_to_as(%{resource | actor: group, creator: creator}),
audience <- %{ audience = %{
"to" => [group.members_url], "to" => [group.members_url],
"cc" => [], "cc" => [],
"actor" => creator_url, "actor" => creator_url,
"attributedTo" => [creator_url] "attributedTo" => [creator_url]
} do }
create_data = create_data =
case parent_id do case parent_id do
nil -> nil ->
@ -60,15 +63,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
end end
{:ok, resource, create_data} {:ok, resource, create_data}
else
err ->
Logger.debug(inspect(err))
err
end end
end end
@impl Entity @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( def update(
%Resource{parent_id: old_parent_id} = old_resource, %Resource{parent_id: old_parent_id} = old_resource,
%{parent_id: parent_id} = args, %{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 def update(%Resource{} = old_resource, %{title: title} = _args, additional) do
with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <- with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
Resources.update_resource(old_resource, %{title: title}), Resources.update_resource(old_resource, %{title: title}),
{:ok, _} <- {:ok, %Actor{} = group, %Actor{url: creator_url}} <-
group_and_creator(group_id, creator_id) do
ResourceActivity.insert_activity(resource, ResourceActivity.insert_activity(resource,
subject: "resource_renamed", subject: "resource_renamed",
old_resource: old_resource 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})
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group}), audience = %{
audience <- %{
"to" => [group.members_url], "to" => [group.members_url],
"cc" => [], "cc" => [],
"actor" => creator_url, "actor" => creator_url,
"attributedTo" => [creator_url] "attributedTo" => [creator_url]
}, }
update_data <-
make_update_data(resource_as_data, Map.merge(audience, additional)) do update_data = make_update_data(resource_as_data, Map.merge(audience, additional))
{:ok, resource, update_data} {:ok, resource, update_data}
else
err ->
Logger.debug(inspect(err))
err
end end
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( def move(
%Resource{parent_id: old_parent_id} = old_resource, %Resource{parent_id: old_parent_id} = old_resource,
%{parent_id: _new_parent_id} = args, %{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{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
resource} <- resource} <-
Resources.update_resource(old_resource, args), Resources.update_resource(old_resource, args),
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_moved"), {:ok, old_parent, new_parent} <- parents(old_parent_id, new_parent_id),
old_parent <- Resources.get_resource(old_parent_id), {:ok, %Actor{} = group, %Actor{url: creator_url}} <-
new_parent <- Resources.get_resource(new_parent_id), group_and_creator(group_id, creator_id) do
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), ResourceActivity.insert_activity(resource, subject: "resource_moved")
%Actor{url: creator_url} <- Actors.get_actor(creator_id), resource_as_data = Convertible.model_to_as(%{resource | actor: group})
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group}), audience = %{
audience <- %{
"to" => [group.members_url], "to" => [group.members_url],
"cc" => [], "cc" => [],
"actor" => creator_url, "actor" => creator_url,
"attributedTo" => [creator_url] "attributedTo" => [creator_url]
}, }
move_data <-
move_data =
make_move_data( make_move_data(
resource_as_data, resource_as_data,
old_parent, old_parent,
new_parent, new_parent,
Map.merge(audience, additional) Map.merge(audience, additional)
) do )
{:ok, resource, move_data} {:ok, resource, move_data}
else
err ->
Logger.debug(inspect(err))
err
end end
end end
@impl Entity @impl Entity
@spec delete(Resource.t(), Actor.t(), boolean, map()) :: @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( def delete(
%Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource, %Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
@ -165,10 +166,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
"to" => [members_url] "to" => [members_url]
} }
with {:ok, _resource} <- Resources.delete_resource(resource), case Resources.delete_resource(resource) do
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_deleted"), {:ok, _resource} ->
{:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do ResourceActivity.insert_activity(resource, subject: "resource_deleted")
Cachex.del(:activity_pub, "resource_#{resource.id}")
{:ok, activity_data, actor, resource} {:ok, activity_data, actor, resource}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@ -183,4 +188,28 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
def permissions(%Resource{}) do def permissions(%Resource{}) do
%Permission{access: :member, create: :member, update: :member, delete: :member} %Permission{access: :member, create: :member, update: :member, delete: :member}
end 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 end

View File

@ -18,33 +18,32 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
| {:error, :group_not_found | Ecto.Changeset.t()} | {:error, :group_not_found | Ecto.Changeset.t()}
def create(args, additional) do def create(args, additional) do
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args), 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), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}), todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group})
audience <- %{"to" => [group.members_url], "cc" => []}, audience = %{"to" => [group.members_url], "cc" => []}
create_data <- create_data = make_create_data(todo_list_as_data, Map.merge(audience, additional))
make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
{:ok, todo_list, create_data} {:ok, todo_list, create_data}
end end
end end
@impl Entity @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 def update(%TodoList{} = old_todo_list, args, additional) do
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- with {:ok, %TodoList{actor_id: group_id} = todo_list} <-
Todos.update_todo_list(old_todo_list, args), Todos.update_todo_list(old_todo_list, args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
todo_list_as_data <- todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group})
Convertible.model_to_as(%{todo_list | actor: group}), audience = %{"to" => [group.members_url], "cc" => []}
audience <- %{"to" => [group.members_url], "cc" => []}, update_data = make_update_data(todo_list_as_data, Map.merge(audience, additional))
update_data <-
make_update_data(todo_list_as_data, Map.merge(audience, additional)) do
{:ok, todo_list, update_data} {:ok, todo_list, update_data}
end end
end end
@impl Entity @impl Entity
@spec delete(TodoList.t(), Actor.t(), boolean(), map()) :: @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( def delete(
%TodoList{url: url, actor: %Actor{url: group_url}} = todo_list, %TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
@ -61,9 +60,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
"to" => [group_url] "to" => [group_url]
} }
with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list), case Todos.delete_todo_list(todo_list) do
{:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do {:ok, _todo_list} ->
Cachex.del(:activity_pub, "todo_list_#{todo_list.id}")
{:ok, activity_data, actor, todo_list} {:ok, activity_data, actor, todo_list}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end

View File

@ -1,5 +1,7 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Todos do defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
@moduledoc false @moduledoc """
ActivityPub type handler for Todos
"""
alias Mobilizon.{Actors, Todos} alias Mobilizon.{Actors, Todos}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Permission
@ -13,41 +15,75 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
@behaviour Entity @behaviour Entity
@impl 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 def create(args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <- with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
Todos.create_todo(args), Todos.create_todo(args),
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id), {:ok, %Actor{} = creator, %TodoList{} = todo_list, %Actor{} = group} <-
%Actor{} = creator <- Actors.get_actor(creator_id), creator_todo_list_and_group(creator_id, todo_list_id) do
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), todo = %{todo | todo_list: %{todo_list | actor: group}, creator: creator}
todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator}, todo_as_data = Convertible.model_to_as(todo)
todo_as_data <- audience = %{"to" => [group.members_url], "cc" => []}
Convertible.model_to_as(todo), create_data = make_create_data(todo_as_data, Map.merge(audience, additional))
audience <- %{"to" => [group.members_url], "cc" => []},
create_data <-
make_create_data(todo_as_data, Map.merge(audience, additional)) do
{:ok, todo, create_data} {:ok, todo, create_data}
end end
end end
@impl Entity @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 def update(%Todo{} = old_todo, args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args), 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, %TodoList{} = todo_list, %Actor{} = group} <- todo_list_and_group(todo_list_id) do
{: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}})
todo_as_data <- audience = %{"to" => [group.members_url], "cc" => []}
Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}), update_data = make_update_data(todo_as_data, Map.merge(audience, additional))
audience <- %{"to" => [group.members_url], "cc" => []},
update_data <-
make_update_data(todo_as_data, Map.merge(audience, additional)) do
{:ok, todo, update_data} {:ok, todo, update_data}
end end
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 @impl Entity
@spec delete(Todo.t(), Actor.t(), boolean(), map()) :: @spec delete(Todo.t(), Actor.t(), any(), any()) ::
{:ok, ActivityStream.t(), Actor.t(), Todo.t()} {:ok, ActivityStream.t(), Actor.t(), Todo.t()} | {:error, Ecto.Changeset.t()}
def delete( def delete(
%Todo{url: url, creator: %Actor{url: group_url}} = todo, %Todo{url: url, creator: %Actor{url: group_url}} = todo,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
@ -60,13 +96,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
"actor" => actor_url, "actor" => actor_url,
"type" => "Delete", "type" => "Delete",
"object" => Convertible.model_to_as(url), "object" => Convertible.model_to_as(url),
"id" => url <> "/delete", "id" => "#{url}/delete",
"to" => [group_url] "to" => [group_url]
} }
with {:ok, _todo} <- Todos.delete_todo(todo), case Todos.delete_todo(todo) do
{:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do {:ok, _todo} ->
Cachex.del(:activity_pub, "todo_#{todo.id}")
{:ok, activity_data, actor, todo} {:ok, activity_data, actor, todo}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@ -84,7 +124,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
end end
end end
@spec permissions(TodoList.t()) :: Permission.t() @spec permissions(Todo.t()) :: Permission.t()
def permissions(%Todo{}) do def permissions(%Todo{}) do
%Permission{access: :member, create: :member, update: :member, delete: :member} %Permission{access: :member, create: :member, update: :member, delete: :member}
end end

View File

@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission 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: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id)
def actor(%Tombstone{actor_id: actor_id}) when not is_nil(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 def actor(_), do: nil
@spec group_actor(any()) :: nil
def group_actor(_), do: nil def group_actor(_), do: nil
@spec permissions(any()) :: Permission.t()
def permissions(_) do def permissions(_) do
%Permission{access: nil, create: nil, update: nil, delete: nil} %Permission{access: nil, create: nil, update: nil, delete: nil}
end end

View File

@ -12,8 +12,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Medias.Media alias Mobilizon.Medias.Media
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Federator, Relay}
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityStream.Converter alias Mobilizon.Federation.ActivityStream.Converter
alias Mobilizon.Federation.HTTPSignatures alias Mobilizon.Federation.HTTPSignatures
@ -23,6 +22,26 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@actor_types ["Group", "Person", "Application"] @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, # 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. # 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 @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}}, %Activity{data: %{"object" => object}},
%Actor{url: attributed_to_url} %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) do_maybe_relay_if_group_activity(object, attributed_to_url)
end end
@ -166,7 +185,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
case Actors.get_local_group_by_url(attributed_to) do case Actors.get_local_group_by_url(attributed_to) do
%Actor{} = group -> %Actor{} = group ->
case ActivityPub.announce(group, object, id, true, false) do case Actions.Announce.announce(group, object, id, true, false) do
{:ok, _activity, _object} -> {:ok, _activity, _object} ->
Logger.info("Forwarded activity to external members of the group") Logger.info("Forwarded activity to external members of the group")
:ok :ok
@ -564,6 +583,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """ @doc """
Converts PEM encoded keys to a public key representation 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 def pem_to_public_key(pem) do
[key_code] = :public_key.pem_decode(pem) [key_code] = :public_key.pem_decode(pem)
key = :public_key.pem_entry_decode(key_code) key = :public_key.pem_entry_decode(key_code)
@ -577,6 +597,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
end end
end end
@spec pem_to_public_key_pem(String.t()) :: String.t()
def pem_to_public_key_pem(pem) do def pem_to_public_key_pem(pem) do
public_key = pem_to_public_key(pem) public_key = pem_to_public_key(pem)
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key) public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)

View File

@ -3,5 +3,5 @@ defmodule Mobilizon.Federation.ActivityStream do
The ActivityStream Type The ActivityStream Type
""" """
@type t :: map() @type t :: %{String.t() => String.t() | list(String.t()) | map() | nil}
end end

View File

@ -46,7 +46,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
end end
@impl Converter @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 def as_to_model_data(%{"type" => "Note", "name" => name} = object) when is_valid_string(name) do
case extract_actors(object) do case extract_actors(object) do
%{actor_id: actor_id, creator_id: creator_id} -> %{actor_id: actor_id, creator_id: creator_id} ->
@ -57,7 +57,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
end end
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) defp extract_actors(%{"actor" => creator_url, "attributedTo" => actor_url} = _object)
when is_valid_string(creator_url) and is_valid_string(actor_url) do when is_valid_string(creator_url) and is_valid_string(actor_url) do
with {:ok, %Actor{id: creator_id, suspended: false}} <- with {:ok, %Actor{id: creator_id, suspended: false}} <-

View File

@ -45,19 +45,20 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @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 def as_to_model_data(object) do
with {:ok, %Actor{id: actor_id}, attributed_to} <- case maybe_fetch_actor_and_attributed_to_id(object) do
maybe_fetch_actor_and_attributed_to_id(object), {:ok, %Actor{id: actor_id}, attributed_to} ->
{:address, address_id} <- address_id = get_address(object["location"])
{:address, get_address(object["location"])}, tags = fetch_tags(object["tag"])
{:tags, tags} <- {:tags, fetch_tags(object["tag"])}, mentions = fetch_mentions(object["tag"])
{:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])}, visibility = get_visibility(object)
{:visibility, visibility} <- {:visibility, get_visibility(object)}, options = get_options(object)
{:options, options} <- {:options, get_options(object)}, metadata = get_metdata(object)
{:metadata, metadata} <- {:metadata, get_metdata(object)},
[description: description, picture_id: picture_id, medias: medias] <- [description: description, picture_id: picture_id, medias: medias] =
process_pictures(object, actor_id) do process_pictures(object, actor_id)
%{ %{
title: object["name"], title: object["name"],
description: description, description: description,
@ -86,9 +87,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
publish_at: object["published"], publish_at: object["published"],
language: object["inLanguage"] language: object["inLanguage"]
} }
else
{:error, _err} -> {:error, err} ->
:error {:error, err}
end end
end end

View File

@ -4,9 +4,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
""" """
alias Mobilizon.Events.EventMetadata alias Mobilizon.Events.EventMetadata
alias Mobilizon.Federation.ActivityStream
@property_value "PropertyValue" @property_value "PropertyValue"
@spec metadata_to_as(EventMetadata.t()) :: map()
def metadata_to_as(%EventMetadata{type: :boolean, value: value, key: key}) def metadata_to_as(%EventMetadata{type: :boolean, value: value, key: key})
when value in ["true", "false"] do when value in ["true", "false"] do
%{ %{
@ -47,6 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
) )
end end
@spec as_to_metadata(ActivityStream.t()) :: map()
def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value}) def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value})
when is_boolean(value) do when is_boolean(value) do
%{type: :boolean, key: key, value: to_string(value)} %{type: :boolean, key: key, value: to_string(value)}

View File

@ -66,6 +66,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
nil -> nil ->
case ActivityPub.fetch_object_from_url(todo_list_url) do case ActivityPub.fetch_object_from_url(todo_list_url) do
{:ok, _, %TodoList{}} ->
as_to_model_data(object)
{:ok, %TodoList{}} -> {:ok, %TodoList{}} ->
as_to_model_data(object) as_to_model_data(object)

View File

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

View File

@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.API.Events do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Utils}
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
@doc """ @doc """
@ -16,7 +15,7 @@ defmodule Mobilizon.GraphQL.API.Events do
@spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any @spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any
def create_event(args) do 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 # 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 end
@doc """ @doc """
@ -24,7 +23,7 @@ defmodule Mobilizon.GraphQL.API.Events do
""" """
@spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any @spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any
def update_event(args, %Event{} = event) do 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 end
@doc """ @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() @spec delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, Activity.t(), Entity.t()} | any()
def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do
ActivityPub.delete(event, actor, federate) Actions.Delete.delete(event, actor, federate)
end end
@spec prepare_args(map) :: map @spec prepare_args(map) :: map

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Activities, Actors} alias Mobilizon.{Activities, Actors}
alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Utils alias Mobilizon.Service.Activity.Utils
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
@ -12,6 +13,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
require Logger 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, %{ 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}} context: %{current_user: %User{role: role}, current_actor: %Actor{id: actor_id}}
}) do }) do

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
@doc """ @doc """
List all of an user's registered push subscriptions 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}, %{ def list_user_push_subscriptions(_parent, %{page: page, limit: limit}, %{
context: %{current_user: %User{id: user_id}} context: %{current_user: %User{id: user_id}}
}) do }) do
@ -22,6 +24,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
@doc """ @doc """
Register a push subscription 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, %{ def register_push_subscription(_parent, args, %{
context: %{current_user: %User{id: user_id}} context: %{current_user: %User{id: user_id}}
}) do }) do

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
alias Mobilizon.{Events, Posts} alias Mobilizon.{Events, Posts}
alias Mobilizon.Events.{Event, Tag} alias Mobilizon.Events.{Event, Tag}
alias Mobilizon.Posts.Post 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 def list_tags(_parent, %{page: page, limit: limit} = args, _resolution) do
filter = Map.get(args, :filter) filter = Map.get(args, :filter)
tags = Mobilizon.Events.list_tags(filter, page, limit) 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 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 def list_tags_for_event(%Event{id: id}, _args, _resolution) do
{:ok, Events.list_tags_for_event(id)} {:ok, Events.list_tags_for_event(id)}
end end
@ -33,6 +36,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
@doc """ @doc """
Retrieve the list of tags for a post 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 def list_tags_for_post(%Post{id: id}, _args, _resolution) do
{:ok, Posts.list_tags_for_post(id)} {:ok, Posts.list_tags_for_post(id)}
end end
@ -50,9 +54,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
@doc """ @doc """
Retrieve the list of related tags for a parent tag 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 def get_related_tags(%Tag{} = tag, _args, _resolution) do
with tags <- Events.list_tag_neighbors(tag) do {:ok, Events.list_tag_neighbors(tag)}
{:ok, tags}
end
end end
end end

View File

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

View File

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

View File

@ -4,10 +4,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
""" """
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Users.User alias Mobilizon.Users.{ActivitySetting, User}
require Logger 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 def user_activity_settings(_parent, _args, %{context: %{current_user: %User{} = user}}) do
{:ok, Users.activity_settings_for_user(user)} {:ok, Users.activity_settings_for_user(user)}
end end
@ -16,6 +18,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
{:error, :unauthenticated} {:error, :unauthenticated}
end 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 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)) Users.create_activity_setting(Map.put(args, :user_id, user_id))
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ defmodule Mobilizon.Actors.Actor do
require Logger require Logger
@type t :: %__MODULE__{ @type t :: %__MODULE__{
id: integer(),
url: String.t(), url: String.t(),
outbox_url: String.t(), outbox_url: String.t(),
inbox_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] @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 def delete_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do
delete_actor_options = Keyword.merge(@delete_actor_default_options, options) delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
@ -533,7 +534,7 @@ defmodule Mobilizon.Actors do
|> Repo.one() |> Repo.one()
end 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 def get_actor_by_followers_url(followers_url) do
Actor Actor
|> where([q], q.followers_url == ^followers_url) |> where([q], q.followers_url == ^followers_url)

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ defmodule Mobilizon.Discussions.Comment do
@type t :: %__MODULE__{ @type t :: %__MODULE__{
text: String.t(), text: String.t(),
url: String.t(), url: String.t(),
id: integer(),
local: boolean, local: boolean,
visibility: CommentVisibility.t(), visibility: CommentVisibility.t(),
uuid: Ecto.UUID.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 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 def build_slug([title, id], %Ecto.Changeset{valid?: true}) do
[title, ShortUUID.encode!(id)] [title, ShortUUID.encode!(id)]
|> Enum.join("-") |> Enum.join("-")
@ -31,6 +32,7 @@ defmodule Mobilizon.Discussions.Discussion do
import Mobilizon.Web.Gettext, only: [dgettext: 2] import Mobilizon.Web.Gettext, only: [dgettext: 2]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
id: String.t(),
creator: Actor.t(), creator: Actor.t(),
actor: Actor.t(), actor: Actor.t(),
title: String.t(), title: String.t(),

View File

@ -377,8 +377,8 @@ defmodule Mobilizon.Discussions do
@doc """ @doc """
Creates a discussion. Creates a discussion.
""" """
@spec create_discussion(map) :: {:ok, Comment.t()} | {:error, Changeset.t()} @spec create_discussion(map()) :: {:ok, Discussion.t()} | {:error, atom(), Changeset.t(), map()}
def create_discussion(attrs \\ %{}) do def create_discussion(attrs) do
with {:ok, %{comment: %Comment{} = _comment, discussion: %Discussion{} = discussion}} <- with {:ok, %{comment: %Comment{} = _comment, discussion: %Discussion{} = discussion}} <-
Multi.new() Multi.new()
|> Multi.insert( |> Multi.insert(
@ -412,19 +412,26 @@ defmodule Mobilizon.Discussions do
@doc """ @doc """
Create a response to a discussion 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 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}} <- with {:ok, %{comment: %Comment{} = comment, discussion: %Discussion{} = discussion}} <-
Multi.new() Multi.new()
|> Multi.insert( |> Multi.insert(
:comment, :comment,
Comment.changeset( changeset
%Comment{},
Map.merge(attrs, %{
discussion_id: discussion_id,
actor_id: Map.get(attrs, :creator_id, attrs.actor_id)
})
)
) )
|> Multi.update(:discussion, fn %{comment: %Comment{id: comment_id}} -> |> Multi.update(:discussion, fn %{comment: %Comment{id: comment_id}} ->
Discussion.changeset( Discussion.changeset(
@ -435,7 +442,7 @@ defmodule Mobilizon.Discussions do
|> Repo.transaction(), |> Repo.transaction(),
# Discussion is not updated # Discussion is not updated
%Comment{} = comment <- Repo.preload(comment, @comment_preloads) do %Comment{} = comment <- Repo.preload(comment, @comment_preloads) do
{:ok, Map.put(discussion, :last_comment, comment)} {:ok, %Discussion{discussion | last_comment: comment}}
end end
end end
@ -453,7 +460,8 @@ defmodule Mobilizon.Discussions do
@doc """ @doc """
Delete a discussion. 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 def delete_discussion(%Discussion{id: discussion_id}) do
Multi.new() Multi.new()
|> Multi.delete_all(:comments, fn _ -> |> Multi.delete_all(:comments, fn _ ->
@ -463,7 +471,7 @@ defmodule Mobilizon.Discussions do
|> Repo.transaction() |> Repo.transaction()
end 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 defp public_comments_for_actor_query(actor_id) do
Comment Comment
|> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility) |> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility)
@ -471,7 +479,7 @@ defmodule Mobilizon.Discussions do
|> preload_for_comment() |> preload_for_comment()
end 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 defp public_replies_for_thread_query(comment_id) do
Comment Comment
|> where([c], c.origin_comment_id == ^comment_id and c.visibility in ^@public_visibility) |> 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 alias Mobilizon.Web.Router.Helpers, as: Routes
@type t :: %__MODULE__{ @type t :: %__MODULE__{
id: String.t(), id: integer(),
url: String.t(), url: String.t(),
local: boolean, local: boolean,
begins_on: DateTime.t(), begins_on: DateTime.t(),
@ -47,16 +47,16 @@ defmodule Mobilizon.Events.Event do
draft: boolean, draft: boolean,
visibility: EventVisibility.t(), visibility: EventVisibility.t(),
join_options: JoinOptions.t(), join_options: JoinOptions.t(),
publish_at: DateTime.t(), publish_at: DateTime.t() | nil,
uuid: Ecto.UUID.t(), uuid: Ecto.UUID.t(),
online_address: String.t(), online_address: String.t() | nil,
phone_address: String.t(), phone_address: String.t(),
category: String.t(), category: String.t(),
options: EventOptions.t(), options: EventOptions.t(),
organizer_actor: Actor.t(), organizer_actor: Actor.t(),
attributed_to: Actor.t() | nil, attributed_to: Actor.t() | nil,
physical_address: Address.t(), physical_address: Address.t() | nil,
picture: Media.t(), picture: Media.t() | nil,
media: [Media.t()], media: [Media.t()],
tracks: [Track.t()], tracks: [Track.t()],
sessions: [Session.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 # We start by inserting the event and then insert a first participant if the event is not a draft
@spec do_create_event(map) :: @spec do_create_event(map) ::
{:ok, Event.t()} {:ok, %{insert: Event.t(), write: Participant.t() | nil}}
| {:error, Changeset.t()} | {:error, Changeset.t()}
| {:error, :update | :write, Changeset.t(), map()} | {:error, :update | :write, Changeset.t(), map()}
defp do_create_event(attrs) do defp do_create_event(attrs) do
@ -368,7 +368,7 @@ defmodule Mobilizon.Events do
Deletes an event. Deletes an event.
Raises an exception if it fails. 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) def delete_event!(%Event{} = event), do: Repo.delete!(event)
@doc """ @doc """
@ -457,6 +457,7 @@ defmodule Mobilizon.Events do
@spec list_organized_events_for_group( @spec list_organized_events_for_group(
Actor.t(), Actor.t(),
EventVisibility.t(),
DateTime.t() | nil, DateTime.t() | nil,
DateTime.t() | nil, DateTime.t() | nil,
integer | nil, integer | nil,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -107,7 +107,7 @@ defmodule Mobilizon.Users do
@doc """ @doc """
Get an user by its activation token. 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 def get_user_by_activation_token(token) do
token token
|> user_by_activation_token_query() |> user_by_activation_token_query()
@ -117,7 +117,7 @@ defmodule Mobilizon.Users do
@doc """ @doc """
Get an user by its reset password token. 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 def get_user_by_reset_password_token(token) do
token token
|> user_by_reset_password_token_query() |> user_by_reset_password_token_query()

View File

@ -40,7 +40,7 @@ defmodule Mobilizon.Service.Activity.Member do
Actors.get_member(member_id) Actors.get_member(member_id)
end 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 defp get_author(%Member{actor_id: actor_id}, options) do
moderator = Keyword.get(options, :moderator) moderator = Keyword.get(options, :moderator)

View File

@ -3,10 +3,12 @@ defmodule Mobilizon.Service.ErrorPage do
Render an error page Render an error page
""" """
@spec init :: :ok | {:error, File.posix()}
def init do def init do
render_error_page() render_error_page()
end end
@spec render_error_page :: :ok | {:error, File.posix()}
defp render_error_page do defp render_error_page do
content = content =
Phoenix.View.render_to_string(Mobilizon.Web.ErrorView, "500.html", conn: %Plug.Conn{}) 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
end end
@spec hashtag_handler(String.t(), String.t(), any(), map()) :: {String.t(), map()}
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag) tag = String.downcase(tag)
url = "#{Endpoint.url()}/tag/#{tag}" url = "#{Endpoint.url()}/tag/#{tag}"
@ -100,6 +101,7 @@ defmodule Mobilizon.Service.Formatter do
@doc """ @doc """
Escapes a special characters in mention names. Escapes a special characters in mention names.
""" """
@spec mentions_escape(String.t(), Keyword.t()) :: String.t()
def mentions_escape(text, options \\ []) do def mentions_escape(text, options \\ []) do
options = options =
Keyword.merge(options, Keyword.merge(options,
@ -111,6 +113,11 @@ defmodule Mobilizon.Service.Formatter do
Linkify.link(text, options) Linkify.link(text, options)
end 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 def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags} {html_escape(text, type), mentions, hashtags}
end end
@ -131,6 +138,7 @@ defmodule Mobilizon.Service.Formatter do
|> Enum.join("") |> Enum.join("")
end end
@spec truncate(String.t(), non_neg_integer(), String.t()) :: String.t()
def truncate(text, max_length \\ 200, omission \\ "...") do def truncate(text, max_length \\ 200, omission \\ "...") do
# Remove trailing whitespace # Remove trailing whitespace
text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}") text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
@ -143,6 +151,7 @@ defmodule Mobilizon.Service.Formatter do
end end
end end
@spec linkify_opts :: Keyword.t()
defp linkify_opts do defp linkify_opts do
Mobilizon.Config.get(__MODULE__) ++ Mobilizon.Config.get(__MODULE__) ++
[ [
@ -186,5 +195,6 @@ defmodule Mobilizon.Service.Formatter do
|> (&" #{&1}").() |> (&" #{&1}").()
end end
@spec tag_text_strip(String.t()) :: String.t()
defp tag_text_strip(tag), do: tag |> String.trim("#") |> String.downcase() defp tag_text_strip(tag), do: tag |> String.trim("#") |> String.downcase()
end end

View File

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

View File

@ -19,7 +19,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
require Logger 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 def trigger_notifications_for_participant(%Participant{} = participant) do
before_event_notification(participant) before_event_notification(participant)
on_day_notification(participant) on_day_notification(participant)
@ -27,6 +27,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
{:ok, nil} {:ok, nil}
end end
@spec before_event_notification(Participant.t()) :: {:ok, nil}
def before_event_notification(%Participant{ def before_event_notification(%Participant{
id: participant_id, id: participant_id,
event: %Event{begins_on: begins_on}, event: %Event{begins_on: begins_on},
@ -46,6 +47,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
def before_event_notification(_), do: {:ok, nil} 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{ def on_day_notification(%Participant{
event: %Event{begins_on: begins_on}, event: %Event{begins_on: begins_on},
actor: %Actor{user_id: user_id} actor: %Actor{user_id: user_id}
@ -90,6 +92,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
def on_day_notification(_), do: {:ok, nil} def on_day_notification(_), do: {:ok, nil}
@spec weekly_notification(Participant.t()) :: {:ok, Oban.Job.t() | nil | String.t()}
def weekly_notification(%Participant{ def weekly_notification(%Participant{
event: %Event{begins_on: begins_on}, event: %Event{begins_on: begins_on},
actor: %Actor{user_id: user_id} actor: %Actor{user_id: user_id}
@ -144,6 +147,7 @@ defmodule Mobilizon.Service.Notifications.Scheduler do
def weekly_notification(_), do: {:ok, nil} 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(event, options \\ [])
def pending_participation_notification( 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()} @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 def notify(%User{} = user, %Activity{} = activity, opts \\ []) do
Enum.each(providers(opts), & &1.send(user, activity, opts)) Enum.each(providers(opts), & &1.send(user, activity, opts))
end end

View File

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

View File

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

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