Various typespec and compilation improvements

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2021-09-10 11:27:59 +02:00
parent 029a4ea194
commit de047c8939
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
125 changed files with 790 additions and 357 deletions

View file

@ -41,7 +41,7 @@ defmodule Mobilizon.Federation.ActivityPub do
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.Types.{Managable, Ownable} 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.Federation.HTTPSignatures.Signature
@ -56,11 +56,9 @@ defmodule Mobilizon.Federation.ActivityPub do
@public_ap_adress "https://www.w3.org/ns/activitystreams#Public" @public_ap_adress "https://www.w3.org/ns/activitystreams#Public"
@doc """ # Wraps an object into an activity
Wraps an object into an activity
"""
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()} @spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
def create_activity(map, local \\ true) when is_map(map) do defp create_activity(map, local) when is_map(map) do
with map <- lazy_put_activity_defaults(map) do with map <- lazy_put_activity_defaults(map) do
{:ok, {:ok,
%Activity{ %Activity{
@ -168,7 +166,7 @@ defmodule Mobilizon.Federation.ActivityPub do
* Federates (asynchronously) the activity * Federates (asynchronously) the activity
* Returns the activity * Returns the activity
""" """
@spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any() @spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), Entity.entities()} | any()
def create(type, args, local \\ false, additional \\ %{}) do def create(type, args, local \\ false, additional \\ %{}) do
Logger.debug("creating an activity") Logger.debug("creating an activity")
Logger.debug(inspect(args)) Logger.debug(inspect(args))
@ -206,7 +204,8 @@ defmodule Mobilizon.Federation.ActivityPub do
* Federates (asynchronously) the activity * Federates (asynchronously) the activity
* Returns the activity * Returns the activity
""" """
@spec update(struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any() @spec update(Entity.entities(), map(), boolean, map()) ::
{:ok, Activity.t(), Entity.entities()} | any()
def update(old_entity, args, local \\ false, additional \\ %{}) do def update(old_entity, args, local \\ false, additional \\ %{}) do
Logger.debug("updating an activity") Logger.debug("updating an activity")
Logger.debug(inspect(args)) Logger.debug(inspect(args))
@ -224,6 +223,12 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
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 def accept(type, entity, local \\ true, additional \\ %{}) do
Logger.debug("We're accepting something") Logger.debug("We're accepting something")
@ -246,6 +251,8 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec reject(acceptable_types, acceptable_entities, boolean, map) ::
{:ok, ActivityStream.t(), acceptable_entities}
def reject(type, entity, local \\ true, additional \\ %{}) do def reject(type, entity, local \\ true, additional \\ %{}) do
{:ok, entity, update_data} = {:ok, entity, update_data} =
case type do case type do
@ -266,6 +273,8 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) ::
{:ok, Activity.t(), ActivityStream.t()}
def announce( def announce(
%Actor{} = actor, %Actor{} = actor,
object, object,
@ -286,6 +295,8 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec unannounce(Actor.t(), ActivityStream.t(), String.t() | nil, String.t() | nil, boolean) ::
{:ok, Activity.t(), ActivityStream.t()}
def unannounce( def unannounce(
%Actor{} = actor, %Actor{} = actor,
object, object,
@ -306,6 +317,8 @@ defmodule Mobilizon.Federation.ActivityPub do
@doc """ @doc """
Make an actor follow another Make an actor follow another
""" """
@spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) ::
{:ok, Activity.t(), Follower.t()} | {:error, String.t()}
def follow( def follow(
%Actor{} = follower, %Actor{} = follower,
%Actor{} = followed, %Actor{} = followed,
@ -336,7 +349,8 @@ defmodule Mobilizon.Federation.ActivityPub do
@doc """ @doc """
Make an actor unfollow another Make an actor unfollow another
""" """
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any() @spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Follower.t()}
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower), with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower),
# We recreate the follow activity # We recreate the follow activity
@ -357,6 +371,7 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec delete(Entity.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Entity.t()}
def delete(object, actor, local \\ true, additional \\ %{}) do def delete(object, actor, local \\ true, additional \\ %{}) do
with {:ok, activity_data, actor, object} <- with {:ok, activity_data, actor, object} <-
Managable.delete(object, actor, local, additional), Managable.delete(object, actor, local, additional),
@ -369,6 +384,9 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec join(Event.t(), Actor.t(), boolean, map) ::
{:ok, Activity.t(), Participant.t()} | {:maximum_attendee_capacity, any}
@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(entity_to_join, actor_joining, local \\ true, additional \\ %{})
def join(%Event{} = event, %Actor{} = actor, local, additional) do def join(%Event{} = event, %Actor{} = actor, local, additional) do
@ -397,6 +415,8 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec leave(Event.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Participant.t()}
@spec leave(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
def leave(object, actor, local \\ true, additional \\ %{}) def leave(object, actor, local \\ true, additional \\ %{})
@doc """ @doc """
@ -462,6 +482,7 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
def remove( def remove(
%Member{} = member, %Member{} = member,
%Actor{type: :Group, url: group_url, members_url: group_members_url}, %Actor{type: :Group, url: group_url, members_url: group_members_url},
@ -502,7 +523,7 @@ defmodule Mobilizon.Federation.ActivityPub do
) do ) do
Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}") 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)}, with {:is_able_to_invite, true} <- {:is_able_to_invite, is_able_to_invite?(actor, group)},
{:ok, %Member{url: member_url} = member} <- {:ok, %Member{url: member_url} = member} <-
Actors.create_member(%{ Actors.create_member(%{
parent_id: group_id, parent_id: group_id,
@ -538,7 +559,8 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
defp is_able_to_invite(%Actor{domain: actor_domain, id: actor_id}, %Actor{ @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, domain: group_domain,
id: group_id id: group_id
}) do }) do
@ -547,12 +569,17 @@ defmodule Mobilizon.Federation.ActivityPub do
true true
else else
# If local group, we'll send the invite # If local group, we'll send the invite
with {:ok, %Member{} = admin_member} <- Actors.get_member(actor_id, group_id) do case Actors.get_member(actor_id, group_id) do
{:ok, %Member{} = admin_member} ->
Member.is_administrator(admin_member) Member.is_administrator(admin_member)
_ ->
false
end end
end 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 def move(type, old_entity, args, local \\ false, additional \\ %{}) do
Logger.debug("We're moving something") Logger.debug("We're moving something")
Logger.debug(inspect(args)) Logger.debug(inspect(args))
@ -572,6 +599,7 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec flag(map, boolean, map) :: {:ok, Activity.t(), Report.t()}
def flag(args, local \\ false, additional \\ %{}) do def flag(args, local \\ false, additional \\ %{}) do
with {report, report_as_data} <- Types.Reports.flag(args, local, additional), with {report, report_as_data} <- Types.Reports.flag(args, local, additional),
{:ok, activity} <- create_activity(report_as_data, local), {:ok, activity} <- create_activity(report_as_data, local),
@ -615,6 +643,7 @@ defmodule Mobilizon.Federation.ActivityPub do
end) end)
end end
@spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())}
defp convert_followers_in_recipients(recipients) do defp convert_followers_in_recipients(recipients) do
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc -> Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
case Actors.get_actor_by_followers_url(recipient) do case Actors.get_actor_by_followers_url(recipient) do
@ -678,6 +707,8 @@ defmodule Mobilizon.Federation.ActivityPub do
@doc """ @doc """
Publish an activity to a specific inbox 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 def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}") Logger.info("Federating #{id} to #{inbox}")
%URI{host: host, path: path} = URI.parse(inbox) %URI{host: host, path: path} = URI.parse(inbox)
@ -711,7 +742,7 @@ defmodule Mobilizon.Federation.ActivityPub do
@doc """ @doc """
Return all public activities (events & comments) for an actor Return all public activities (events & comments) for an actor
""" """
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map() @spec fetch_public_activities_for_actor(Actor.t(), pos_integer(), pos_integer()) :: map()
def fetch_public_activities_for_actor(%Actor{id: actor_id} = actor, page \\ 1, limit \\ 10) do def fetch_public_activities_for_actor(%Actor{id: actor_id} = actor, page \\ 1, limit \\ 10) do
%Actor{id: relay_actor_id} = Relay.get_actor() %Actor{id: relay_actor_id} = Relay.get_actor()
@ -769,6 +800,8 @@ defmodule Mobilizon.Federation.ActivityPub do
defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url) defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url)
defp check_for_tombstones(_), do: nil defp check_for_tombstones(_), do: nil
@typep accept_follow_entities :: Follower.t()
@spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any @spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any
defp accept_follow(%Follower{} = follower, additional) do defp accept_follow(%Follower{} = follower, additional) do
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}), with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}),
@ -792,7 +825,10 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec accept_join(Participant.t(), map) :: {:ok, Participant.t(), Activity.t()} | any @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 defp accept_join(%Participant{} = participant, additional) do
with {:ok, %Participant{} = participant} <- with {:ok, %Participant{} = participant} <-
Events.update_participant(participant, %{role: :participant}), Events.update_participant(participant, %{role: :participant}),
@ -820,7 +856,6 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec accept_join(Member.t(), map) :: {:ok, Member.t(), Activity.t()} | any
defp accept_join(%Member{} = member, additional) do defp accept_join(%Member{} = member, additional) do
with {:ok, %Member{} = member} <- with {:ok, %Member{} = member} <-
Actors.update_member(member, %{role: :member}), Actors.update_member(member, %{role: :member}),
@ -854,6 +889,8 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@typep accept_invite_entities :: Member.t()
@spec accept_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any @spec accept_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
defp accept_invite( defp accept_invite(
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member, %Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
@ -881,6 +918,7 @@ defmodule Mobilizon.Federation.ActivityPub do
end end
end end
@spec maybe_refresh_group(Member.t()) :: :ok | nil
defp maybe_refresh_group(%Member{ defp maybe_refresh_group(%Member{
parent: %Actor{domain: parent_domain, url: parent_url}, parent: %Actor{domain: parent_domain, url: parent_url},
actor: %Actor{} = actor actor: %Actor{} = actor

View file

@ -108,7 +108,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
{:ok, url} when is_binary(url) -> {:ok, url} when is_binary(url) ->
make_actor_from_url(url, preload) make_actor_from_url(url, preload)
_e -> {:error, _e} ->
{:error, "No ActivityPub URL found in WebFinger"} {:error, "No ActivityPub URL found in WebFinger"}
end end
end end

View file

@ -99,6 +99,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
} }
end end
@spec get_to_and_cc(Actor.t(), list(), :direct | :private | :public | :unlisted | {:list, any}) ::
{list(), list()}
@doc """ @doc """
Determines the full audience based on mentions for an audience Determines the full audience based on mentions for an audience
@ -118,7 +120,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
* `to` : the mentioned actors and the eventual actor we're replying to * `to` : the mentioned actors and the eventual actor we're replying to
* `cc` : none * `cc` : none
""" """
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :public) do def get_to_and_cc(%Actor{} = actor, mentions, :public) do
to = [@ap_public | mentions] to = [@ap_public | mentions]
cc = [actor.followers_url] cc = [actor.followers_url]
@ -128,7 +129,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{to, cc} {to, cc}
end end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do
to = [actor.followers_url | mentions] to = [actor.followers_url | mentions]
cc = [@ap_public] cc = [@ap_public]
@ -138,7 +138,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{to, cc} {to, cc}
end end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :private) do def get_to_and_cc(%Actor{} = actor, mentions, :private) do
{to, cc} = get_to_and_cc(actor, mentions, :direct) {to, cc} = get_to_and_cc(actor, mentions, :direct)
@ -147,7 +146,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{to, cc} {to, cc}
end end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(_actor, mentions, :direct) do def get_to_and_cc(_actor, mentions, :direct) do
{mentions, []} {mentions, []}
end end

View file

@ -13,6 +13,8 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
@member_roles [:member, :moderator, :administrator] @member_roles [:member, :moderator, :administrator]
@type object :: %{id: String.t(), url: String.t()}
@doc """ @doc """
Check that actor can access the object Check that actor can access the object
""" """
@ -66,8 +68,8 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
@spec can_manage_group_object?( @spec can_manage_group_object?(
existing_object_permissions(), existing_object_permissions(),
Actor.t(), %Actor{url: String.t()},
any() object()
) :: boolean() ) :: boolean()
defp can_manage_group_object?(permission, %Actor{url: actor_url} = actor, object) do defp can_manage_group_object?(permission, %Actor{url: actor_url} = actor, object) do
if Ownable.group_actor(object) != nil do if Ownable.group_actor(object) != nil do
@ -94,7 +96,7 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
end end
end end
@spec activity_actor_is_group_member?(Actor.t(), Entity.t(), atom()) :: boolean() @spec activity_actor_is_group_member?(Actor.t(), object(), atom()) :: boolean()
defp activity_actor_is_group_member?( defp activity_actor_is_group_member?(
%Actor{id: actor_id, url: actor_url}, %Actor{id: actor_id, url: actor_url},
object, object,

View file

@ -951,7 +951,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
defp do_handle_incoming_reject_invite(invite_object, %Actor{} = actor_rejecting) do defp do_handle_incoming_reject_invite(invite_object, %Actor{} = actor_rejecting) do
with {:invite, {:ok, %Member{role: :invited, actor_id: actor_id} = member}} <- with {:invite, {:ok, %Member{role: :invited, actor_id: actor_id} = member}} <-
{: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 ActivityPub.reject(:invite, member, false) do
{:ok, activity, member} {:ok, activity, member}

View file

@ -1,10 +1,11 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Actors do defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{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.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
alias Mobilizon.Service.Activity.Group, as: GroupActivity alias Mobilizon.Service.Activity.Group, as: GroupActivity
@ -17,7 +18,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@behaviour Entity @behaviour Entity
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, map()} @spec create(map(), map()) :: {:ok, Actor.t(), ActivityStream.t()}
def create(args, additional) do def create(args, additional) do
with args <- prepare_args_for_actor(args), with args <- prepare_args_for_actor(args),
{:ok, %Actor{} = actor} <- Actors.create_actor(args), {:ok, %Actor{} = actor} <- Actors.create_actor(args),
@ -35,7 +36,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
end end
@impl Entity @impl Entity
@spec update(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any @spec update(Actor.t(), map, map) :: {:ok, Actor.t(), ActivityStream.t()}
def update(%Actor{} = old_actor, args, additional) do def update(%Actor{} = old_actor, args, additional) do
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args), with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
{:ok, _} <- {:ok, _} <-
@ -57,6 +58,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@public_ap "https://www.w3.org/ns/activitystreams#Public" @public_ap "https://www.w3.org/ns/activitystreams#Public"
@impl Entity @impl Entity
@spec delete(Actor.t(), Actor.t(), boolean, map) ::
{:ok, ActivityStream.t(), Actor.t(), Actor.t()}
def delete( def delete(
%Actor{ %Actor{
followers_url: followers_url, followers_url: followers_url,
@ -100,10 +103,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
end end
end end
@spec actor(Actor.t()) :: Actor.t() | nil
def actor(%Actor{} = actor), do: actor def actor(%Actor{} = actor), do: actor
@spec actor(Actor.t()) :: Actor.t() | nil
def group_actor(%Actor{} = actor), do: actor def group_actor(%Actor{} = actor), do: actor
@spec permissions(Actor.t()) :: Permission.t()
def permissions(%Actor{} = _group) do def permissions(%Actor{} = _group) do
%Permission{ %Permission{
access: :member, access: :member,
@ -113,7 +119,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
} }
end end
@spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, map(), Member.t()} @spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, ActivityStreams.t(), Member.t()}
def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do
with role <- with role <-
additional additional
@ -153,6 +159,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
end end
end end
@spec follow(Actor.t(), Actor.t(), boolean, map) ::
{:accept, any}
| {:ok, ActivityStreams.t(), Follower.t()}
| {:error, :no_person, String.t()}
def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional) def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional)
when type != :Person do when type != :Person do
with {:ok, %Follower{} = follower} <- with {:ok, %Follower{} = follower} <-
@ -165,6 +175,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
def follow(_, _, _, _), do: {:error, :no_person, "Only group and instances can be followed"} def follow(_, _, _, _), do: {:error, :no_person, "Only group and instances can be followed"}
@spec prepare_args_for_actor(map) :: map
defp prepare_args_for_actor(args) do defp prepare_args_for_actor(args) do
args args
|> maybe_sanitize_username() |> maybe_sanitize_username()
@ -191,8 +202,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
defp maybe_sanitize_summary(args), do: args defp maybe_sanitize_summary(args), do: args
# 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_member(Actor.t(), Actor.t(), map(), Member.t(), atom()) :: @spec approve_if_default_role_is_member(
{:ok, map(), Member.t()} Actor.t(),
Actor.t(),
ActivityStreams.t(),
Member.t(),
MemberRole.t()
) ::
{:ok, ActivityStreams.t(), Member.t()}
defp approve_if_default_role_is_member( defp approve_if_default_role_is_member(
%Actor{type: :Group} = group, %Actor{type: :Group} = group,
%Actor{} = actor, %Actor{} = actor,
@ -202,7 +219,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
) do ) do
if is_nil(group.domain) && !is_nil(actor.domain) do if is_nil(group.domain) && !is_nil(actor.domain) do
cond do cond 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( ActivityPub.accept(
@ -212,7 +229,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
%{"actor" => group.url} %{"actor" => group.url}
)} )}
Mobilizon.Actors.get_default_member_role(group) === :not_approved && Mobilizon.Actors.get_default_member_role(group) == :not_approved &&
role == :not_approved -> role == :not_approved ->
Scheduler.pending_membership_notification(group) Scheduler.pending_membership_notification(group)
{:ok, activity_data, member} {:ok, activity_data, member}
@ -225,6 +242,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
end end
end end
@spec approve_if_manually_approves_followers(Follower.t(), ActivityStreams.t()) ::
{:accept, any} | {:ok, ActivityStreams.t(), Follower.t()}
defp approve_if_manually_approves_followers( defp approve_if_manually_approves_followers(
%Follower{} = follower, %Follower{} = follower,
follow_as_data follow_as_data

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
alias Mobilizon.Events.{Event, EventOptions} alias Mobilizon.Events.{Event, EventOptions}
alias Mobilizon.Federation.ActivityPub.{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
@ -20,7 +21,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
@behaviour Entity @behaviour Entity
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, map()} @spec create(map(), map()) :: {:ok, Comment.t(), ActivityStream.t()}
def create(args, additional) do def create(args, additional) do
with args <- prepare_args_for_comment(args), with args <- prepare_args_for_comment(args),
:ok <- make_sure_event_allows_commenting(args), :ok <- make_sure_event_allows_commenting(args),
@ -41,7 +42,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
end end
@impl Entity @impl Entity
@spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any() @spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), ActivityStream.t()}
def update(%Comment{} = old_comment, args, additional) do def update(%Comment{} = old_comment, args, additional) do
with args <- prepare_args_for_comment_update(args), with args <- prepare_args_for_comment_update(args),
{:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args), {:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args),
@ -60,7 +61,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
end end
@impl Entity @impl Entity
@spec delete(Comment.t(), Actor.t(), boolean, map()) :: {:ok, Comment.t()} @spec delete(Comment.t(), Actor.t(), boolean, map()) ::
{: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,
@ -91,6 +93,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
end end
end end
@spec actor(Comment.t()) :: Actor.t() | nil
def actor(%Comment{actor: %Actor{} = actor}), do: actor def actor(%Comment{actor: %Actor{} = actor}), do: actor
def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id), def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id),
@ -98,6 +101,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
def actor(_), do: nil def actor(_), do: nil
@spec group_actor(Comment.t()) :: Actor.t() | nil
def group_actor(%Comment{attributed_to: %Actor{} = group}), do: group def group_actor(%Comment{attributed_to: %Actor{} = group}), do: group
def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
@ -105,6 +109,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
def group_actor(_), do: nil def group_actor(_), do: nil
@spec permissions(Comment.t()) :: Permission.t()
def permissions(%Comment{}), def permissions(%Comment{}),
do: %Permission{ do: %Permission{
access: :member, access: :member,
@ -114,6 +119,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
} }
# Prepare and sanitize arguments for comments # Prepare and sanitize arguments for comments
@spec prepare_args_for_comment(map) :: map
defp prepare_args_for_comment(args) do defp prepare_args_for_comment(args) do
with in_reply_to_comment <- with in_reply_to_comment <-
args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(), args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(),
@ -150,6 +156,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
end end
end end
@spec prepare_args_for_comment_update(map) :: map
defp prepare_args_for_comment_update(args) do defp prepare_args_for_comment_update(args) do
with {text, mentions, tags} <- with {text, mentions, tags} <-
APIUtils.make_content_html( APIUtils.make_content_html(
@ -174,6 +181,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
defp handle_event_for_comment(nil), do: nil defp handle_event_for_comment(nil), do: nil
@spec maybe_publish_graphql_subscription(String.t() | integer() | nil) :: :ok
defp maybe_publish_graphql_subscription(nil), do: :ok defp maybe_publish_graphql_subscription(nil), do: :ok
defp maybe_publish_graphql_subscription(discussion_id) do defp maybe_publish_graphql_subscription(discussion_id) do
@ -186,6 +194,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
end end
end end
@spec make_sure_event_allows_commenting(%{actor_id: String.t() | integer, event: Event.t()}) ::
:ok | {:error, :event_comments_are_closed}
defp make_sure_event_allows_commenting(%{ defp make_sure_event_allows_commenting(%{
actor_id: actor_id, actor_id: actor_id,
event: %Event{ event: %Event{

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub.{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.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
alias Mobilizon.Service.Activity.Discussion, as: DiscussionActivity alias Mobilizon.Service.Activity.Discussion, as: DiscussionActivity
@ -16,7 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
@behaviour Entity @behaviour Entity
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, map()} @spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.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), with args <- prepare_args(args),
%Discussion{} = discussion <- Discussions.get_discussion(discussion_id), %Discussion{} = discussion <- Discussions.get_discussion(discussion_id),
@ -39,7 +40,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
end end
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, map()} @spec create(map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()}
def create(args, additional) do def create(args, additional) do
with args <- prepare_args(args), with args <- prepare_args(args),
{:ok, %Discussion{} = discussion} <- {:ok, %Discussion{} = discussion} <-
@ -56,7 +57,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
end end
@impl Entity @impl Entity
@spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), Activity.t()} | any() @spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), ActivityStream.t()}
def update(%Discussion{} = old_discussion, args, additional) do def update(%Discussion{} = old_discussion, args, additional) do
with {:ok, %Discussion{} = new_discussion} <- with {:ok, %Discussion{} = new_discussion} <-
Discussions.update_discussion(old_discussion, args), Discussions.update_discussion(old_discussion, args),
@ -80,7 +81,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
end end
@impl Entity @impl Entity
@spec delete(Discussion.t(), Actor.t(), boolean, map()) :: {:ok, Discussion.t()} @spec delete(Discussion.t(), Actor.t(), boolean, map()) ::
{: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,
@ -106,10 +108,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
end end
end end
@spec actor(Discussion.t()) :: Actor.t() | nil
def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id) def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id)
@spec group_actor(Discussion.t()) :: Actor.t() | nil
def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id) def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
@spec permissions(Discussion.t()) :: Permission.t()
def permissions(%Discussion{}) do def permissions(%Discussion{}) do
%Permission{access: :member, create: :member, update: :moderator, delete: :moderator} %Permission{access: :member, create: :member, update: :moderator, delete: :moderator}
end end
@ -123,6 +128,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
:ok :ok
end end
@spec prepare_args(map) :: map
defp prepare_args(args) do defp prepare_args(args) do
{text, _mentions, _tags} = {text, _mentions, _tags} =
APIUtils.make_content_html( APIUtils.make_content_html(

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 alias Mobilizon.Events.{Event, Participant}
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,7 +28,19 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
@moduledoc """ @moduledoc """
ActivityPub entity behaviour ActivityPub entity behaviour
""" """
@type t :: %{id: String.t()} @type t :: %{id: String.t(), url: String.t()}
@type entities ::
Actor.t()
| Member.t()
| Event.t()
| Participant.t()
| Comment.t()
| Discussion.t()
| Post.t()
| Resource.t()
| Todo.t()
| TodoList.t()
@callback create(data :: any(), additionnal :: map()) :: @callback create(data :: any(), additionnal :: map()) ::
{:ok, t(), ActivityStream.t()} {:ok, t(), ActivityStream.t()}

View file

@ -3,8 +3,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events, as: EventsManager alias Mobilizon.Events, as: EventsManager
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant, ParticipantRole}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.{ActivityPub, ActivityStream}
alias Mobilizon.Federation.ActivityPub.{Audience, Permission} alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
@ -22,7 +22,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
@behaviour Entity @behaviour Entity
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, map()} @spec create(map(), map()) :: {:ok, Event.t(), ActivityStream.t()}
def create(args, additional) do def create(args, additional) do
with args <- prepare_args_for_event(args), with args <- prepare_args_for_event(args),
{:ok, %Event{} = event} <- EventsManager.create_event(args), {:ok, %Event{} = event} <- EventsManager.create_event(args),
@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
end end
@impl Entity @impl Entity
@spec update(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any() @spec update(Event.t(), map(), map()) :: {:ok, Event.t(), ActivityStream.t()}
def update(%Event{} = old_event, args, additional) do def update(%Event{} = old_event, args, additional) do
with args <- prepare_args_for_event(args), with args <- prepare_args_for_event(args),
{:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args), {:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args),
@ -59,7 +59,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
end end
@impl Entity @impl Entity
@spec delete(Event.t(), Actor.t(), boolean, map()) :: {:ok, Event.t()} @spec delete(Event.t(), Actor.t(), boolean, map()) ::
{:ok, ActivityStream.t(), Actor.t(), Event.t()}
def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do
activity_data = %{ activity_data = %{
"type" => "Delete", "type" => "Delete",
@ -82,6 +83,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
end end
end end
@spec actor(Event.t()) :: Actor.t() | nil
def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor
def actor(%Event{organizer_actor_id: organizer_actor_id}), def actor(%Event{organizer_actor_id: organizer_actor_id}),
@ -89,6 +91,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
def actor(_), do: nil def actor(_), do: nil
@spec group_actor(Event.t()) :: Actor.t() | nil
def group_actor(%Event{attributed_to: %Actor{} = group}), do: group def group_actor(%Event{attributed_to: %Actor{} = group}), do: group
def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
@ -96,6 +99,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
def group_actor(_), do: nil def group_actor(_), do: nil
@spec permissions(Event.t()) :: Permission.t()
def permissions(%Event{draft: draft, attributed_to_id: _attributed_to_id}) do def permissions(%Event{draft: draft, attributed_to_id: _attributed_to_id}) do
%Permission{ %Permission{
access: if(draft, do: nil, else: :member), access: if(draft, do: nil, else: :member),
@ -105,9 +109,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
} }
end end
@spec join(Event.t(), Actor.t(), boolean, map) ::
{:ok, ActivityStreams.t(), Participant.t()}
| {:error, :maximum_attendee_capacity_reached}
def join(%Event{} = event, %Actor{} = actor, _local, additional) do def join(%Event{} = event, %Actor{} = actor, _local, additional) do
with {:maximum_attendee_capacity, true} <- with {:maximum_attendee_capacity, true} <-
{:maximum_attendee_capacity, check_attendee_capacity(event)}, {:maximum_attendee_capacity, check_attendee_capacity?(event)},
role <- role <-
additional additional
|> Map.get(:metadata, %{}) |> Map.get(:metadata, %{})
@ -133,12 +140,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
role role
) )
else else
{:maximum_attendee_capacity, err} -> {:maximum_attendee_capacity, false} ->
{:maximum_attendee_capacity, err} {:error, :maximum_attendee_capacity_reached}
end end
end end
defp check_attendee_capacity(%Event{options: options} = event) do @spec check_attendee_capacity?(Event.t()) :: boolean
defp check_attendee_capacity?(%Event{options: options} = event) do
with maximum_attendee_capacity <- with maximum_attendee_capacity <-
Map.get(options, :maximum_attendee_capacity) || 0 do Map.get(options, :maximum_attendee_capacity) || 0 do
maximum_attendee_capacity == 0 || maximum_attendee_capacity == 0 ||
@ -147,6 +155,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
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(
Event.t(),
ActivityStreams.t(),
Participant.t(),
ParticipantRole.t()
) :: {:ok, ActivityStreams.t(), Participant.t()}
defp approve_if_default_role_is_participant(event, activity_data, participant, role) do defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
case event do case event do
%Event{attributed_to: %Actor{id: group_id, url: group_url}} -> %Event{attributed_to: %Actor{id: group_id, url: group_url}} ->
@ -171,9 +185,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
end end
end end
@spec do_approve(Event.t(), ActivityStreams.t(), Particpant.t(), ParticipantRole.t(), map()) ::
{:accept, any} | {:ok, ActivityStreams.t(), Participant.t()}
defp do_approve(event, activity_data, participant, role, additionnal) do defp do_approve(event, activity_data, participant, role, additionnal) do
cond do cond 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( ActivityPub.accept(
@ -183,7 +199,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
additionnal additionnal
)} )}
Mobilizon.Events.get_default_participant_role(event) === :not_approved && Mobilizon.Events.get_default_participant_role(event) == :not_approved &&
role == :not_approved -> role == :not_approved ->
Scheduler.pending_participation_notification(event) Scheduler.pending_participation_notification(event)
{:ok, activity_data, participant} {:ok, activity_data, participant}
@ -194,6 +210,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
end end
# Prepare and sanitize arguments for events # Prepare and sanitize arguments for events
@spec prepare_args_for_event(map) :: map
defp prepare_args_for_event(args) do defp prepare_args_for_event(args) do
# If title is not set: we are not updating it # If title is not set: we are not updating it
args = args =

View file

@ -1,14 +1,15 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Members do defmodule Mobilizon.Federation.ActivityPub.Types.Members do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member, MemberRole}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.{ActivityPub, 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
require Logger require Logger
import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2] import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2]
@spec update(Member.t(), map, map) :: {:ok, Member.t(), ActivityStream.t()}
def update( def update(
%Member{ %Member{
parent: %Actor{id: group_id} = group, parent: %Actor{id: group_id} = group,
@ -24,7 +25,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
when moderator_role in [:moderator, :administrator, :creator] <- when moderator_role in [:moderator, :administrator, :creator] <-
{:has_rights_to_update_role, Actors.get_member(moderator_id, group_id)}, {:has_rights_to_update_role, Actors.get_member(moderator_id, group_id)},
{:is_only_admin, false} <- {:is_only_admin, false} <-
{:is_only_admin, check_admins_left(member_id, group_id, current_role, updated_role)}, {:is_only_admin, check_admins_left?(member_id, group_id, current_role, updated_role)},
{:ok, %Member{} = member} <- {:ok, %Member{} = member} <-
Actors.update_member(old_member, args), Actors.update_member(old_member, args),
{:ok, _} <- {:ok, _} <-
@ -56,6 +57,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
end end
# Used only when a group is suspended # Used only when a group is suspended
@spec delete(Member.t(), Actor.t(), boolean(), map()) :: {:ok, Activity.t(), Member.t()}
def delete( def delete(
%Member{parent: %Actor{} = group, actor: %Actor{} = actor} = _member, %Member{parent: %Actor{} = group, actor: %Actor{} = actor} = _member,
%Actor{}, %Actor{},
@ -66,13 +68,21 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
ActivityPub.leave(group, actor, local, %{force_member_removal: true}) ActivityPub.leave(group, actor, local, %{force_member_removal: true})
end end
@spec actor(Member.t()) :: Actor.t() | nil
def actor(%Member{actor_id: actor_id}), def actor(%Member{actor_id: actor_id}),
do: Actors.get_actor(actor_id) do: Actors.get_actor(actor_id)
@spec group_actor(Member.t()) :: Actor.t() | nil
def group_actor(%Member{parent_id: parent_id}), def group_actor(%Member{parent_id: parent_id}),
do: Actors.get_actor(parent_id) do: Actors.get_actor(parent_id)
defp check_admins_left(member_id, group_id, current_role, updated_role) do @spec check_admins_left?(
String.t() | integer,
String.t() | integer,
MemberRole.t(),
MemberRole.t()
) :: boolean
defp check_admins_left?(member_id, group_id, current_role, updated_role) do
Actors.is_only_administrator?(member_id, group_id) && current_role == :administrator && Actors.is_only_administrator?(member_id, group_id) && current_role == :administrator &&
updated_role != :administrator updated_role != :administrator
end end

View file

@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.{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.Posts.Post alias Mobilizon.Posts.Post
@ -17,6 +18,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
@public_ap "https://www.w3.org/ns/activitystreams#Public" @public_ap "https://www.w3.org/ns/activitystreams#Public"
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, Post.t(), ActivityStream.t()}
def create(args, additional) do def create(args, additional) do
with args <- prepare_args(args), with args <- prepare_args(args),
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
@ -37,6 +39,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
end end
@impl Entity @impl Entity
@spec update(Post.t(), map(), map()) :: {:ok, Post.t(), ActivityStream.t()}
def update(%Post{} = post, args, additional) do def update(%Post{} = post, args, additional) do
with args <- prepare_args(args), with args <- prepare_args(args),
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
@ -60,6 +63,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
end end
@impl Entity @impl Entity
@spec delete(Post.t(), Actor.t(), boolean, map) ::
{:ok, ActivityStream.t(), Actor.t(), Post.t()}
def delete( def delete(
%Post{ %Post{
url: url, url: url,
@ -86,12 +91,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
end end
end end
@spec actor(Post.t()) :: Actor.t() | nil
def actor(%Post{author_id: author_id}), def actor(%Post{author_id: author_id}),
do: Actors.get_actor(author_id) do: Actors.get_actor(author_id)
@spec group_actor(Post.t()) :: Actor.t() | nil
def group_actor(%Post{attributed_to_id: attributed_to_id}), def group_actor(%Post{attributed_to_id: attributed_to_id}),
do: Actors.get_actor(attributed_to_id) do: Actors.get_actor(attributed_to_id)
@spec permissions(Post.t()) :: Permission.t()
def permissions(%Post{}) do def permissions(%Post{}) do
%Permission{ %Permission{
access: :member, access: :member,
@ -101,6 +109,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
} }
end end
@spec prepare_args(map()) :: map
defp prepare_args(args) do defp prepare_args(args) do
args args
|> Map.update(:tags, [], &ConverterUtils.fetch_tags/1) |> Map.update(:tags, [], &ConverterUtils.fetch_tags/1)

View file

@ -2,11 +2,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Discussions, Reports} alias Mobilizon.{Actors, Discussions, Reports}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
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()}
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 {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
{:create_report, {:ok, %Report{} = report}} <- {:create_report, {:ok, %Report{} = report}} <-
@ -18,6 +20,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
end end
end end
@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} <- with {:reporter, %Actor{} = reporter_actor} <-
{:reporter, Actors.get_actor!(args.reporter_id)}, {:reporter, Actors.get_actor!(args.reporter_id)},

View file

@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Resources.Resource alias Mobilizon.Resources.Resource
alias Mobilizon.Service.Activity.Resource, as: ResourceActivity alias Mobilizon.Service.Activity.Resource, as: ResourceActivity
@ -16,6 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
@behaviour Entity @behaviour Entity
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, Resource.t(), ActivityStream.t()}
def create(%{type: type} = args, additional) do def create(%{type: type} = args, additional) do
args = args =
case type do case type do
@ -66,6 +68,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
end end
@impl Entity @impl Entity
@spec update(Resource.t(), map(), map()) :: {:ok, Resource.t(), ActivityStream.t()}
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,
@ -104,6 +107,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
end end
end end
@spec update(Resource.t(), map(), map()) :: {:ok, Resource.t(), ActivityStream.t()}
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,
@ -142,6 +146,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
end end
@impl Entity @impl Entity
@spec delete(Resource.t(), Actor.t(), boolean, map()) ::
{:ok, ActivityStream.t(), Actor.t(), Resource.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,
@ -166,11 +172,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
end end
end end
@spec actor(Todo.t()) :: Actor.t() | nil
def actor(%Resource{creator_id: creator_id}), def actor(%Resource{creator_id: creator_id}),
do: Actors.get_actor(creator_id) do: Actors.get_actor(creator_id)
@spec group_actor(Todo.t()) :: Actor.t() | nil
def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id) def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
@spec permissions(TodoList.t()) :: Permission.t()
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

View file

@ -13,7 +13,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
@behaviour Entity @behaviour Entity
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, map()} @spec create(map(), map()) ::
{:ok, TodoList.t(), ActivityStream.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),
@ -26,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
end end
@impl Entity @impl Entity
@spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), Activity.t()} | any @spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), ActivityStream.t()} | any
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),
@ -65,10 +67,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
end end
end end
@spec actor(TodoList.t()) :: nil
def actor(%TodoList{}), do: nil def actor(%TodoList{}), do: nil
@spec group_actor(TodoList.t()) :: Actor.t() | nil
def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id) def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
@spec permissions(TodoList.t()) :: Permission.t()
def permissions(%TodoList{}) do def permissions(%TodoList{}) 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.Todos do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Todos.{Todo, TodoList}
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
@ -12,7 +13,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
@behaviour Entity @behaviour Entity
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, map()} @spec create(map(), map()) :: {:ok, Todo.t(), ActivityStream.t()}
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),
@ -30,7 +31,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
end end
@impl Entity @impl Entity
@spec update(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any @spec update(Todo.t(), map, map) :: {:ok, Todo.t(), ActivityStream.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), %TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
@ -69,8 +70,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
end end
end end
@spec actor(Todo.t()) :: Actor.t() | nil
def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id) def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id)
@spec group_actor(Todo.t()) :: Actor.t() | nil
def group_actor(%Todo{todo_list_id: todo_list_id}) do def group_actor(%Todo{todo_list_id: todo_list_id}) do
case Todos.get_todo_list(todo_list_id) do case Todos.get_todo_list(todo_list_id) do
%TodoList{actor_id: group_id} -> %TodoList{actor_id: group_id} ->
@ -81,6 +84,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
end end
end end
@spec permissions(TodoList.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

@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor 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()) :: {:ok, map()} @spec as_to_model_data(map()) :: map() | {:error, :actor_not_allowed_type}
def as_to_model_data(%{"type" => type} = data) when type in @allowed_types do def as_to_model_data(%{"type" => type} = data) when type in @allowed_types do
avatar = avatar =
download_picture(get_in(data, ["icon", "url"]), get_in(data, ["icon", "name"]), "avatar") download_picture(get_in(data, ["icon", "url"]), get_in(data, ["icon", "name"]), "avatar")
@ -64,7 +64,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
} }
end end
def as_to_model_data(_), do: :error def as_to_model_data(_), do: {:error, :actor_not_allowed_type}
@doc """ @doc """
Convert an actor struct to an ActivityStream representation. Convert an actor struct to an ActivityStream representation.
@ -135,7 +135,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
end end
end end
@spec download_picture(String.t() | nil, String.t(), String.t()) :: map() @spec download_picture(String.t() | nil, String.t(), String.t()) :: map() | nil
defp download_picture(nil, _name, _default_name), do: nil defp download_picture(nil, _name, _default_name), do: nil
defp download_picture(url, name, default_name) do defp download_picture(url, name, default_name) do

View file

@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment 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) :: {:ok, map} | {:error, any()} @spec as_to_model_data(map) :: map | {:error, any()}
def as_to_model_data(object) do def as_to_model_data(object) do
Logger.debug("We're converting raw ActivityStream data to a comment entity") Logger.debug("We're converting raw ActivityStream data to a comment entity")
Logger.debug(inspect(object)) Logger.debug(inspect(object))

View file

@ -8,6 +8,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter do
@type model_data :: map() @type model_data :: map()
@callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data() @callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data() | {:error, any()}
@callback model_to_as(model :: struct()) :: ActivityStream.t() @callback model_to_as(model :: struct()) :: ActivityStream.t()
end end

View file

@ -12,6 +12,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
require Logger require Logger
@ -45,20 +46,27 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
end end
@impl Converter @impl Converter
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()} @spec as_to_model_data(map) :: map() | {:error, any()}
def as_to_model_data(%{"type" => "Note", "name" => name} = object) when not is_nil(name) do def as_to_model_data(%{"type" => "Note", "name" => name} = object) when is_valid_string(name) do
with creator_url <- Map.get(object, "actor"), case extract_actors(object) do
{:ok, %Actor{id: creator_id, suspended: false}} <- %{actor_id: actor_id, creator_id: creator_id} ->
%{actor_id: actor_id, creator_id: creator_id, title: name, url: object["id"]}
{:error, error} ->
{:error, error}
end
end
@spec extract_actors(map()) :: %{actor_id: String.t(), creator_id: String.t()} | {:error, any()}
defp extract_actors(%{"actor" => creator_url, "attributedTo" => actor_url} = _object)
when is_valid_string(creator_url) and is_valid_string(actor_url) do
with {:ok, %Actor{id: creator_id, suspended: false}} <-
ActivityPubActor.get_or_fetch_actor_by_url(creator_url), ActivityPubActor.get_or_fetch_actor_by_url(creator_url),
actor_url <- Map.get(object, "attributedTo"),
{:ok, %Actor{id: actor_id, suspended: false}} <- {:ok, %Actor{id: actor_id, suspended: false}} <-
ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
%{ %{actor_id: actor_id, creator_id: creator_id}
title: name, else
actor_id: actor_id, {:error, error} -> {:error, error}
creator_id: creator_id,
url: object["id"]
}
end end
end end
end end

View file

@ -45,7 +45,7 @@ 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) :: {:ok, map()} | {:error, any()} @spec as_to_model_data(map) :: map() | {:error, any()} | :error
def as_to_model_data(object) do def as_to_model_data(object) do
with {%Actor{id: actor_id}, attributed_to} <- with {%Actor{id: actor_id}, attributed_to} <-
maybe_fetch_actor_and_attributed_to_id(object), maybe_fetch_actor_and_attributed_to_id(object),

View file

@ -33,6 +33,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Member do
} }
end end
@spec as_to_model_data(map()) :: map()
def as_to_model_data(%{ def as_to_model_data(%{
"type" => "Member", "type" => "Member",
"actor" => actor, "actor" => actor,

View file

@ -18,6 +18,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
process_pictures: 2 process_pictures: 2
] ]
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
@behaviour Converter @behaviour Converter
defimpl Convertible, for: Post do defimpl Convertible, for: Post do
@ -63,15 +65,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post 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) :: {:ok, map} | {:error, any()} @spec as_to_model_data(map) :: map() | {:error, any()}
def as_to_model_data( def as_to_model_data(
%{"type" => "Article", "actor" => creator, "attributedTo" => group_uri} = object %{"type" => "Article", "actor" => creator, "attributedTo" => group_uri} = object
) do ) do
with {:ok, %Actor{id: attributed_to_id} = group} <- get_actor(group_uri), with {:ok, %Actor{id: attributed_to_id} = group} <- get_actor(group_uri),
{:ok, %Actor{id: author_id}} <- get_actor(creator), {:ok, %Actor{id: author_id}} <- get_actor(creator) do
{:visibility, visibility} <- {:visibility, get_visibility(object, group)}, [description: description, picture_id: picture_id, medias: medias] =
[description: description, picture_id: picture_id, medias: medias] <- process_pictures(object, attributed_to_id)
process_pictures(object, attributed_to_id) do
%{ %{
title: object["name"], title: object["name"],
body: description, body: description,
@ -82,7 +84,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
publish_at: object["published"], publish_at: object["published"],
picture_id: picture_id, picture_id: picture_id,
medias: medias, medias: medias,
visibility: visibility, visibility: get_visibility(object, group),
draft: object["draft"] == true draft: object["draft"] == true
} }
else else
@ -92,11 +94,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
end end
@spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()} @spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()}
defp get_actor(nil), do: {:error, "nil property found for actor data"} defp get_actor(actor) when is_valid_string(actor),
defp get_actor(actor),
do: actor |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url() do: actor |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url()
defp get_actor(_), do: {:error, "nil property found for actor data"}
@spec to_date(DateTime.t() | NaiveDateTime.t() | nil) :: String.t() | nil
defp to_date(nil), do: nil defp to_date(nil), do: nil
defp to_date(%DateTime{} = date), do: DateTime.to_iso8601(date) defp to_date(%DateTime{} = date), do: DateTime.to_iso8601(date)
defp to_date(%NaiveDateTime{} = date), do: NaiveDateTime.to_iso8601(date) defp to_date(%NaiveDateTime{} = date), do: NaiveDateTime.to_iso8601(date)

View file

@ -56,18 +56,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Resource 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) :: {:ok, map} | {:error, any()} @spec as_to_model_data(map) :: map() | {:error, any()}
def as_to_model_data(%{"type" => type, "actor" => creator, "attributedTo" => group} = object) do def as_to_model_data(%{"type" => type, "actor" => creator, "attributedTo" => group} = object) do
with {:ok, %Actor{id: actor_id, resources_url: resources_url}} <- get_actor(group), with {:ok, %Actor{id: actor_id, resources_url: resources_url}} <- get_actor(group),
{:ok, %Actor{id: creator_id}} <- get_actor(creator), {:ok, %Actor{id: creator_id}} <- get_actor(creator) do
parent_id <- get_parent_id(object["context"], resources_url) do
data = %{ data = %{
title: object["name"], title: object["name"],
summary: object["summary"], summary: object["summary"],
url: object["id"], url: object["id"],
actor_id: actor_id, actor_id: actor_id,
creator_id: creator_id, creator_id: creator_id,
parent_id: parent_id, parent_id: get_parent_id(object["context"], resources_url),
published_at: object["published"] published_at: object["published"]
} }

View file

@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo 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) :: {:ok, map} | {:error, any()} @spec as_to_model_data(map) :: map() | {:error, any()}
def as_to_model_data( def as_to_model_data(
%{"type" => "Todo", "actor" => actor_url, "todoList" => todo_list_url} = object %{"type" => "Todo", "actor" => actor_url, "todoList" => todo_list_url} = object
) do ) do

View file

@ -37,7 +37,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList 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) :: {:ok, map} | {:error, any()} @spec as_to_model_data(map) :: map() | {:error, :group_not_found}
def as_to_model_data(%{"type" => "TodoList", "actor" => actor_url} = object) do def as_to_model_data(%{"type" => "TodoList", "actor" => actor_url} = object) do
case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
{:ok, %Actor{type: :Group, id: group_id} = _group} -> {:ok, %Actor{type: :Group, id: group_id} = _group} ->

View file

@ -19,7 +19,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
@spec key_id_to_actor_url(String.t()) :: String.t() @spec key_id_to_actor_url(String.t()) :: String.t()
def key_id_to_actor_url(key_id) do def key_id_to_actor_url(key_id) do
%{path: path} = %URI{path: path} =
uri = uri =
key_id key_id
|> URI.parse() |> URI.parse()
@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
if is_nil(path) do if is_nil(path) do
uri uri
else else
Map.put(uri, :path, String.trim_trailing(path, "/publickey")) %URI{uri | path: String.trim_trailing(path, "/publickey")}
end end
URI.to_string(uri) URI.to_string(uri)
@ -78,6 +78,9 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
end end
end end
@spec fetch_public_key(Plug.Conn.t()) ::
{:ok, String.t()}
| {:error, :actor_fetch_error | :actor_not_fetchable | :pem_decode_error}
def fetch_public_key(conn) do def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid), actor_id <- key_id_to_actor_url(kid),
@ -87,6 +90,9 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
end end
end end
@spec refetch_public_key(Plug.Conn.t()) ::
{:ok, String.t()}
| {:error, :actor_fetch_error | :actor_not_fetchable | :pem_decode_error}
def refetch_public_key(conn) do def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid), actor_id <- key_id_to_actor_url(kid),
@ -97,6 +103,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
end end
end end
@spec sign(Actor.t(), map()) :: String.t()
def sign(%Actor{domain: domain, keys: keys} = actor, headers) when is_nil(domain) do def sign(%Actor{domain: domain, keys: keys} = actor, headers) when is_nil(domain) do
Logger.debug("Signing a payload on behalf of #{actor.url}") Logger.debug("Signing a payload on behalf of #{actor.url}")
Logger.debug("headers") Logger.debug("headers")
@ -112,14 +119,17 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
raise ArgumentError, message: "Can't do a signature on remote actor #{url}" raise ArgumentError, message: "Can't do a signature on remote actor #{url}"
end end
@spec generate_date_header :: String.t()
def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now()) def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now())
def generate_date_header(%NaiveDateTime{} = date) do def generate_date_header(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end end
@spec generate_request_target(String.t(), String.t()) :: String.t()
def generate_request_target(method, path), do: "#{method} #{path}" def generate_request_target(method, path), do: "#{method} #{path}"
@spec build_digest(String.t()) :: String.t()
def build_digest(body) do def build_digest(body) do
"SHA-256=#{:sha256 |> :crypto.hash(body) |> Base.encode64()}" "SHA-256=#{:sha256 |> :crypto.hash(body) |> Base.encode64()}"
end end

View file

@ -66,10 +66,10 @@ defmodule Mobilizon.Federation.WebFinger do
end end
end end
@spec represent_actor(Actor.t()) :: struct() @spec represent_actor(Actor.t()) :: map()
@spec represent_actor(Actor.t(), String.t()) :: map()
def represent_actor(%Actor{} = actor), do: represent_actor(actor, "JSON") def represent_actor(%Actor{} = actor), do: represent_actor(actor, "JSON")
@spec represent_actor(Actor.t(), String.t()) :: struct()
def represent_actor(%Actor{} = actor, "JSON") do def represent_actor(%Actor{} = actor, "JSON") do
links = links =
[ [
@ -141,11 +141,15 @@ defmodule Mobilizon.Federation.WebFinger do
@doc """ @doc """
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`) Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
""" """
@spec find_webfinger_endpoint(String.t()) :: String.t() @spec find_webfinger_endpoint(String.t()) ::
{:ok, String.t()} | {:error, :link_not_found} | {:error, any()}
def find_webfinger_endpoint(domain) when is_binary(domain) do def find_webfinger_endpoint(domain) when is_binary(domain) do
with {:ok, %{body: body}} <- fetch_document("http://#{domain}/.well-known/host-meta"), with {:ok, %{body: body}} <- fetch_document("http://#{domain}/.well-known/host-meta"),
link_template when is_binary(link_template) <- find_link_from_template(body) do link_template when is_binary(link_template) <- find_link_from_template(body) do
{:ok, link_template} {:ok, link_template}
else
{:error, :link_not_found} -> {:error, :link_not_found}
{:error, error} -> {:error, error}
end end
end end

View file

@ -15,12 +15,13 @@ defmodule Mobilizon.GraphQL.API.Reports do
@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()}
def report(args) do def report(args) do
case {:make_activity, ActivityPub.flag(args, Map.get(args, :forward, false) == true)} do case ActivityPub.flag(args, Map.get(args, :forward, false) == true) do
{:make_activity, {:ok, %Activity{} = activity, %Report{} = report}} -> {:ok, %Activity{} = activity, %Report{} = report} ->
{:ok, activity, report} {:ok, activity, report}
{:make_activity, err} -> err ->
{:error, err} {:error, err}
end end
end end
@ -28,10 +29,12 @@ defmodule Mobilizon.GraphQL.API.Reports do
@doc """ @doc """
Update the state of a report Update the state of a report
""" """
@spec update_report_status(Actor.t(), Report.t(), ReportStatus.t()) ::
{:ok, Report.t()} | {:error, 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} <- with {:valid_state, true} <-
{:valid_state, ReportStatus.valid_value?(state)}, {:valid_state, ReportStatus.valid_value?(state)},
{:ok, report} <- ReportsAction.update_report(report, %{"status" => state}), {:ok, %Report{} = report} <- ReportsAction.update_report(report, %{"status" => state}),
{:ok, _} <- Admin.log_action(actor, "update", report) do {:ok, _} <- Admin.log_action(actor, "update", report) do
{:ok, report} {:ok, report}
else else
@ -42,7 +45,8 @@ defmodule Mobilizon.GraphQL.API.Reports do
@doc """ @doc """
Create a note on a report Create a note on a report
""" """
@spec create_report_note(Report.t(), Actor.t(), String.t()) :: {:ok, Note.t()} @spec create_report_note(Report.t(), Actor.t(), String.t()) ::
{:ok, Note.t()} | {:error, String.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,
@ -67,7 +71,7 @@ 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()} @spec delete_report_note(Note.t(), Actor.t()) :: {:ok, Note.t()} | {:error, 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

View file

@ -10,7 +10,7 @@ defmodule Mobilizon.GraphQL.API.Utils do
@doc """ @doc """
Creates HTML content from text and mentions Creates HTML content from text and mentions
""" """
@spec make_content_html(String.t(), list(), String.t()) :: String.t() @spec make_content_html(String.t(), list(), String.t()) :: {String.t(), list(), list()}
def make_content_html(text, additional_tags, content_type) do def make_content_html(text, additional_tags, content_type) do
with {text, mentions, tags} <- format_input(text, content_type, []) do with {text, mentions, tags} <- format_input(text, content_type, []) do
{text, mentions, additional_tags ++ Enum.map(tags, fn {_, tag} -> tag end)} {text, mentions, additional_tags ++ Enum.map(tags, fn {_, tag} -> tag end)}

View file

@ -66,7 +66,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
%CommentModel{actor_id: comment_actor_id} = comment <- %CommentModel{actor_id: comment_actor_id} = comment <-
Mobilizon.Discussions.get_comment_with_preload(comment_id), Mobilizon.Discussions.get_comment_with_preload(comment_id),
true <- actor_id === comment_actor_id, true <- actor_id == comment_actor_id,
{:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do {:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do
{:ok, comment} {:ok, comment}
end end

View file

@ -6,7 +6,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Event.Utils do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Permission
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
@spec can_event_be_updated_by?(%Event{id: String.t()}, Actor.t()) ::
boolean
def can_event_be_updated_by?( def can_event_be_updated_by?(
%Event{attributed_to: %Actor{type: :Group}} = event, %Event{attributed_to: %Actor{type: :Group}} = event,
%Actor{} = actor_member %Actor{} = actor_member
@ -21,10 +24,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event.Utils do
Event.can_be_managed_by?(event, actor_member_id) Event.can_be_managed_by?(event, actor_member_id)
end end
@spec can_event_be_deleted_by?(%Event{id: String.t(), url: String.t()}, Actor.t()) ::
boolean
def can_event_be_deleted_by?( def can_event_be_deleted_by?(
%Event{attributed_to: %Actor{type: :Group}} = event, %Event{attributed_to: %Actor{type: :Group}, id: event_id, url: event_url} = event,
%Actor{} = actor_member %Actor{} = actor_member
) do )
when is_valid_string(event_id) and is_valid_string(event_url) do
Permission.can_delete_group_object?(actor_member, event) Permission.can_delete_group_object?(actor_member, event)
end end

View file

@ -101,7 +101,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
%Member{actor: %Actor{id: member_actor_id}} = member <- %Member{actor: %Actor{id: member_actor_id}} = member <-
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( ActivityPub.accept(
:invite, :invite,
@ -119,7 +119,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
{:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <- {:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <-
{: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( ActivityPub.reject(
:invite, :invite,

View file

@ -95,6 +95,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
ActivityPub.create(:todo_list, Map.put(args, :actor_id, group_id), true, %{}) do ActivityPub.create(:todo_list, Map.put(args, :actor_id, group_id), true, %{}) do
{:ok, todo_list} {:ok, todo_list}
else else
{:actor, nil} ->
{:error, dgettext("errors", "No profile found for user")}
{:member, _} -> {:member, _} ->
{:error, dgettext("errors", "Profile is not member of group")} {:error, dgettext("errors", "Profile is not member of group")}
end end
@ -187,6 +190,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
ActivityPub.create(:todo, Map.put(args, :creator_id, actor_id), true, %{}) do ActivityPub.create(:todo, Map.put(args, :creator_id, actor_id), true, %{}) do
{:ok, todo} {:ok, todo}
else else
{:actor, nil} ->
{:error, dgettext("errors", "No profile found for user")}
{:todo_list, _} -> {:todo_list, _} ->
{:error, dgettext("errors", "Todo list doesn't exist")} {:error, dgettext("errors", "Todo list doesn't exist")}
@ -212,6 +218,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
ActivityPub.update(todo, args, true, %{}) do ActivityPub.update(todo, args, true, %{}) do
{:ok, todo} {:ok, todo}
else else
{:actor, nil} ->
{:error, dgettext("errors", "No profile found for user")}
{:todo_list, _} -> {:todo_list, _} ->
{:error, dgettext("errors", "Todo list doesn't exist")} {:error, dgettext("errors", "Todo list doesn't exist")}

View file

@ -65,9 +65,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
refresh_token: _refresh_token, refresh_token: _refresh_token,
user: %User{} = user user: %User{} = user
} = user_and_tokens} <- Authenticator.authenticate(email, password), } = user_and_tokens} <- Authenticator.authenticate(email, password),
{:ok, %User{} = user} <- update_user_login_information(user, context), {:ok, %User{} = user} <- update_user_login_information(user, context) do
user_and_tokens <- Map.put(user_and_tokens, :user, user) do {:ok, %{user_and_tokens | user: user}}
{:ok, user_and_tokens}
else else
{:error, :user_not_found} -> {:error, :user_not_found} ->
{:error, :user_not_found} {:error, :user_not_found}
@ -133,7 +132,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, map, any) :: tuple @spec create_user(any, %{email: String.t()}, any) :: tuple
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),
@ -160,20 +159,21 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
end end
end end
@spec check_registration_config(map) :: atom @spec check_registration_config(String.t()) :: atom
defp check_registration_config(email) do defp check_registration_config(email) do
cond do cond do
Config.instance_registrations_open?() -> Config.instance_registrations_open?() ->
:registration_ok :registration_ok
Config.instance_registrations_allowlist?() -> Config.instance_registrations_allowlist?() ->
check_allow_listed_email?(email) check_allow_listed_email(email)
true -> true ->
:registration_closed :registration_closed
end end
end end
@spec check_registration_denylist(String.t()) :: :deny_listed | :not_deny_listed
defp check_registration_denylist(email) do defp check_registration_denylist(email) do
# Remove everything behind the + # Remove everything behind the +
email = String.replace(email, ~r/(\+.*)(?=\@)/, "") email = String.replace(email, ~r/(\+.*)(?=\@)/, "")
@ -183,8 +183,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
else: :not_deny_listed else: :not_deny_listed
end end
@spec check_allow_listed_email?(String.t()) :: :registration_ok | :not_allowlisted @spec check_allow_listed_email(String.t()) :: :registration_ok | :not_allowlisted
defp check_allow_listed_email?(email) do defp check_allow_listed_email(email) do
if email_in_list(email, Config.instance_registrations_allowlist()), if email_in_list(email, Config.instance_registrations_allowlist()),
do: :registration_ok, do: :registration_ok,
else: :not_allowlisted else: :not_allowlisted
@ -199,12 +199,14 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
Validate an user, get its actor and a token Validate an user, get its actor and a token
""" """
@spec validate_user(map(), %{token: String.t()}, map()) :: {:ok, map()}
def validate_user(_parent, %{token: token}, _resolution) do def validate_user(_parent, %{token: token}, _resolution) do
with {:check_confirmation_token, {:ok, %User{} = user}} <- with {:check_confirmation_token, {:ok, %User{} = user}} <-
{:check_confirmation_token, Email.User.check_confirmation_token(token)}, {:check_confirmation_token, Email.User.check_confirmation_token(token)},
{:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)}, {:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)} do
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <- {:ok, %{access_token: access_token, refresh_token: refresh_token}} =
Authenticator.generate_tokens(user) do Authenticator.generate_tokens(user)
{:ok, {:ok,
%{ %{
access_token: access_token, access_token: access_token,
@ -267,12 +269,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
Reset the password from an user Reset the password from an user
""" """
@spec reset_password(map(), %{password: String.t(), token: String.t()}, map()) ::
{:ok, map()} | {:error, String.t()}
def reset_password(_parent, %{password: password, token: token}, _resolution) do def reset_password(_parent, %{password: password, token: token}, _resolution) do
with {:ok, %User{email: email} = user} <- case Email.User.check_reset_password_token(password, token) do
Email.User.check_reset_password_token(password, token), {:ok, %User{email: email} = user} ->
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <- {:ok, tokens} = Authenticator.authenticate(email, password)
Authenticator.authenticate(email, password) do {:ok, Map.put(tokens, :user, user)}
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
{:error, error} ->
{:error, error}
end end
end end
@ -369,6 +375,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|> Repo.update() do |> Repo.update() do
{:ok, user} {:ok, user}
else else
{:can_change_password, false} ->
{:error, dgettext("errors", "You cannot change your password.")}
{:current_password, _} -> {:current_password, _} ->
{:error, dgettext("errors", "The current password is invalid")} {:error, dgettext("errors", "The current password is invalid")}
@ -408,14 +417,18 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
{:ok, user} {:ok, user}
else else
{:current_password, _} -> {:current_password, {:error, _}} ->
{:error, dgettext("errors", "The password provided is invalid")} {:error, dgettext("errors", "The password provided is invalid")}
{:same_email, true} -> {:same_email, true} ->
{:error, dgettext("errors", "The new email must be different")} {:error, dgettext("errors", "The new email must be different")}
{:email_valid, _} -> {:email_valid, false} ->
{:error, dgettext("errors", "The new email doesn't seem to be valid")} {:error, dgettext("errors", "The new email doesn't seem to be valid")}
{:error, %Ecto.Changeset{} = err} ->
Logger.debug(inspect(err))
{:error, dgettext("errors", "Failed to update user email")}
end end
end end
@ -423,12 +436,21 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
{:error, dgettext("errors", "You need to be logged-in to change your email")} {:error, dgettext("errors", "You need to be logged-in to change your email")}
end end
@spec validate_email(map(), %{token: String.t()}, map()) ::
{:ok, User.t()} | {:error, String.t()}
def validate_email(_parent, %{token: token}, _resolution) do def validate_email(_parent, %{token: token}, _resolution) do
with {:get, %User{} = user} <- {:get, Users.get_user_by_activation_token(token)}, case Users.get_user_by_activation_token(token) do
{:ok, %User{} = user} <- Users.validate_email(user) do %User{} = user ->
case Users.validate_email(user) do
{:ok, %User{} = user} ->
{:ok, user} {:ok, user}
else
{:get, nil} -> {:error, %Ecto.Changeset{} = err} ->
Logger.debug(inspect(err))
{:error, dgettext("errors", "Failed to validate user email")}
end
nil ->
{:error, dgettext("errors", "Invalid activation token")} {:error, dgettext("errors", "Invalid activation token")}
end end
end end
@ -547,11 +569,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
def update_locale(_parent, %{locale: locale}, %{ def update_locale(_parent, %{locale: locale}, %{
context: %{current_user: %User{locale: current_locale} = user} context: %{current_user: %User{locale: current_locale} = user}
}) do }) do
with true <- current_locale != locale, if current_locale != locale do
{:ok, %User{} = updated_user} <- Users.update_user(user, %{locale: locale}) do case Users.update_user(user, %{locale: locale}) do
{:ok, %User{} = updated_user} ->
{:ok, updated_user} {:ok, updated_user}
{:error, %Ecto.Changeset{} = err} ->
Logger.debug(err)
{:error, dgettext("errors", "Error while updating locale")}
end
else else
false ->
{:ok, user} {:ok, user}
end end
end end

View file

@ -23,6 +23,7 @@ defmodule Mobilizon.GraphQL.Schema.TagType do
object :tag_queries do object :tag_queries do
@desc "Get the list of tags" @desc "Get the list of tags"
field :tags, non_null(list_of(:tag)) do field :tags, non_null(list_of(:tag)) do
arg(:filter, :string, description: "The filter to apply to the search")
arg(:page, :integer, default_value: 1, description: "The page in the paginated tags list") arg(:page, :integer, default_value: 1, description: "The page in the paginated tags list")
arg(:limit, :integer, default_value: 10, description: "The limit of tags per page") arg(:limit, :integer, default_value: 10, description: "The limit of tags per page")
resolve(&Tag.list_tags/3) resolve(&Tag.list_tags/3)

View file

@ -71,9 +71,6 @@ defmodule Mix.Tasks.Mobilizon.Actors.Refresh do
Actor #{preferred_username} refreshed Actor #{preferred_username} refreshed
""") """)
{:actor, nil} ->
shell_error("Error: No such actor")
{:error, err} when is_binary(err) -> {:error, err} when is_binary(err) ->
shell_error(err) shell_error(err)

View file

@ -25,7 +25,7 @@ defmodule Mix.Tasks.Mobilizon.Actors.Utils do
end end
# Profile from name # Profile from name
@spec username_and_name(String.t() | nil, String.t() | nil) :: String.t() @spec username_and_name(String.t() | nil, String.t() | nil) :: {String.t(), String.t()}
def username_and_name(nil, profile_name) do def username_and_name(nil, profile_name) do
{generate_username(profile_name), profile_name} {generate_username(profile_name), profile_name}
end end

View file

@ -61,7 +61,7 @@ defmodule Mix.Tasks.Mobilizon.Common do
else: IO.puts(message) else: IO.puts(message)
end end
@spec shell_error(String.t()) :: :ok @spec shell_error(String.t(), Keyword.t()) :: nil | no_return
def shell_error(message, options \\ []) do def shell_error(message, options \\ []) do
if mix_shell?() do if mix_shell?() do
Mix.shell().error(message) Mix.shell().error(message)

View file

@ -35,6 +35,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
@preferred_cli_env "prod" @preferred_cli_env "prod"
@shortdoc "Generates a new config" @shortdoc "Generates a new config"
@spec run(list(binary())) :: no_return
def run(["gen" | options]) do def run(["gen" | options]) do
{options, [], []} = {options, [], []} =
OptionParser.parse( OptionParser.parse(

View file

@ -54,13 +54,20 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
), ),
{:makes_changes, true} <- {:makes_changes, attrs != %{}}, {:makes_changes, true} <- {:makes_changes, attrs != %{}},
{:ok, %User{} = user} <- Users.update_user(user, attrs) do {:ok, %User{} = user} <- Users.update_user(user, attrs) do
status =
case user.confirmed_at do
%DateTime{} = confirmed_at ->
"Activated on #{DateTime.to_string(confirmed_at)} (UTC)"
_ ->
"disabled"
end
shell_info(""" shell_info("""
An user has been modified with the following information: An user has been modified with the following information:
- email: #{user.email} - email: #{user.email}
- Role: #{user.role} - Role: #{user.role}
- account status: #{if user.confirmed_at, - account status: #{status}
do: "activated on #{DateTime.to_string(user.confirmed_at)} (UTC)",
else: "disabled"}
""") """)
else else
{:makes_changes, false} -> {:makes_changes, false} ->

View file

@ -78,6 +78,8 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
shell_error("mobilizon.users.new requires an email as argument") shell_error("mobilizon.users.new requires an email as argument")
end end
@spec create_user(String.t(), String.t() | nil, String.t(), Keyword.t()) ::
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
defp create_user(email, provider, password, options) do defp create_user(email, provider, password, options) do
role = get_role(options) role = get_role(options)
@ -96,6 +98,8 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
end end
end end
@spec create_database_user(String.t(), String.t(), role()) ::
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
defp create_database_user(email, password, role) do defp create_database_user(email, password, role) do
Users.register(%{ Users.register(%{
email: email, email: email,
@ -107,6 +111,8 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
}) })
end end
@spec create_user_from_provider(String.t(), String.t(), role()) ::
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
defp create_user_from_provider(email, provider, role) do defp create_user_from_provider(email, provider, role) do
Users.create_external(email, provider, %{role: role}) Users.create_external(email, provider, %{role: role})
end end
@ -137,6 +143,7 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
end end
end end
@spec check_password_and_provider_options(Keyword.t()) :: nil | no_return()
defp check_password_and_provider_options(options) do defp check_password_and_provider_options(options) do
if Keyword.get(options, :password) != nil && Keyword.get(options, :provider) != nil do if Keyword.get(options, :password) != nil && Keyword.get(options, :provider) != nil do
shell_error(""" shell_error("""

View file

@ -17,11 +17,18 @@ defmodule Mix.Tasks.Mobilizon.Users.Show do
with {:ok, %User{} = user} <- Users.get_user_by_email(email), with {:ok, %User{} = user} <- Users.get_user_by_email(email),
actors <- Users.get_actors_for_user(user) do actors <- Users.get_actors_for_user(user) do
status =
case user.confirmed_at do
%DateTime{} = confirmed_at ->
"Activated on #{DateTime.to_string(confirmed_at)} (UTC)"
_ ->
"disabled"
end
shell_info(""" shell_info("""
Informations for the user #{user.email}: Informations for the user #{user.email}:
- account status: #{if user.confirmed_at, - account status: #{status}
do: "Activated on #{DateTime.to_string(user.confirmed_at)} (UTC)",
else: "disabled"}
- Role: #{user.role} - Role: #{user.role}
#{display_actors(actors)} #{display_actors(actors)}
""") """)

View file

@ -1466,7 +1466,7 @@ defmodule Mobilizon.Actors do
@spec actors_for_location(Ecto.Query.t(), String.t(), integer()) :: Ecto.Query.t() @spec actors_for_location(Ecto.Query.t(), String.t(), integer()) :: Ecto.Query.t()
defp actors_for_location(query, location, radius) defp actors_for_location(query, location, radius)
when is_valid_string?(location) and not is_nil(radius) do when is_valid_string(location) and not is_nil(radius) do
with {lon, lat} <- Geohax.decode(location), with {lon, lat} <- Geohax.decode(location),
point <- Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})") do point <- Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})") do
query query

View file

@ -4,7 +4,6 @@ defmodule Mobilizon.Config do
""" """
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.GitStatus alias Mobilizon.Service.GitStatus
@spec instance_config :: keyword @spec instance_config :: keyword
@ -317,14 +316,14 @@ defmodule Mobilizon.Config do
@spec create_cache(atom()) :: integer() @spec create_cache(atom()) :: integer()
defp create_cache(:anonymous_actor_id) do defp create_cache(:anonymous_actor_id) do
with {:ok, %Actor{id: actor_id}} <- Actors.get_or_create_internal_actor("anonymous") do with {:ok, %{id: actor_id}} <- Actors.get_or_create_internal_actor("anonymous") do
actor_id actor_id
end end
end end
@spec create_cache(atom()) :: integer() @spec create_cache(atom()) :: integer()
defp create_cache(:relay_actor_id) do defp create_cache(:relay_actor_id) do
with {:ok, %Actor{id: actor_id}} <- Actors.get_or_create_internal_actor("relay") do with {:ok, %{id: actor_id}} <- Actors.get_or_create_internal_actor("relay") do
actor_id actor_id
end end
end end

View file

@ -249,8 +249,7 @@ defmodule Mobilizon.Discussions do
{:ok, comment} {:ok, comment}
end end
else else
comment Repo.delete(comment)
|> Repo.delete()
end end
end end

View file

@ -206,6 +206,7 @@ defmodule Mobilizon.Events.Event do
defp put_tags(%Changeset{} = changeset, _), do: changeset defp put_tags(%Changeset{} = changeset, _), do: changeset
@spec process_tag(map() | Tag.t()) :: Tag.t() | Ecto.Changeset.t()
# We need a changeset instead of a raw struct because of slug which is generated in changeset # We need a changeset instead of a raw struct because of slug which is generated in changeset
defp process_tag(%{id: id} = _tag) do defp process_tag(%{id: id} = _tag) do
Events.get_tag(id) Events.get_tag(id)
@ -248,13 +249,8 @@ defmodule Mobilizon.Events.Event do
# In case the provided picture is an existing one # In case the provided picture is an existing one
@spec put_picture(Changeset.t(), map) :: Changeset.t() @spec put_picture(Changeset.t(), map) :: Changeset.t()
defp put_picture(%Changeset{} = changeset, %{picture: %{media_id: id} = _picture}) do defp put_picture(%Changeset{} = changeset, %{picture: %{media_id: id} = _picture}) do
case Medias.get_media!(id) do %Media{} = picture = Medias.get_media!(id)
%Media{} = picture ->
put_assoc(changeset, :picture, picture) put_assoc(changeset, :picture, picture)
_ ->
changeset
end
end end
# In case it's a new picture # In case it's a new picture

View file

@ -631,9 +631,10 @@ defmodule Mobilizon.Events do
@doc """ @doc """
Returns the list of tags. Returns the list of tags.
""" """
@spec list_tags(integer | nil, integer | nil) :: [Tag.t()] @spec list_tags(String.t() | nil, integer | nil, integer | nil) :: [Tag.t()]
def list_tags(page \\ nil, limit \\ nil) do def list_tags(filter \\ nil, page \\ nil, limit \\ nil) do
Tag Tag
|> tag_filter(filter)
|> Page.paginate(page, limit) |> Page.paginate(page, limit)
|> Repo.all() |> Repo.all()
end end
@ -1396,7 +1397,7 @@ defmodule Mobilizon.Events do
end end
@spec events_for_tags(Ecto.Query.t(), map()) :: Ecto.Query.t() @spec events_for_tags(Ecto.Query.t(), map()) :: Ecto.Query.t()
defp events_for_tags(query, %{tags: tags}) when is_valid_string?(tags) do defp events_for_tags(query, %{tags: tags}) when is_valid_string(tags) do
query query
|> join(:inner, [q], te in "events_tags", on: q.id == te.event_id) |> join(:inner, [q], te in "events_tags", on: q.id == te.event_id)
|> join(:inner, [q, ..., te], t in Tag, on: te.tag_id == t.id) |> join(:inner, [q, ..., te], t in Tag, on: te.tag_id == t.id)
@ -1410,7 +1411,7 @@ defmodule Mobilizon.Events do
do: query do: query
defp events_for_location(query, %{location: location, radius: radius}) defp events_for_location(query, %{location: location, radius: radius})
when is_valid_string?(location) and not is_nil(radius) do when is_valid_string(location) and not is_nil(radius) do
with {lon, lat} <- Geohax.decode(location), with {lon, lat} <- Geohax.decode(location),
point <- Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})") do point <- Geo.WKT.decode!("SRID=4326;POINT(#{lon} #{lat})") do
query query
@ -1471,6 +1472,16 @@ defmodule Mobilizon.Events do
from(t in Tag, where: t.title == ^title, limit: 1) from(t in Tag, where: t.title == ^title, limit: 1)
end end
@spec tag_filter(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t()
defp tag_filter(query, nil), do: query
defp tag_filter(query, ""), do: query
defp tag_filter(query, filter) when is_binary(filter) do
query
|> where([q], ilike(q.slug, ^"%#{filter}%"))
|> or_where([q], ilike(q.title, ^"%#{filter}%"))
end
@spec tags_for_event_query(integer) :: Ecto.Query.t() @spec tags_for_event_query(integer) :: Ecto.Query.t()
defp tags_for_event_query(event_id) do defp tags_for_event_query(event_id) do
from( from(

View file

@ -10,7 +10,7 @@ defmodule Mobilizon.Events.Participant do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Event, ParticipantRole} alias Mobilizon.Events.{Event, ParticipantRole}
alias Mobilizon.Web.Email.Checker alias Mobilizon.Events.Participant.Metadata
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
@ -24,7 +24,6 @@ defmodule Mobilizon.Events.Participant do
@required_attrs [:url, :role, :event_id, :actor_id] @required_attrs [:url, :role, :event_id, :actor_id]
@attrs @required_attrs @attrs @required_attrs
@metadata_attrs [:email, :confirmation_token, :cancellation_token, :message, :locale]
@timestamps_opts [type: :utc_datetime] @timestamps_opts [type: :utc_datetime]
@ -33,13 +32,7 @@ defmodule Mobilizon.Events.Participant do
field(:role, ParticipantRole, default: :participant) field(:role, ParticipantRole, default: :participant)
field(:url, :string) field(:url, :string)
embeds_one :metadata, Metadata, on_replace: :delete do embeds_one(:metadata, Metadata, on_replace: :delete)
field(:email, :string)
field(:confirmation_token, :string)
field(:cancellation_token, :string)
field(:message, :string)
field(:locale, :string)
end
belongs_to(:event, Event, primary_key: true) belongs_to(:event, Event, primary_key: true)
belongs_to(:actor, Actor, primary_key: true) belongs_to(:actor, Actor, primary_key: true)
@ -68,18 +61,12 @@ defmodule Mobilizon.Events.Participant do
def changeset(%__MODULE__{} = participant, attrs) do def changeset(%__MODULE__{} = participant, attrs) do
participant participant
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> cast_embed(:metadata, with: &metadata_changeset/2) |> cast_embed(:metadata)
|> ensure_url() |> ensure_url()
|> validate_required(@required_attrs) |> validate_required(@required_attrs)
|> unique_constraint(:actor_id, name: :participants_event_id_actor_id_index) |> unique_constraint(:actor_id, name: :participants_event_id_actor_id_index)
end end
defp metadata_changeset(schema, params) do
schema
|> cast(params, @metadata_attrs)
|> Checker.validate_changeset()
end
# If there's a blank URL that's because we're doing the first insert # If there's a blank URL that's because we're doing the first insert
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t() @spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do

View file

@ -0,0 +1,36 @@
defmodule Mobilizon.Events.Participant.Metadata do
@moduledoc """
Participation stats on event
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Web.Email.Checker
@type t :: %__MODULE__{
email: String.t(),
confirmation_token: String.t(),
cancellation_token: String.t(),
message: String.t(),
locale: String.t()
}
@attrs [:email, :confirmation_token, :cancellation_token, :message, :locale]
@derive Jason.Encoder
embedded_schema do
field(:email, :string)
field(:confirmation_token, :string)
field(:cancellation_token, :string)
field(:message, :string)
field(:locale, :string)
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(schema, params) do
schema
|> cast(params, @attrs)
|> Checker.validate_changeset()
end
end

View file

@ -51,7 +51,7 @@ defmodule Mobilizon.Medias.Media do
end end
@doc false @doc false
@spec changeset(struct(), map) :: Ecto.Changeset.t() @spec metadata_changeset(Metadata.t(), map) :: Ecto.Changeset.t()
def metadata_changeset(metadata, attrs) do def metadata_changeset(metadata, attrs) do
metadata metadata
|> cast(attrs, @metadata_attrs) |> cast(attrs, @metadata_attrs)

View file

@ -19,9 +19,17 @@ defmodule Mobilizon.Service.Activity.Renderer do
require Logger require Logger
import Mobilizon.Web.Gettext, only: [dgettext: 3] import Mobilizon.Web.Gettext, only: [dgettext: 3]
@type render :: %{body: String.t(), url: String.t()} @type render :: %{
body: String.t(),
url: String.t(),
timestamp: String.t(),
locale: String.t(),
title: String.t()
}
@callback render(entity :: Activity.t(), Keyword.t()) :: render() @type common_render :: %{body: String.t(), url: String.t()}
@callback render(entity :: Activity.t(), Keyword.t()) :: common_render()
@spec render(Activity.t()) :: render() @spec render(Activity.t()) :: render()
def render(%Activity{} = activity, options \\ []) do def render(%Activity{} = activity, options \\ []) do
@ -43,6 +51,7 @@ defmodule Mobilizon.Service.Activity.Renderer do
res res
end end
@spec do_render(Activity.t(), Keyword.t()) :: common_render()
defp do_render(%Activity{type: type} = activity, options) do defp do_render(%Activity{type: type} = activity, options) do
case type do case type do
:discussion -> Discussion.render(activity, options) :discussion -> Discussion.render(activity, options)

View file

@ -12,7 +12,7 @@ defmodule Mobilizon.Service.Activity.Utils do
|> add_activity_object() |> add_activity_object()
end end
@spec add_activity_object(Activity.t()) :: Activity.t() @spec add_activity_object(Activity.t()) :: map()
def add_activity_object(%Activity{} = activity) do def add_activity_object(%Activity{} = activity) do
Map.put(activity, :object, ActivityService.object(activity)) Map.put(activity, :object, ActivityService.object(activity))
end end

View file

@ -143,15 +143,13 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
if is_nil(value), do: url, else: do_add_parameter(url, key, value) if is_nil(value), do: url, else: do_add_parameter(url, key, value)
end end
@spec do_add_parameter(String.t(), atom(), any()) :: String.t() @spec do_add_parameter(String.t(), :zoom | :country_code | :api_key, any()) :: String.t()
defp do_add_parameter(url, :zoom, zoom), defp do_add_parameter(url, :zoom, zoom),
do: "#{url}&zoom=#{zoom}" do: "#{url}&zoom=#{zoom}"
@spec do_add_parameter(String.t(), atom(), any()) :: String.t()
defp do_add_parameter(url, :country_code, country_code), defp do_add_parameter(url, :country_code, country_code),
do: "#{url}&countrycodes=#{country_code}" do: "#{url}&countrycodes=#{country_code}"
@spec do_add_parameter(String.t(), atom(), any()) :: String.t()
defp do_add_parameter(url, :api_key, api_key), defp do_add_parameter(url, :api_key, api_key),
do: "#{url}&key=#{api_key}" do: "#{url}&key=#{api_key}"

View file

@ -67,7 +67,7 @@ defmodule Mobilizon.Service.Geospatial.Provider do
@doc """ @doc """
Returns a `Geo.Point` for given coordinates Returns a `Geo.Point` for given coordinates
""" """
@spec coordinates([number], number) :: Geo.Point.t() @spec coordinates([number], number) :: Geo.Point.t() | nil
def coordinates(coords, srid \\ 4326) def coordinates(coords, srid \\ 4326)
def coordinates([x, y], srid) when is_number(x) and is_number(y) do def coordinates([x, y], srid) when is_number(x) and is_number(y) do
@ -78,7 +78,6 @@ defmodule Mobilizon.Service.Geospatial.Provider do
%Geo.Point{coordinates: {String.to_float(x), String.to_float(y)}, srid: srid} %Geo.Point{coordinates: {String.to_float(x), String.to_float(y)}, srid: srid}
end end
@spec coordinates(any) :: nil
def coordinates(_, _), do: nil def coordinates(_, _), do: nil
@spec endpoint(atom()) :: String.t() @spec endpoint(atom()) :: String.t()

View file

@ -3,7 +3,31 @@ defmodule Mobilizon.Service.Guards do
Various guards Various guards
""" """
defguard is_valid_string?(value) when is_binary(value) and value != "" @doc """
Returns `true` if `term` is a valid string and not empty.
defguard is_valid_list?(value) when is_list(value) and length(value) > 0 ## Examples
iex> is_valid_string("one")
true
iex> is_valid_string("")
false
iex> is_valid_string(2)
false
"""
defguard is_valid_string(term) when is_binary(term) and term != ""
@doc """
Returns `true` if `term` is a valid list and not empty.
## Examples
iex> is_valid_list(["one"])
true
iex> is_valid_list([])
false
iex> is_valid_list("foo")
false
"""
defguard is_valid_list(term) when is_list(term) and length(term) > 0
end end

View file

@ -64,7 +64,7 @@ defmodule Mobilizon.Service.LanguageDetection do
def normalize(language) do def normalize(language) do
case Cldr.AcceptLanguage.parse(language, Mobilizon.Cldr) do case Cldr.AcceptLanguage.parse(language, Mobilizon.Cldr) do
{:ok, [{_, tag}]} -> {:ok, [{_, %Cldr.LanguageTag{} = tag}]} ->
tag.language tag.language
_ -> _ ->

View file

@ -9,17 +9,20 @@ defmodule Mobilizon.Web.Auth.Context do
alias Mobilizon.Service.ErrorReporting.Sentry, as: SentryAdapter alias Mobilizon.Service.ErrorReporting.Sentry, as: SentryAdapter
alias Mobilizon.Users.User alias Mobilizon.Users.User
@spec init(Plug.opts()) :: Plug.opts()
def init(opts) do def init(opts) do
opts opts
end end
@spec call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
def call(%{assigns: %{ip: _}} = conn, _opts), do: conn def call(%{assigns: %{ip: _}} = conn, _opts), do: conn
def call(conn, _opts) do def call(conn, _opts) do
set_user_information_in_context(conn) set_user_information_in_context(conn)
end end
def set_user_information_in_context(conn) do @spec set_user_information_in_context(Plug.Conn.t()) :: Plug.Conn.t()
defp set_user_information_in_context(conn) do
context = %{ip: conn.remote_ip |> :inet.ntoa() |> to_string()} context = %{ip: conn.remote_ip |> :inet.ntoa() |> to_string()}
{conn, context} = {conn, context} =

View file

@ -4,16 +4,19 @@ defmodule Mobilizon.Web.Cache do
""" """
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Web.Cache.ActivityPub alias Mobilizon.Web.Cache.ActivityPub
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
@caches [:activity_pub, :feed, :ics] @caches [:activity_pub, :feed, :ics]
@type local_actor :: %Actor{domain: nil}
@doc """ @doc """
Clears all caches for an actor. Clears all caches for a local actor.
""" """
@spec clear_cache(Actor.t()) :: {:ok, true} @spec clear_cache(%Actor{domain: nil, preferred_username: String.t()}) :: :ok
def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do def clear_cache(%Actor{preferred_username: preferred_username, domain: nil})
when is_valid_string(preferred_username) do
Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username)) Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username))
end end

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.Web.GraphQLSocket do
alias Mobilizon.Users.User alias Mobilizon.Users.User
@spec connect(map, Phoenix.Socket.t()) :: {:ok, Phoenix.Socket.t()} | :error
def connect(%{"token" => token}, socket) do def connect(%{"token" => token}, socket) do
with {:ok, authed_socket} <- with {:ok, authed_socket} <-
Guardian.Phoenix.Socket.authenticate(socket, Mobilizon.Web.Auth.Guardian, token), Guardian.Phoenix.Socket.authenticate(socket, Mobilizon.Web.Auth.Guardian, token),
@ -26,5 +27,6 @@ defmodule Mobilizon.Web.GraphQLSocket do
def connect(_args, _socket), do: :error def connect(_args, _socket), do: :error
@spec id(any) :: nil
def id(_socket), do: nil def id(_socket), do: nil
end end

View file

@ -12,7 +12,7 @@ defmodule Mobilizon.Web.Email.Activity do
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Web.Email alias Mobilizon.Web.Email
@spec direct_activity(String.t(), list(), String.t()) :: @spec direct_activity(String.t(), list(), Keyword.t()) ::
Bamboo.Email.t() Bamboo.Email.t()
def direct_activity( def direct_activity(
email, email,

View file

@ -5,6 +5,7 @@ defmodule Mobilizon.Web.Email.Mailer do
use Bamboo.Mailer, otp_app: :mobilizon use Bamboo.Mailer, otp_app: :mobilizon
alias Mobilizon.Service.ErrorReporting.Sentry alias Mobilizon.Service.ErrorReporting.Sentry
@spec send_email_later(Bamboo.Email.t()) :: Bamboo.Email.t()
def send_email_later(email) do def send_email_later(email) do
Mobilizon.Web.Email.Mailer.deliver_later!(email) Mobilizon.Web.Email.Mailer.deliver_later!(email)
rescue rescue
@ -17,6 +18,7 @@ defmodule Mobilizon.Web.Email.Mailer do
reraise error, __STACKTRACE__ reraise error, __STACKTRACE__
end end
@spec send_email(Bamboo.Email.t()) :: Bamboo.Email.t() | {Bamboo.Email.t(), any()}
def send_email(email) do def send_email(email) do
Mobilizon.Web.Email.Mailer.deliver_now!(email) Mobilizon.Web.Email.Mailer.deliver_now!(email)
rescue rescue

View file

@ -50,7 +50,6 @@ defmodule Mobilizon.Web.Email.Participation do
Bamboo.Email.t() Bamboo.Email.t()
def participation_updated(user, participant, locale \\ "en") def participation_updated(user, participant, locale \\ "en")
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated( def participation_updated(
%User{email: email}, %User{email: email},
%Participant{} = participant, %Participant{} = participant,
@ -58,7 +57,6 @@ defmodule Mobilizon.Web.Email.Participation do
), ),
do: participation_updated(email, participant, locale) do: participation_updated(email, participant, locale)
@spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated( def participation_updated(
email, email,
%Participant{event: event, role: :rejected}, %Participant{event: event, role: :rejected},
@ -79,7 +77,6 @@ defmodule Mobilizon.Web.Email.Participation do
|> render(:event_participation_rejected) |> render(:event_participation_rejected)
end end
@spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated( def participation_updated(
email, email,
%Participant{event: %Event{join_options: :free} = event, role: :participant}, %Participant{event: %Event{join_options: :free} = event, role: :participant},
@ -100,7 +97,6 @@ defmodule Mobilizon.Web.Email.Participation do
|> render(:event_participation_confirmed) |> render(:event_participation_confirmed)
end end
@spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
def participation_updated( def participation_updated(
email, email,
%Participant{event: event, role: :participant}, %Participant{event: event, role: :participant},

View file

@ -57,6 +57,7 @@ defmodule Mobilizon.Web.Email.User do
|> render(:password_reset) |> render(:password_reset)
end end
@spec check_confirmation_token(String.t()) :: {:ok, User.t()} | {:error, :invalid_token}
def check_confirmation_token(token) when is_binary(token) do def check_confirmation_token(token) when is_binary(token) do
with %User{} = user <- Users.get_user_by_activation_token(token), with %User{} = user <- Users.get_user_by_activation_token(token),
{:ok, %User{} = user} <- {:ok, %User{} = user} <-
@ -86,7 +87,7 @@ defmodule Mobilizon.Web.Email.User do
end end
end end
@spec send_confirmation_email(User.t(), String.t()) :: {:ok, Bamboo.Email.t()} | {:error, any()} @spec send_confirmation_email(User.t(), String.t()) :: Bamboo.Email.t()
def send_confirmation_email(%User{} = user, locale \\ "en") do def send_confirmation_email(%User{} = user, locale \\ "en") do
user user
|> Email.User.confirmation_email(locale) |> Email.User.confirmation_email(locale)
@ -96,7 +97,8 @@ defmodule Mobilizon.Web.Email.User do
@doc """ @doc """
Check that the provided token is correct and update provided password Check that the provided token is correct and update provided password
""" """
@spec check_reset_password_token(String.t(), String.t()) :: tuple @spec check_reset_password_token(String.t(), String.t()) ::
{:ok, User.t()} | {:error, String.t()}
def check_reset_password_token(password, token) do def check_reset_password_token(password, token) do
with %User{} = user <- Users.get_user_by_reset_password_token(token), with %User{} = user <- Users.get_user_by_reset_password_token(token),
{:ok, %User{} = user} <- {:ok, %User{} = user} <-

View file

@ -21,8 +21,8 @@ defmodule Mobilizon.Web do
quote do quote do
use Phoenix.Controller, namespace: Mobilizon.Web use Phoenix.Controller, namespace: Mobilizon.Web
import Plug.Conn import Plug.Conn
import Mobilizon.Web.Router.Helpers
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
alias Mobilizon.Web.Router.Helpers, as: Routes
end end
end end
@ -33,14 +33,13 @@ defmodule Mobilizon.Web do
pattern: "**/*", pattern: "**/*",
namespace: Mobilizon.Web namespace: Mobilizon.Web
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc) # Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML use Phoenix.HTML
import Mobilizon.Web.Router.Helpers import Phoenix.View
import Mobilizon.Web.ErrorHelpers
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
alias Mobilizon.Web.Router.Helpers, as: Routes
end end
end end

View file

@ -19,12 +19,13 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
if Config.get([:http_security, :enabled]) do if Config.get([:http_security, :enabled]) do
conn conn
|> merge_resp_headers(headers(options)) |> merge_resp_headers(headers(options))
|> maybe_send_sts_header(Config.get([:http_security, :sts])) |> maybe_send_sts_header(Config.get([:http_security, :sts], false))
else else
conn conn
end end
end end
@spec headers(Keyword.t()) :: list({String.t(), String.t()})
defp headers(options) do defp headers(options) do
referrer_policy = referrer_policy =
Keyword.get(options, :referrer_policy, Config.get([:http_security, :referrer_policy])) Keyword.get(options, :referrer_policy, Config.get([:http_security, :referrer_policy]))
@ -55,6 +56,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
@style_src "style-src 'self' " @style_src "style-src 'self' "
@font_src "font-src 'self' " @font_src "font-src 'self' "
@spec csp_string(Keyword.t()) :: String.t()
defp csp_string(options) do defp csp_string(options) do
scheme = Keyword.get(options, :scheme, Config.get([Pleroma.Web.Endpoint, :url])[:scheme]) scheme = Keyword.get(options, :scheme, Config.get([Pleroma.Web.Endpoint, :url])[:scheme])
static_url = Mobilizon.Web.Endpoint.static_url() static_url = Mobilizon.Web.Endpoint.static_url()
@ -115,10 +117,11 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
|> to_string() |> to_string()
end end
@spec add_csp_param(list(), list(String.t()) | String.t() | nil) :: list()
defp add_csp_param(csp_iodata, nil), do: csp_iodata defp add_csp_param(csp_iodata, nil), do: csp_iodata
defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata] defp add_csp_param(csp_iodata, param), do: [[param, ?;] | csp_iodata]
@spec maybe_send_sts_header(Plug.Conn.t(), boolean()) :: Plug.Conn.t()
defp maybe_send_sts_header(conn, true) do defp maybe_send_sts_header(conn, true) do
max_age_sts = Config.get([:http_security, :sts_max_age]) max_age_sts = Config.get([:http_security, :sts_max_age])
@ -127,8 +130,9 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
]) ])
end end
defp maybe_send_sts_header(conn, _), do: conn defp maybe_send_sts_header(conn, false), do: conn
@spec get_csp_config(atom(), Keyword.t()) :: String.t()
defp get_csp_config(type, options) do defp get_csp_config(type, options) do
options options
|> Keyword.get(type, Config.get([:http_security, :csp_policy, type])) |> Keyword.get(type, Config.get([:http_security, :csp_policy, type]))

View file

@ -16,6 +16,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSignatures do
options options
end end
@spec call(Plug.Conn.t(), any) :: Plug.Conn.t()
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
conn conn
end end

View file

@ -45,11 +45,13 @@ defmodule Mobilizon.Web.Plugs.UploadedMedia do
config = Config.get([Upload]) config = Config.get([Upload])
with uploader <- Keyword.fetch!(config, :uploader), uploader = Keyword.fetch!(config, :uploader)
proxy_remote = Keyword.get(config, :proxy_remote, false), proxy_remote = Keyword.get(config, :proxy_remote, false)
{:ok, get_method} <- uploader.get_file(file) do
case uploader.get_file(file) do
{:ok, get_method} ->
get_media(conn, get_method, proxy_remote, opts) get_media(conn, get_method, proxy_remote, opts)
else
_ -> _ ->
conn conn
|> send_resp(500, "Failed") |> send_resp(500, "Failed")
@ -59,6 +61,12 @@ defmodule Mobilizon.Web.Plugs.UploadedMedia do
def call(conn, _opts), do: conn def call(conn, _opts), do: conn
@spec get_media(
Plug.Conn.t(),
{:static_dir, String.t()} | {:url, String.t()} | any(),
boolean,
any()
) :: Plug.Conn.t()
defp get_media(conn, {:static_dir, directory}, _, opts) do defp get_media(conn, {:static_dir, directory}, _, opts) do
static_opts = static_opts =
opts opts

View file

@ -381,6 +381,10 @@ defmodule Mobilizon.Web.ReverseProxy do
defp body_size_constraint(_, _), do: :ok defp body_size_constraint(_, _), do: :ok
@spec check_read_duration(any(), integer()) ::
{:ok, {integer(), integer()}}
| {:ok, :no_duration_limit, :no_duration_limit}
| {:error, :read_duration_exceeded}
defp check_read_duration(duration, max) defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do when is_integer(duration) and is_integer(max) and max > 0 do
if duration > max do if duration > max do

View file

@ -5,7 +5,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]
@ -20,7 +20,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]
@ -37,7 +37,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]
@ -52,7 +52,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]

View file

@ -4,22 +4,22 @@
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :participation_event_comment -> %><%= dgettext("activity", "%{profile} has posted an announcement under event %{event}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :participation_event_comment -> %><%= dgettext("activity", "%{profile} has posted an announcement under event %{event}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_new_comment -> %><%= if @activity.subject_params["comment_reply_to"] do %><%=dgettext("activity", "%{profile} has posted a new reply under your event %{event}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_new_comment -> %><%= if @activity.subject_params["comment_reply_to"] do %><%=dgettext("activity", "%{profile} has posted a new reply under your event %{event}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= "#{page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode()}#comment-#{@activity.subject_params["comment_reply_to_uuid"]}-#{@activity.subject_params["comment_uuid"]}" %><% else %><%= dgettext("activity", "%{profile} has posted a new comment under your event %{event}.", <%= "#{Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode()}#comment-#{@activity.subject_params["comment_reply_to_uuid"]}-#{@activity.subject_params["comment_uuid"]}" %><% else %><%= dgettext("activity", "%{profile} has posted a new comment under your event %{event}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= "#{page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode()}#comment-#{@activity.subject_params["comment_uuid"]}"%><% end %><% end %> <%= "#{Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode()}#comment-#{@activity.subject_params["comment_uuid"]}"%><% end %><% end %>

View file

@ -5,7 +5,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
discussion: "<a href=\"#{ discussion: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:discussion, :discussion,
Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"] Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]
@ -19,7 +19,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
discussion: "<a href=\"#{ discussion: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:discussion, :discussion,
Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"] Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]
@ -33,7 +33,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
discussion: "<a href=\"#{ discussion: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:discussion, :discussion,
Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"] Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]
@ -47,7 +47,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
discussion: "<a href=\"#{ discussion: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:discussion, :discussion,
Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"] Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]

View file

@ -4,25 +4,25 @@
discussion: @activity.subject_params["discussion_title"] discussion: @activity.subject_params["discussion_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_replied -> %><%= dgettext("activity", "%{profile} replied to the discussion %{discussion}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_replied -> %><%= dgettext("activity", "%{profile} replied to the discussion %{discussion}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
discussion: @activity.subject_params["discussion_title"] discussion: @activity.subject_params["discussion_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_renamed -> %><%= dgettext("activity", "%{profile} renamed the discussion %{discussion}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_renamed -> %><%= dgettext("activity", "%{profile} renamed the discussion %{discussion}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
discussion: @activity.subject_params["discussion_title"] discussion: @activity.subject_params["discussion_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_archived -> %><%= dgettext("activity", "%{profile} archived the discussion %{discussion}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_archived -> %><%= dgettext("activity", "%{profile} archived the discussion %{discussion}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
discussion: @activity.subject_params["discussion_title"] discussion: @activity.subject_params["discussion_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_deleted -> %><%= dgettext("activity", "%{profile} deleted the discussion %{discussion}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :discussion, Mobilizon.Actors.Actor.preferred_username_and_domain(@activity.group), @activity.subject_params["discussion_slug"]) |> URI.decode() %><% :discussion_deleted -> %><%= dgettext("activity", "%{profile} deleted the discussion %{discussion}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
discussion: @activity.subject_params["discussion_title"] discussion: @activity.subject_params["discussion_title"]

View file

@ -5,7 +5,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]
@ -20,7 +20,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]
@ -44,7 +44,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]
@ -59,7 +59,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]

View file

@ -4,13 +4,13 @@
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_updated -> %><%= dgettext("activity", "The event %{event} was updated by %{profile}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_updated -> %><%= dgettext("activity", "The event %{event} was updated by %{profile}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_deleted -> %><%= dgettext("activity", "The event %{event} was deleted by %{profile}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% :event_deleted -> %><%= dgettext("activity", "The event %{event} was deleted by %{profile}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
@ -22,10 +22,10 @@
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} posted a comment on the event %{event}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} posted a comment on the event %{event}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% end %><% end %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %><% end %><% end %>

View file

@ -5,7 +5,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
group: "<a href=\"#{ group: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:actor, :actor,
@activity.subject_params["group_federated_username"] @activity.subject_params["group_federated_username"]
@ -20,7 +20,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
group: "<a href=\"#{ group: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:actor, :actor,
@activity.subject_params["group_federated_username"] @activity.subject_params["group_federated_username"]

View file

@ -4,10 +4,10 @@
group: @activity.subject_params["group_name"] group: @activity.subject_params["group_name"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :actor, @activity.subject_params["group_federated_username"]) |> URI.decode() %><% :group_updated -> %><%= dgettext("activity", "%{profile} updated the group %{group}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, @activity.subject_params["group_federated_username"]) |> URI.decode() %><% :group_updated -> %><%= dgettext("activity", "%{profile} updated the group %{group}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
group: @activity.subject_params["group_name"] group: @activity.subject_params["group_name"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :actor, @activity.subject_params["group_federated_username"]) |> URI.decode() %><% end %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, @activity.subject_params["group_federated_username"]) |> URI.decode() %><% end %>

View file

@ -5,7 +5,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
post: "<a href=\"#{ post: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:post, :post,
@activity.subject_params["post_slug"] @activity.subject_params["post_slug"]
@ -20,7 +20,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
post: "<a href=\"#{ post: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:post, :post,
@activity.subject_params["post_slug"] @activity.subject_params["post_slug"]

View file

@ -4,13 +4,13 @@
post: @activity.subject_params["post_title"] post: @activity.subject_params["post_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :post, @activity.subject_params["post_slug"]) |> URI.decode() %><% :post_updated -> %><%= dgettext("activity", "The post %{post} was updated by %{profile}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :post, @activity.subject_params["post_slug"]) |> URI.decode() %><% :post_updated -> %><%= dgettext("activity", "The post %{post} was updated by %{profile}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
post: @activity.subject_params["post_title"] post: @activity.subject_params["post_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :post, @activity.subject_params["post_slug"]) |> URI.decode() %><% :post_deleted -> %><%= dgettext("activity", "The post %{post} was deleted by %{profile}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :post, @activity.subject_params["post_slug"]) |> URI.decode() %><% :post_deleted -> %><%= dgettext("activity", "The post %{post} was deleted by %{profile}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
post: @activity.subject_params["post_title"] post: @activity.subject_params["post_title"]

View file

@ -6,7 +6,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
resource: "<a href=\"#{ resource: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:resource, :resource,
@activity.subject_params["resource_uuid"] @activity.subject_params["resource_uuid"]
@ -21,7 +21,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
resource: "<a href=\"#{ resource: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:resource, :resource,
@activity.subject_params["resource_uuid"] @activity.subject_params["resource_uuid"]
@ -38,7 +38,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
resource: "<a href=\"#{ resource: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:resource, :resource,
@activity.subject_params["resource_uuid"] @activity.subject_params["resource_uuid"]
@ -54,7 +54,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
resource: "<a href=\"#{ resource: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:resource, :resource,
@activity.subject_params["resource_uuid"] @activity.subject_params["resource_uuid"]
@ -72,7 +72,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
resource: "<a href=\"#{ resource: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:resource, :resource,
@activity.subject_params["resource_uuid"] @activity.subject_params["resource_uuid"]
@ -87,7 +87,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
resource: "<a href=\"#{ resource: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:resource, :resource,
@activity.subject_params["resource_uuid"] @activity.subject_params["resource_uuid"]

View file

@ -5,39 +5,39 @@
resource: @activity.subject_params["resource_title"] resource: @activity.subject_params["resource_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} created the resource %{resource}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} created the resource %{resource}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
resource: @activity.subject_params["resource_title"] resource: @activity.subject_params["resource_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_renamed -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} renamed the folder from %{old_resource_title} to %{resource}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_renamed -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} renamed the folder from %{old_resource_title} to %{resource}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
resource: @activity.subject_params["resource_title"], resource: @activity.subject_params["resource_title"],
old_resource_title: @activity.subject_params["old_resource_title"] old_resource_title: @activity.subject_params["old_resource_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} renamed the resource from %{old_resource_title} to %{resource}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} renamed the resource from %{old_resource_title} to %{resource}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
resource: @activity.subject_params["resource_title"], resource: @activity.subject_params["resource_title"],
old_resource_title: @activity.subject_params["old_resource_title"] old_resource_title: @activity.subject_params["old_resource_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_moved -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} moved the folder %{resource}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_moved -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} moved the folder %{resource}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
resource: @activity.subject_params["resource_title"] resource: @activity.subject_params["resource_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} moved the resource %{resource}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% else %><%= dgettext("activity", "%{profile} moved the resource %{resource}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
resource: @activity.subject_params["resource_title"] resource: @activity.subject_params["resource_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_deleted -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} deleted the folder %{resource}.", <%= Routes.page_url(Mobilizon.Web.Endpoint, :resource, @activity.subject_params["resource_uuid"]) |> URI.decode() %><% end %><% :resource_deleted -> %><%= if @activity.subject_params["is_folder"] do %><%= dgettext("activity", "%{profile} deleted the folder %{resource}.",
%{ %{
profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author), profile: Mobilizon.Actors.Actor.display_name_and_username(@activity.author),
resource: @activity.subject_params["resource_title"] resource: @activity.subject_params["resource_title"]

View file

@ -47,7 +47,7 @@
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;"> <td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= page_url(Mobilizon.Web.Endpoint, :participation_email_confirmation, @participant.metadata.confirmation_token) %>" target="_blank" style="font-size: 20px; font-family: 'Roboto', Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :participation_email_confirmation, @participant.metadata.confirmation_token) %>" target="_blank" style="font-size: 20px; font-family: 'Roboto', Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= gettext "Confirm my e-mail address" %> <%= gettext "Confirm my e-mail address" %>
</a></td> </a></td>
</tr> </tr>

View file

@ -2,5 +2,5 @@
== ==
<%= gettext "Hi there! You just registered to join this event: « %{title} ». Please confirm the e-mail address you provided:", title: @participant.event.title %> <%= gettext "Hi there! You just registered to join this event: « %{title} ». Please confirm the e-mail address you provided:", title: @participant.event.title %>
<%= gettext "If you didn't trigger this email, you may safely ignore it." %> <%= gettext "If you didn't trigger this email, you may safely ignore it." %>
<%= page_url(Mobilizon.Web.Endpoint, :participation_email_confirmation, @participant.metadata.confirmation_token) %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :participation_email_confirmation, @participant.metadata.confirmation_token) %>
<%= ngettext "Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.", "Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.", 1 %> <%= ngettext "Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.", "Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.", 1 %>

View file

@ -47,7 +47,7 @@
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;"> <td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @participant.event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @participant.event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= gettext "Go to event page" %> <%= gettext "Go to event page" %>
</a></td> </a></td>
</tr> </tr>

View file

@ -2,5 +2,5 @@
== ==
<%= gettext "Get ready for %{title}", title: @participant.event.title %> <%= gettext "Get ready for %{title}", title: @participant.event.title %>
<%= gettext "Go to event page" %> <%= gettext "Go to event page" %>
<%= gettext "View the event on: %{link}", link: page_url(Mobilizon.Web.Endpoint, :event, @participant.event.uuid) %> <%= gettext "View the event on: %{link}", link: Routes.page_url(Mobilizon.Web.Endpoint, :event, @participant.event.uuid) %>
<%= gettext "If you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button." %> <%= gettext "If you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button." %>

View file

@ -42,7 +42,7 @@
%{ %{
profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>", profile: "<b>#{Mobilizon.Actors.Actor.display_name_and_username(@activity.author)}</b>",
event: "<a href=\"#{ event: "<a href=\"#{
page_url( Routes.page_url(
Mobilizon.Web.Endpoint, Mobilizon.Web.Endpoint,
:event, :event,
@activity.subject_params["event_uuid"] @activity.subject_params["event_uuid"]
@ -63,7 +63,7 @@
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;"> <td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= gettext "Visit event page" %> <%= gettext "Visit event page" %>
</a></td> </a></td>
</tr> </tr>

View file

@ -8,4 +8,4 @@
event: @activity.subject_params["event_title"] event: @activity.subject_params["event_title"]
} }
) %> ) %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @activity.subject_params["event_uuid"]) |> URI.decode() %>

View file

@ -47,7 +47,7 @@
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E">
<a href="<%= page_url(Mobilizon.Web.Endpoint, :user_email_validation, @token) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :user_email_validation, @token) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= gettext "Verify your email address" %> <%= gettext "Verify your email address" %>
</a> </a>
</td> </td>

View file

@ -1,5 +1,5 @@
<%= gettext "Confirm new email" %> <%= gettext "Confirm new email" %>
== ==
<%= gettext "Hi there! It seems like you wanted to change the email address linked to your account on <b>%{instance}</b>. If you still wish to do so, please click the button below to confirm the change. You will then be able to log in to %{instance} with this new email address.", %{instance: @instance_name} %> <%= gettext "Hi there! It seems like you wanted to change the email address linked to your account on <b>%{instance}</b>. If you still wish to do so, please click the button below to confirm the change. You will then be able to log in to %{instance} with this new email address.", %{instance: @instance_name} %>
<%= page_url(Mobilizon.Web.Endpoint, :user_email_validation, @token) %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :user_email_validation, @token) %>
<%= gettext "If you didn't trigger the change yourself, please ignore this message." %> <%= gettext "If you didn't trigger the change yourself, please ignore this message." %>

View file

@ -64,7 +64,7 @@
<tr> <tr>
<%= if hd(group_activities).group.avatar do %> <%= if hd(group_activities).group.avatar do %>
<td width="85"> <td width="85">
<a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;"> <a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;">
<img width="80" src="<%= hd(group_activities).group.avatar.url %>" style="width: 80px;max-height: 100px;" style="margin:0; padding:0; border:none; display:block;" border="0" alt="" /> <img width="80" src="<%= hd(group_activities).group.avatar.url %>" style="width: 80px;max-height: 100px;" style="margin:0; padding:0; border:none; display:block;" border="0" alt="" />
</a> </a>
</td> </td>
@ -73,7 +73,7 @@
<table width="" cellpadding="0" cellspacing="0" border="0" style="max-width: 400px;width: 100%;" align="left"> <table width="" cellpadding="0" cellspacing="0" border="0" style="max-width: 400px;width: 100%;" align="left">
<tr> <tr>
<td align="left"> <td align="left">
<a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;color: #474467;font-family: 'Roboto', Helvetica, Arial, sans-serif;font-size: 18px;font-weight: bold;line-height: 25px;"> <a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;color: #474467;font-family: 'Roboto', Helvetica, Arial, sans-serif;font-size: 18px;font-weight: bold;line-height: 25px;">
<%= hd(group_activities).group.name || "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)}" %> <%= hd(group_activities).group.name || "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)}" %>
</a> </a>
</td> </td>
@ -81,7 +81,7 @@
<%= if hd(group_activities).group.name do %> <%= if hd(group_activities).group.name do %>
<tr> <tr>
<td align="left"> <td align="left">
<a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;display: block;color: #7a7a7a;font-family: 'Roboto', Helvetica, Arial, sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;"> <a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>" target="_blank" style="text-decoration: none;display: block;color: #7a7a7a;font-family: 'Roboto', Helvetica, Arial, sans-serif;font-size: 16px;font-weight: 400;line-height: 25px;">
@<%= Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group) %> @<%= Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group) %>
</a> </a>
</td> </td>
@ -131,7 +131,7 @@
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E">
<a href="<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>/timeline" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>/timeline" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= dngettext "activity", "View one more activity", "View %{count} more activities", length(group_activities) - 5, %{count: length(group_activities) - 5} %> <%= dngettext "activity", "View one more activity", "View %{count} more activities", length(group_activities) - 5, %{count: length(group_activities) - 5} %>
</a> </a>
</td> </td>

View file

@ -21,7 +21,7 @@
<% end %> <% end %>
<%= if length(group_activities) > 5 do %> <%= if length(group_activities) > 5 do %>
<%= dngettext "activity", "View one more activity", "View %{count} more activities", length(group_activities) - 5, %{count: length(group_activities) - 5} %> <%= dngettext "activity", "View one more activity", "View %{count} more activities", length(group_activities) - 5, %{count: length(group_activities) - 5} %>
<%= page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>/timeline <%= Routes.page_url(Mobilizon.Web.Endpoint, :actor, Mobilizon.Actors.Actor.preferred_username_and_domain(hd(group_activities).group)) |> URI.decode() %>/timeline
<% end %> <% end %>
<% end %> <% end %>
<%= dgettext("activity", "Don't want to receive activity notifications? You may change frequency or disable them in your settings.") %> <%= dgettext("activity", "Don't want to receive activity notifications? You may change frequency or disable them in your settings.") %>

View file

@ -54,7 +54,7 @@
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;"> <td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= gettext "Visit event page" %> <%= gettext "Visit event page" %>
</a></td> </a></td>
</tr> </tr>

View file

@ -6,6 +6,6 @@
<%= gettext "Good news: one of the event organizers just approved your request. Update your calendar, because you're on the guest list now!" %> <%= gettext "Good news: one of the event organizers just approved your request. Update your calendar, because you're on the guest list now!" %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>
<%= gettext "Would you wish to update or cancel your attendance, simply access the event page through the link above and click on the Attending button." %> <%= gettext "Would you wish to update or cancel your attendance, simply access the event page through the link above and click on the Attending button." %>

View file

@ -54,7 +54,7 @@
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;"> <td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= gettext "Visit event page" %> <%= gettext "Visit event page" %>
</a></td> </a></td>
</tr> </tr>

View file

@ -2,5 +2,5 @@
== ==
<%= gettext "You recently requested to attend %{title}.", title: @event.title %> <%= gettext "You recently requested to attend %{title}.", title: @event.title %>
<%= gettext "You have confirmed your participation. Update your calendar, because you're on the guest list now!" %> <%= gettext "You have confirmed your participation. Update your calendar, because you're on the guest list now!" %>
<%= page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>
<%= gettext "Would you wish to update or cancel your attendance, simply access the event page through the link above and click on the Attending button." %> <%= gettext "Would you wish to update or cancel your attendance, simply access the event page through the link above and click on the Attending button." %>

View file

@ -117,7 +117,7 @@
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;"> <td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E"><a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= gettext "Visit the updated event page" %> <%= gettext "Visit the updated event page" %>
</a></td> </a></td>
</tr> </tr>

View file

@ -20,5 +20,5 @@
<%= if MapSet.member?(@changes, :ends_on) && !is_nil(@event.ends_on) do %> <%= if MapSet.member?(@changes, :ends_on) && !is_nil(@event.ends_on) do %>
<%= gettext "End %{ends_on}", ends_on: @event.ends_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> <%= gettext "End %{ends_on}", ends_on: @event.ends_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %>
<% end %> <% end %>
<%= gettext "Visit the updated event page: %{link}", link: page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %> <%= gettext "Visit the updated event page: %{link}", link: Routes.page_url(Mobilizon.Web.Endpoint, :event, @event.uuid) %>
<%= ngettext "Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.", "Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.", 1 %> <%= ngettext "Would you wish to cancel your attendance, visit the event page through the link above and click the « Attending » button.", "Would you wish to cancel your attendance to one or several events, visit the event pages through the links above and click the « Attending » button.", 1 %>

View file

@ -55,7 +55,7 @@
<table border="0" cellspacing="0" cellpadding="0"> <table border="0" cellspacing="0" cellpadding="0">
<tr> <tr>
<td align="center" style="border-radius: 3px;" bgcolor="#3C376E"> <td align="center" style="border-radius: 3px;" bgcolor="#3C376E">
<a href="<%= page_url(Mobilizon.Web.Endpoint, :my_groups) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;"> <a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :my_groups) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;">
<%= gettext "See my groups" %> <%= gettext "See my groups" %>
</a> </a>
</td> </td>

View file

@ -3,4 +3,4 @@
<%= gettext "%{inviter} just invited you to join their group %{group}", group: @group.name, inviter: @inviter.name %> <%= gettext "%{inviter} just invited you to join their group %{group}", group: @group.name, inviter: @inviter.name %>
<%= @group.url %> <%= @group.url %>
<%= gettext "To accept this invitation, head over to your groups." %> <%= gettext "To accept this invitation, head over to your groups." %>
<%= page_url(Mobilizon.Web.Endpoint, :my_groups) %> <%= Routes.page_url(Mobilizon.Web.Endpoint, :my_groups) %>

View file

@ -48,7 +48,7 @@
<strong> <strong>
<%= participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> <%= participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %>
</strong> </strong>
<a href="<%= page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %>" target="_blank"> <a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, participation.event.uuid) %>" target="_blank">
<%= participation.event.title %> <%= participation.event.title %>
</a> </a>
</li> </li>
@ -58,7 +58,7 @@
<strong> <strong>
<%= @participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %> <%= @participation.event.begins_on |> datetime_tz_convert(@timezone) |> datetime_to_string(@locale) %>
</strong> </strong>
<a href="<%= page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %>" target="_blank"> <a href="<%= Routes.page_url(Mobilizon.Web.Endpoint, :event, @participation.event.uuid) %>" target="_blank">
<%= @participation.event.title %> <%= @participation.event.title %>
</a> </a>
<% end %> <% end %>

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