Add anonymous and remote participations
This commit is contained in:
@@ -426,19 +426,25 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
# TODO Refactor me for federation
|
||||
with {:maximum_attendee_capacity, true} <-
|
||||
{:maximum_attendee_capacity, check_attendee_capacity(event)},
|
||||
role <-
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.create_participant(%{
|
||||
role: :not_approved,
|
||||
role: role,
|
||||
event_id: event.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url)
|
||||
url: Map.get(additional, :url),
|
||||
metadata: Map.get(additional, :metadata)
|
||||
}),
|
||||
join_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(participant),
|
||||
{:ok, activity} <- create_activity(Map.merge(join_data, audience), local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
if event.local && Mobilizon.Events.get_default_participant_role(event) === :participant do
|
||||
if event.local && Mobilizon.Events.get_default_participant_role(event) === :participant &&
|
||||
role == :participant do
|
||||
accept(
|
||||
:join,
|
||||
participant,
|
||||
@@ -464,19 +470,24 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
def leave(object, actor, local \\ true)
|
||||
def leave(object, actor, local \\ true, additional \\ %{})
|
||||
|
||||
# TODO: If we want to use this for exclusion we need to have an extra field
|
||||
# for the actor that excluded the participant
|
||||
def leave(
|
||||
%Event{id: event_id, url: event_url} = _event,
|
||||
%Actor{id: actor_id, url: actor_url} = _actor,
|
||||
local
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
with {:only_organizer, false} <-
|
||||
{:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.get_participant(event_id, actor_id),
|
||||
Mobilizon.Events.get_participant(
|
||||
event_id,
|
||||
actor_id,
|
||||
Map.get(additional, :metadata, %{})
|
||||
),
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Events.delete_participant(participant),
|
||||
leave_data <- %{
|
||||
@@ -604,6 +615,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
|
||||
digest = Signature.build_digest(json)
|
||||
date = Signature.generate_date_header()
|
||||
|
||||
# request_target = Signature.generate_request_target("POST", path)
|
||||
|
||||
signature =
|
||||
@@ -823,7 +835,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
@spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any()
|
||||
defp reject_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{approved: false, role: :rejected}),
|
||||
Events.update_participant(participant, %{role: :rejected}),
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
),
|
||||
@@ -908,6 +920,18 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
args
|
||||
end
|
||||
|
||||
# Check that we can only allow anonymous participation if our instance allows it
|
||||
{_, options} =
|
||||
Map.get_and_update(
|
||||
Map.get(args, :options, %{anonymous_participation: false}),
|
||||
:anonymous_participation,
|
||||
fn value ->
|
||||
{value, value && Mobilizon.Config.anonymous_participation?()}
|
||||
end
|
||||
)
|
||||
|
||||
args = Map.put(args, :options, options)
|
||||
|
||||
Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
|
||||
end
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
|
||||
alias Mobilizon.GraphQL.API.Follows
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
|
||||
require Logger
|
||||
|
||||
def init do
|
||||
@@ -30,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
||||
@spec get_actor() :: Actor.t() | {:error, Ecto.Changeset.t()}
|
||||
def get_actor do
|
||||
with {:ok, %Actor{} = actor} <-
|
||||
Actors.get_or_create_instance_actor_by_url("#{Endpoint.url()}/relay") do
|
||||
Actors.get_or_create_internal_actor("relay") do
|
||||
actor
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,6 +73,10 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
"joinMode" => %{
|
||||
"@id" => "mz:joinMode",
|
||||
"@type" => "mz:joinModeType"
|
||||
},
|
||||
"anonymousParticipationEnabled" => %{
|
||||
"@id" => "mz:anonymousParticipationEnabled",
|
||||
"@type" => "sc:Boolean"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -121,6 +121,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,
|
||||
"repliesModerationOption" => event.options.comment_moderation,
|
||||
"commentsEnabled" => event.options.comment_moderation == :allow_all,
|
||||
"anonymousParticipationEnabled" => event.options.anonymous_participation,
|
||||
# "draft" => event.draft,
|
||||
"ical:status" => event.status |> to_string |> String.upcase(),
|
||||
"id" => event.url,
|
||||
@@ -142,6 +143,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
defp get_options(object) do
|
||||
%{
|
||||
maximum_attendee_capacity: object["maximumAttendeeCapacity"],
|
||||
anonymous_participation: object["anonymousParticipationEnabled"],
|
||||
comment_moderation:
|
||||
Map.get(
|
||||
object,
|
||||
|
||||
@@ -10,11 +10,9 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
alias Mobilizon.Federation.WebFinger.XmlBuilder
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
|
||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||
require Jason
|
||||
require Logger
|
||||
|
||||
@@ -69,6 +67,10 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
"rel" => "https://webfinger.net/rel/profile-page/",
|
||||
"type" => "text/html",
|
||||
"href" => actor.url
|
||||
},
|
||||
%{
|
||||
"rel" => "http://ostatus.org/schema/1.0/subscribe",
|
||||
"template" => "#{Routes.page_url(Endpoint, :interact, uri: nil)}{uri}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,22 +4,26 @@ defmodule Mobilizon.GraphQL.API.Participations do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Web.Email.Participation
|
||||
|
||||
@spec join(Event.t(), Actor.t()) :: {:ok, Participant.t()}
|
||||
def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor) do
|
||||
with {:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
|
||||
{:ok, activity, participant} <- ActivityPub.join(event, actor, true) do
|
||||
@spec join(Event.t(), Actor.t(), map()) :: {:ok, Activity.t(), Participant.t()}
|
||||
def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor, args \\ %{}) do
|
||||
with {:error, :participant_not_found} <-
|
||||
Mobilizon.Events.get_participant(event_id, actor_id, args),
|
||||
{:ok, activity, participant} <-
|
||||
ActivityPub.join(event, actor, Map.get(args, :local, true), %{metadata: args}) do
|
||||
{:ok, activity, participant}
|
||||
end
|
||||
end
|
||||
|
||||
def leave(%Event{} = event, %Actor{} = actor) do
|
||||
with {:ok, activity, participant} <- ActivityPub.leave(event, actor, true) do
|
||||
@spec leave(Event.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()}
|
||||
def leave(%Event{} = event, %Actor{} = actor, args \\ %{}) do
|
||||
with {:ok, activity, participant} <-
|
||||
ActivityPub.leave(event, actor, Map.get(args, :local, true), %{metadata: args}) do
|
||||
{:ok, activity, participant}
|
||||
end
|
||||
end
|
||||
@@ -27,14 +31,23 @@ defmodule Mobilizon.GraphQL.API.Participations do
|
||||
@doc """
|
||||
Update participation status
|
||||
"""
|
||||
def update(%Participant{} = participation, %Actor{} = moderator, :participant) do
|
||||
accept(participation, moderator)
|
||||
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
|
||||
def update(%Participant{} = participation, %Actor{} = moderator, :participant),
|
||||
do: accept(participation, moderator)
|
||||
|
||||
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
|
||||
def update(%Participant{} = participation, %Actor{} = _moderator, :not_approved) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participation, %{role: :not_approved}) do
|
||||
{:ok, nil, participant}
|
||||
end
|
||||
end
|
||||
|
||||
def update(%Participant{} = participation, %Actor{} = moderator, :rejected) do
|
||||
reject(participation, moderator)
|
||||
end
|
||||
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
|
||||
def update(%Participant{} = participation, %Actor{} = moderator, :rejected),
|
||||
do: reject(participation, moderator)
|
||||
|
||||
@spec accept(Participant.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()}
|
||||
defp accept(
|
||||
%Participant{} = participation,
|
||||
%Actor{} = moderator
|
||||
@@ -51,6 +64,7 @@ defmodule Mobilizon.GraphQL.API.Participations do
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject(Participant.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()}
|
||||
defp reject(
|
||||
%Participant{} = participation,
|
||||
%Actor{} = moderator
|
||||
|
||||
@@ -5,18 +5,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.{Actors, Admin, Config, Events}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Admin.ActionLog
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Admin.{ActionLog, Setting}
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
alias Mobilizon.Service.Statistics
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
|
||||
def list_action_logs(
|
||||
_parent,
|
||||
%{page: page, limit: limit},
|
||||
@@ -132,6 +132,43 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
{:error, "You need to be logged-in and an administrator to access dashboard statistics"}
|
||||
end
|
||||
|
||||
def get_settings(_parent, _args, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
{:ok,
|
||||
%{
|
||||
instance_description: Config.instance_description(),
|
||||
instance_name: Config.instance_name(),
|
||||
registrations_open: Config.instance_registrations_open?(),
|
||||
instance_terms: Config.instance_terms(),
|
||||
instance_terms_type: Config.instance_terms_type(),
|
||||
instance_terms_url: Config.instance_terms_url()
|
||||
}}
|
||||
end
|
||||
|
||||
def get_settings(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in and an administrator to access admin settings"}
|
||||
end
|
||||
|
||||
def save_settings(_parent, args, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
when is_admin(role) do
|
||||
with {:ok, res} <- Admin.save_settings("instance", args) do
|
||||
res =
|
||||
res |> Enum.map(fn {key, %Setting{value: value}} -> {key, value} end) |> Enum.into(%{})
|
||||
|
||||
Config.clear_config_cache()
|
||||
|
||||
{:ok, res}
|
||||
end
|
||||
end
|
||||
|
||||
def save_settings(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in and an administrator to save admin settings"}
|
||||
end
|
||||
|
||||
def list_relay_followers(
|
||||
_parent,
|
||||
%{page: page, limit: limit},
|
||||
|
||||
@@ -25,25 +25,82 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
name: Config.instance_name(),
|
||||
registrations_open: Config.instance_registrations_open?(),
|
||||
registrations_whitelist: Config.instance_registrations_whitelist?(),
|
||||
demo_mode: Config.instance_demo_mode?(),
|
||||
description: Config.instance_description(),
|
||||
location: location,
|
||||
country_code: country_code,
|
||||
geocoding: %{
|
||||
provider: Config.instance_geocoding_provider(),
|
||||
autocomplete: Config.instance_geocoding_autocomplete()
|
||||
},
|
||||
maps: %{
|
||||
tiles: %{
|
||||
endpoint: Config.instance_maps_tiles_endpoint(),
|
||||
attribution: Config.instance_maps_tiles_attribution()
|
||||
}
|
||||
}
|
||||
}}
|
||||
data = Map.merge(config_cache(), %{location: location, country_code: country_code})
|
||||
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
def terms(_parent, %{locale: locale}, _resolution) do
|
||||
type = Config.instance_terms_type()
|
||||
|
||||
{url, body_html} =
|
||||
case type do
|
||||
"URL" -> {Config.instance_terms_url(), nil}
|
||||
"DEFAULT" -> {nil, Config.generate_terms(locale)}
|
||||
_ -> {nil, Config.instance_terms(locale)}
|
||||
end
|
||||
|
||||
{:ok, %{body_html: body_html, type: type, url: url}}
|
||||
end
|
||||
|
||||
defp config_cache do
|
||||
case Cachex.fetch(:config, "full_config", fn _key ->
|
||||
case build_config_cache() do
|
||||
value when not is_nil(value) -> {:commit, value}
|
||||
err -> {:ignore, err}
|
||||
end
|
||||
end) do
|
||||
{status, value} when status in [:ok, :commit] -> value
|
||||
_err -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp build_config_cache do
|
||||
%{
|
||||
name: Config.instance_name(),
|
||||
registrations_open: Config.instance_registrations_open?(),
|
||||
registrations_whitelist: Config.instance_registrations_whitelist?(),
|
||||
demo_mode: Config.instance_demo_mode?(),
|
||||
description: Config.instance_description(),
|
||||
anonymous: %{
|
||||
participation: %{
|
||||
allowed: Config.anonymous_participation?(),
|
||||
validation: %{
|
||||
email: %{
|
||||
enabled: Config.anonymous_participation_email_required?(),
|
||||
confirmation_required:
|
||||
Config.anonymous_event_creation_email_confirmation_required?()
|
||||
},
|
||||
captcha: %{
|
||||
enabled: Config.anonymous_event_creation_email_captcha_required?()
|
||||
}
|
||||
}
|
||||
},
|
||||
event_creation: %{
|
||||
allowed: Config.anonymous_event_creation?(),
|
||||
validation: %{
|
||||
email: %{
|
||||
enabled: Config.anonymous_event_creation_email_required?(),
|
||||
confirmation_required:
|
||||
Config.anonymous_event_creation_email_confirmation_required?()
|
||||
},
|
||||
captcha: %{
|
||||
enabled: Config.anonymous_event_creation_email_captcha_required?()
|
||||
}
|
||||
}
|
||||
},
|
||||
actor_id: Config.anonymous_actor_id()
|
||||
},
|
||||
geocoding: %{
|
||||
provider: Config.instance_geocoding_provider(),
|
||||
autocomplete: Config.instance_geocoding_autocomplete()
|
||||
},
|
||||
maps: %{
|
||||
tiles: %{
|
||||
endpoint: Config.instance_maps_tiles_endpoint(),
|
||||
attribution: Config.instance_maps_tiles_attribution()
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
|
||||
alias Mobilizon.{Actors, Admin, Events}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.{Event, EventParticipantStats, Participant}
|
||||
alias Mobilizon.Events.{Event, EventParticipantStats}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.GraphQL.API
|
||||
@@ -19,7 +19,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
|
||||
def list_events(_parent, %{page: page, limit: limit}, _resolution)
|
||||
when limit < @event_max_limit do
|
||||
{:ok, Mobilizon.Events.list_events(page, limit)}
|
||||
{:ok, Events.list_events(page, limit)}
|
||||
end
|
||||
|
||||
def list_events(_parent, %{page: _page, limit: _limit}, _resolution) do
|
||||
@@ -31,7 +31,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
%{uuid: uuid},
|
||||
%{context: %{current_user: %User{id: user_id}}} = _resolution
|
||||
) do
|
||||
case {:has_event, Mobilizon.Events.get_own_event_by_uuid_with_preload(uuid, user_id)} do
|
||||
case {:has_event, Events.get_own_event_by_uuid_with_preload(uuid, user_id)} do
|
||||
{:has_event, %Event{} = event} ->
|
||||
{:ok, Map.put(event, :organizer_actor, Person.proxify_pictures(event.organizer_actor))}
|
||||
|
||||
@@ -45,7 +45,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
end
|
||||
|
||||
def find_event(parent, %{uuid: uuid} = args, resolution) do
|
||||
case {:has_event, Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid)} do
|
||||
case {:has_event, Events.get_public_event_by_uuid_with_preload(uuid)} do
|
||||
{:has_event, %Event{} = event} ->
|
||||
{:ok, Map.put(event, :organizer_actor, Person.proxify_pictures(event.organizer_actor))}
|
||||
|
||||
@@ -65,7 +65,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
|
||||
# Check that moderator has right
|
||||
{:actor_approve_permission, true} <-
|
||||
{:actor_approve_permission, Mobilizon.Events.moderator_for_event?(event_id, actor_id)} do
|
||||
{:actor_approve_permission, Events.moderator_for_event?(event_id, actor_id)} do
|
||||
roles =
|
||||
case roles do
|
||||
"" ->
|
||||
@@ -78,7 +78,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
|> Enum.map(&String.to_existing_atom/1)
|
||||
end
|
||||
|
||||
{:ok, Mobilizon.Events.list_participants_for_event(event_id, roles, page, limit)}
|
||||
{:ok, Events.list_participants_for_event(event_id, roles, page, limit)}
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Moderator Actor ID is not owned by authenticated user"}
|
||||
@@ -142,118 +142,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
|
||||
defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end)
|
||||
|
||||
@doc """
|
||||
Join an event for an actor
|
||||
"""
|
||||
def actor_join_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id},
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
{:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
|
||||
{:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
|
||||
{:ok, _activity, participant} <- API.Participations.join(event, actor),
|
||||
participant <-
|
||||
participant
|
||||
|> Map.put(:event, event)
|
||||
|> Map.put(:actor, Person.proxify_pictures(actor)) do
|
||||
{:ok, participant}
|
||||
else
|
||||
{:maximum_attendee_capacity, _} ->
|
||||
{:error, "The event has already reached its maximum capacity"}
|
||||
|
||||
{:has_event, _} ->
|
||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:error, :event_not_found} ->
|
||||
{:error, "Event id not found"}
|
||||
|
||||
{:ok, %Participant{}} ->
|
||||
{:error, "You are already a participant of this event"}
|
||||
end
|
||||
end
|
||||
|
||||
def actor_join_event(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to join an event"}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Leave an event for an actor
|
||||
"""
|
||||
def actor_leave_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id},
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
{:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
|
||||
{:ok, _activity, _participant} <- API.Participations.leave(event, actor) do
|
||||
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
|
||||
else
|
||||
{:has_event, _} ->
|
||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:only_organizer, true} ->
|
||||
{:error, "You can't leave event because you're the only event creator participant"}
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, "Participant not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def actor_leave_event(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to leave an event"}
|
||||
end
|
||||
|
||||
def update_participation(
|
||||
_parent,
|
||||
%{id: participation_id, moderator_actor_id: moderator_actor_id, role: new_role},
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
# Check that moderator provided is rightly authenticated
|
||||
with {:is_owned, moderator_actor} <- User.owns_actor(user, moderator_actor_id),
|
||||
# Check that participation already exists
|
||||
{:has_participation, %Participant{role: old_role} = participation} <-
|
||||
{:has_participation, Mobilizon.Events.get_participant(participation_id)},
|
||||
{:same_role, false} <- {:same_role, new_role == old_role},
|
||||
# Check that moderator has right
|
||||
{:actor_approve_permission, true} <-
|
||||
{:actor_approve_permission,
|
||||
Mobilizon.Events.moderator_for_event?(participation.event.id, moderator_actor_id)},
|
||||
{:ok, _activity, participation} <-
|
||||
API.Participations.update(participation, moderator_actor, new_role) do
|
||||
{:ok, participation}
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Moderator Actor ID is not owned by authenticated user"}
|
||||
|
||||
{:has_participation, %Participant{role: role, id: id}} ->
|
||||
{:error,
|
||||
"Participant #{id} can't be approved since it's already a participant (with role #{role})"}
|
||||
|
||||
{:has_participation, nil} ->
|
||||
{:error, "Participant not found"}
|
||||
|
||||
{:actor_approve_permission, _} ->
|
||||
{:error, "Provided moderator actor ID doesn't have permission on this event"}
|
||||
|
||||
{:same_role, true} ->
|
||||
{:error, "Participant already has role #{new_role}"}
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, "Participant not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an event
|
||||
"""
|
||||
|
||||
262
lib/graphql/resolvers/participant.ex
Normal file
262
lib/graphql/resolvers/participant.ex
Normal file
@@ -0,0 +1,262 @@
|
||||
defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
||||
@moduledoc """
|
||||
Handles the participation-related GraphQL calls.
|
||||
"""
|
||||
alias Mobilizon.{Actors, Config, Crypto, Events}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.GraphQL.API.Participations
|
||||
alias Mobilizon.GraphQL.Resolvers.Person
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Web.Email
|
||||
alias Mobilizon.Web.Email.Checker
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Join an event for an regular actor
|
||||
"""
|
||||
def actor_join_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id},
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
) do
|
||||
case User.owns_actor(user, actor_id) do
|
||||
{:is_owned, %Actor{} = actor} ->
|
||||
do_actor_join_event(actor, event_id)
|
||||
|
||||
_ ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Join an event for an anonymous actor
|
||||
"""
|
||||
def actor_join_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id} = args,
|
||||
_resolution
|
||||
) do
|
||||
with {:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
|
||||
{:anonymous_participation_enabled, true} <-
|
||||
{:anonymous_participation_enabled,
|
||||
event.local == true && Config.anonymous_participation?() &&
|
||||
event.options.anonymous_participation == true},
|
||||
{:anonymous_actor_id, true} <-
|
||||
{:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id},
|
||||
{:email_required, true} <-
|
||||
{:email_required,
|
||||
Config.anonymous_participation_email_required?() &&
|
||||
args |> Map.get(:email) |> valid_email?()},
|
||||
{:confirmation_token, {confirmation_token, role}} <-
|
||||
{:confirmation_token,
|
||||
if(Config.anonymous_participation_email_confirmation_required?(),
|
||||
do: {Crypto.random_string(30), :not_confirmed},
|
||||
else: {nil, :participant}
|
||||
)},
|
||||
# We only federate if the participation is not to be confirmed later
|
||||
args <-
|
||||
args
|
||||
|> Map.put(:confirmation_token, confirmation_token)
|
||||
|> Map.put(:cancellation_token, Crypto.random_string(30))
|
||||
|> Map.put(:role, role)
|
||||
|> Map.put(:local, role == :participant),
|
||||
{:actor_not_found, %Actor{} = actor} <-
|
||||
{:actor_not_found, Actors.get_actor_with_preload(actor_id)},
|
||||
{:ok, %Participant{} = participant} <- do_actor_join_event(actor, event_id, args) do
|
||||
if Config.anonymous_participation_email_required?() &&
|
||||
Config.anonymous_participation_email_confirmation_required?() do
|
||||
args
|
||||
|> Map.get(:email)
|
||||
|> Email.Participation.anonymous_participation_confirmation(participant)
|
||||
|> Email.Mailer.deliver_later()
|
||||
end
|
||||
|
||||
{:ok, participant}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
{:has_event, _} ->
|
||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||
|
||||
{:anonymous_participation_enabled, false} ->
|
||||
{:error, "Anonymous participation is not enabled"}
|
||||
|
||||
{:anonymous_actor_id, false} ->
|
||||
{:error, "Actor ID provided is not the anonymous actor one"}
|
||||
|
||||
{:email_required, _} ->
|
||||
{:error, "A valid email is required by your instance"}
|
||||
|
||||
{:actor_not_found, _} ->
|
||||
Logger.error(
|
||||
"The actor ID \"#{actor_id}\" provided by configuration doesn't match any actor in database"
|
||||
)
|
||||
|
||||
{:error, "Internal Error"}
|
||||
end
|
||||
end
|
||||
|
||||
def actor_join_event(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to join an event"}
|
||||
end
|
||||
|
||||
@spec do_actor_join_event(Actor.t(), integer | String.t(), map()) ::
|
||||
{:ok, Participant.t()} | {:error, String.t()}
|
||||
defp do_actor_join_event(actor, event_id, args \\ %{}) do
|
||||
with {:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Events.get_event_with_preload(event_id)},
|
||||
{:ok, _activity, participant} <- Participations.join(event, actor, args),
|
||||
%Participant{} = participant <-
|
||||
participant
|
||||
|> Map.put(:event, event)
|
||||
|> Map.put(:actor, Person.proxify_pictures(actor)) do
|
||||
{:ok, participant}
|
||||
else
|
||||
{:maximum_attendee_capacity, _} ->
|
||||
{:error, "The event has already reached its maximum capacity"}
|
||||
|
||||
{:has_event, _} ->
|
||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||
|
||||
{:error, :event_not_found} ->
|
||||
{:error, "Event id not found"}
|
||||
|
||||
{:ok, %Participant{}} ->
|
||||
{:error, "You are already a participant of this event"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Leave an event for an actor
|
||||
"""
|
||||
def actor_leave_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id, token: token},
|
||||
_resolution
|
||||
) do
|
||||
with {:anonymous_participation_enabled, true} <-
|
||||
{:anonymous_participation_enabled, Config.anonymous_participation?()},
|
||||
{:anonymous_actor_id, true} <-
|
||||
{:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id},
|
||||
{:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
|
||||
%Actor{} = actor <- Actors.get_actor_with_preload(actor_id),
|
||||
{:ok, _activity, %Participant{id: participant_id} = _participant} <-
|
||||
Participations.leave(event, actor, %{local: false, cancellation_token: token}) do
|
||||
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}, id: participant_id}}
|
||||
else
|
||||
{:has_event, _} ->
|
||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:only_organizer, true} ->
|
||||
{:error, "You can't leave event because you're the only event creator participant"}
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, "Participant not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def actor_leave_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id},
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
{:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Events.get_event_with_preload(event_id)},
|
||||
{:ok, _activity, _participant} <- Participations.leave(event, actor) do
|
||||
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
|
||||
else
|
||||
{:has_event, _} ->
|
||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:only_organizer, true} ->
|
||||
{:error, "You can't leave event because you're the only event creator participant"}
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, "Participant not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def actor_leave_event(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to leave an event"}
|
||||
end
|
||||
|
||||
def update_participation(
|
||||
_parent,
|
||||
%{id: participation_id, moderator_actor_id: moderator_actor_id, role: new_role},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
) do
|
||||
# Check that moderator provided is rightly authenticated
|
||||
with {:is_owned, moderator_actor} <- User.owns_actor(user, moderator_actor_id),
|
||||
# Check that participation already exists
|
||||
{:has_participation, %Participant{role: old_role} = participation} <-
|
||||
{:has_participation, Events.get_participant(participation_id)},
|
||||
{:same_role, false} <- {:same_role, new_role == old_role},
|
||||
# Check that moderator has right
|
||||
{:actor_approve_permission, true} <-
|
||||
{:actor_approve_permission,
|
||||
Events.moderator_for_event?(participation.event.id, moderator_actor_id)},
|
||||
{:ok, _activity, participation} <-
|
||||
Participations.update(participation, moderator_actor, new_role) do
|
||||
{:ok, participation}
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Moderator Actor ID is not owned by authenticated user"}
|
||||
|
||||
{:has_participation, nil} ->
|
||||
{:error, "Participant not found"}
|
||||
|
||||
{:actor_approve_permission, _} ->
|
||||
{:error, "Provided moderator actor ID doesn't have permission on this event"}
|
||||
|
||||
{:same_role, true} ->
|
||||
{:error, "Participant already has role #{new_role}"}
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, "Participant not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec confirm_participation_from_token(map(), map(), map()) ::
|
||||
{:ok, Participant.t()} | {:error, String.t()}
|
||||
def confirm_participation_from_token(
|
||||
_parent,
|
||||
%{confirmation_token: confirmation_token},
|
||||
_context
|
||||
) do
|
||||
with {:has_participant,
|
||||
%Participant{actor: actor, role: :not_confirmed, event: event} = participant} <-
|
||||
{:has_participant, Events.get_participant_by_confirmation_token(confirmation_token)},
|
||||
default_role <- Events.get_default_participant_role(event),
|
||||
{:ok, _activity, %Participant{} = participant} <-
|
||||
Participations.update(participant, actor, default_role) do
|
||||
{:ok, participant}
|
||||
else
|
||||
{:has_participant, _} ->
|
||||
{:error, "This token is invalid"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec valid_email?(String.t() | nil) :: boolean
|
||||
defp valid_email?(email) when is_nil(email), do: false
|
||||
|
||||
defp valid_email?(email) when is_bitstring(email) do
|
||||
email
|
||||
|> String.trim()
|
||||
|> Checker.valid?()
|
||||
end
|
||||
end
|
||||
@@ -60,6 +60,21 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
field(:number_of_reports, :integer, description: "The number of current opened reports")
|
||||
end
|
||||
|
||||
object :admin_settings do
|
||||
field(:instance_name, :string)
|
||||
field(:instance_description, :string)
|
||||
field(:instance_terms, :string)
|
||||
field(:instance_terms_type, :instance_terms_type)
|
||||
field(:instance_terms_url, :string)
|
||||
field(:registrations_open, :boolean)
|
||||
end
|
||||
|
||||
enum :instance_terms_type do
|
||||
value(:url, as: "URL")
|
||||
value(:default, as: "DEFAULT")
|
||||
value(:custom, as: "CUSTOM")
|
||||
end
|
||||
|
||||
object :admin_queries do
|
||||
@desc "Get the list of action logs"
|
||||
field :action_logs, type: list_of(:action_log) do
|
||||
@@ -72,6 +87,10 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
resolve(&Admin.get_dashboard/3)
|
||||
end
|
||||
|
||||
field :admin_settings, type: :admin_settings do
|
||||
resolve(&Admin.get_settings/3)
|
||||
end
|
||||
|
||||
field :relay_followers, type: :paginated_follower_list do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
@@ -115,5 +134,16 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
|
||||
resolve(&Admin.reject_subscription/3)
|
||||
end
|
||||
|
||||
field :save_admin_settings, type: :admin_settings do
|
||||
arg(:instance_name, :string)
|
||||
arg(:instance_description, :string)
|
||||
arg(:instance_terms, :string)
|
||||
arg(:instance_terms_type, :instance_terms_type)
|
||||
arg(:instance_terms_url, :string)
|
||||
arg(:registrations_open, :boolean)
|
||||
|
||||
resolve(&Admin.save_settings/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,6 +19,18 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
|
||||
field(:location, :lonlat)
|
||||
field(:geocoding, :geocoding)
|
||||
field(:maps, :maps)
|
||||
field(:anonymous, :anonymous)
|
||||
|
||||
field(:terms, :terms, description: "The instance's terms") do
|
||||
arg(:locale, :string, default_value: "en")
|
||||
resolve(&Config.terms/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :terms do
|
||||
field(:url, :string)
|
||||
field(:type, :instance_terms_type)
|
||||
field(:body_html, :string)
|
||||
end
|
||||
|
||||
object :lonlat do
|
||||
@@ -41,6 +53,50 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
|
||||
field(:attribution, :string)
|
||||
end
|
||||
|
||||
object :anonymous do
|
||||
field(:participation, :anonymous_participation)
|
||||
field(:event_creation, :anonymous_event_creation)
|
||||
field(:actor_id, :id)
|
||||
end
|
||||
|
||||
object :anonymous_participation do
|
||||
field(:allowed, :boolean)
|
||||
field(:validation, :anonymous_participation_validation)
|
||||
end
|
||||
|
||||
object :anonymous_participation_validation do
|
||||
field(:email, :anonymous_participation_validation_email)
|
||||
field(:captcha, :anonymous_participation_validation_captcha)
|
||||
end
|
||||
|
||||
object :anonymous_participation_validation_email do
|
||||
field(:enabled, :boolean)
|
||||
field(:confirmation_required, :boolean)
|
||||
end
|
||||
|
||||
object :anonymous_participation_validation_captcha do
|
||||
field(:enabled, :boolean)
|
||||
end
|
||||
|
||||
object :anonymous_event_creation do
|
||||
field(:allowed, :boolean)
|
||||
field(:validation, :anonymous_event_creation_validation)
|
||||
end
|
||||
|
||||
object :anonymous_event_creation_validation do
|
||||
field(:email, :anonymous_event_creation_validation_email)
|
||||
field(:captcha, :anonymous_event_creation_validation_captcha)
|
||||
end
|
||||
|
||||
object :anonymous_event_creation_validation_email do
|
||||
field(:enabled, :boolean)
|
||||
field(:confirmation_required, :boolean)
|
||||
end
|
||||
|
||||
object :anonymous_event_creation_validation_captcha do
|
||||
field(:enabled, :boolean)
|
||||
end
|
||||
|
||||
object :config_queries do
|
||||
@desc "Get the instance config"
|
||||
field :config, :config do
|
||||
|
||||
@@ -122,6 +122,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
)
|
||||
|
||||
field(:not_approved, :integer, description: "The number of not approved participants")
|
||||
field(:not_confirmed, :integer, description: "The number of not confirmed participants")
|
||||
field(:rejected, :integer, description: "The number of rejected participants")
|
||||
|
||||
field(:participant, :integer,
|
||||
@@ -177,6 +178,10 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
description: "Whether or not to show the number of remaining seats for this event"
|
||||
)
|
||||
|
||||
field(:anonymous_participation, :boolean,
|
||||
description: "Whether or not to allow anonymous participation (if the server allows it)"
|
||||
)
|
||||
|
||||
field(:offers, list_of(:event_offer), description: "The list of offers to show for this event")
|
||||
|
||||
field(:participation_conditions, list_of(:event_participation_condition),
|
||||
@@ -211,6 +216,11 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
description: "Whether or not to show the number of remaining seats for this event"
|
||||
)
|
||||
|
||||
field(:anonymous_participation, :boolean,
|
||||
default_value: false,
|
||||
description: "Whether or not to allow anonymous participation (if the server allows it)"
|
||||
)
|
||||
|
||||
field(:offers, list_of(:event_offer_input),
|
||||
description: "The list of offers to show for this event"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.{Actors, Events}
|
||||
alias Mobilizon.GraphQL.Resolvers.Event
|
||||
alias Mobilizon.GraphQL.Resolvers.Participant
|
||||
|
||||
@desc "Represents a participant to an event"
|
||||
object :participant do
|
||||
@@ -29,10 +29,21 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||
)
|
||||
|
||||
field(:role, :participant_role_enum, description: "The role of this actor at this event")
|
||||
|
||||
field(:metadata, :participant_metadata,
|
||||
description: "The metadata associated to this participant"
|
||||
)
|
||||
end
|
||||
|
||||
object :participant_metadata do
|
||||
field(:cancellation_token, :string,
|
||||
description: "The eventual token to leave an event when user is anonymous"
|
||||
)
|
||||
end
|
||||
|
||||
enum :participant_role_enum do
|
||||
value(:not_approved)
|
||||
value(:not_confirmed)
|
||||
value(:participant)
|
||||
value(:moderator)
|
||||
value(:administrator)
|
||||
@@ -52,16 +63,18 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||
field :join_event, :participant do
|
||||
arg(:event_id, non_null(:id))
|
||||
arg(:actor_id, non_null(:id))
|
||||
arg(:email, :string)
|
||||
|
||||
resolve(&Event.actor_join_event/3)
|
||||
resolve(&Participant.actor_join_event/3)
|
||||
end
|
||||
|
||||
@desc "Leave an event"
|
||||
field :leave_event, :deleted_participant do
|
||||
arg(:event_id, non_null(:id))
|
||||
arg(:actor_id, non_null(:id))
|
||||
arg(:token, :string)
|
||||
|
||||
resolve(&Event.actor_leave_event/3)
|
||||
resolve(&Participant.actor_leave_event/3)
|
||||
end
|
||||
|
||||
@desc "Accept a participation"
|
||||
@@ -70,7 +83,13 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
|
||||
arg(:role, non_null(:participant_role_enum))
|
||||
arg(:moderator_actor_id, non_null(:id))
|
||||
|
||||
resolve(&Event.update_participation/3)
|
||||
resolve(&Participant.update_participation/3)
|
||||
end
|
||||
|
||||
@desc "Confirm a participation"
|
||||
field :confirm_participation, :participant do
|
||||
arg(:confirmation_token, non_null(:string))
|
||||
resolve(&Participant.confirm_participation_from_token/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule Mobilizon do
|
||||
|
||||
@name Mix.Project.config()[:name]
|
||||
@version Mix.Project.config()[:version]
|
||||
@env Mix.env()
|
||||
|
||||
@spec named_version :: String.t()
|
||||
def named_version, do: "#{@name} #{@version}"
|
||||
@@ -34,21 +35,23 @@ defmodule Mobilizon do
|
||||
@spec start(:normal | {:takeover, node} | {:failover, node}, term) ::
|
||||
{:ok, pid} | {:ok, pid, term} | {:error, term}
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# supervisors
|
||||
Storage.Repo,
|
||||
Web.Endpoint,
|
||||
{Absinthe.Subscription, [Web.Endpoint]},
|
||||
{Oban, Application.get_env(:mobilizon, Oban)},
|
||||
# workers
|
||||
Guardian.DB.Token.SweeperServer,
|
||||
ActivityPub.Federator,
|
||||
cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1),
|
||||
cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1),
|
||||
cachex_spec(:statistics, 10, 60, 60),
|
||||
cachex_spec(:activity_pub, 2500, 3, 15),
|
||||
internal_actor()
|
||||
]
|
||||
children =
|
||||
[
|
||||
# supervisors
|
||||
Storage.Repo,
|
||||
Web.Endpoint,
|
||||
{Absinthe.Subscription, [Web.Endpoint]},
|
||||
{Oban, Application.get_env(:mobilizon, Oban)},
|
||||
# workers
|
||||
Guardian.DB.Token.SweeperServer,
|
||||
ActivityPub.Federator,
|
||||
cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1),
|
||||
cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1),
|
||||
cachex_spec(:statistics, 10, 60, 60),
|
||||
cachex_spec(:config, 10, 60, 60),
|
||||
cachex_spec(:activity_pub, 2500, 3, 15)
|
||||
] ++
|
||||
task_children(@env)
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_one, name: Mobilizon.Supervisor)
|
||||
end
|
||||
@@ -92,11 +95,22 @@ defmodule Mobilizon do
|
||||
defp fallback_options(nil), do: []
|
||||
defp fallback_options(fallback), do: [fallback: fallback(default: fallback)]
|
||||
|
||||
defp internal_actor do
|
||||
defp task_children(:test), do: []
|
||||
defp task_children(_), do: [relay_actor(), anonymous_actor()]
|
||||
|
||||
defp relay_actor do
|
||||
%{
|
||||
id: :internal_actor_init,
|
||||
id: :relay_actor_init,
|
||||
start: {Task, :start_link, [&ActivityPub.Relay.init/0]},
|
||||
restart: :temporary
|
||||
}
|
||||
end
|
||||
|
||||
defp anonymous_actor do
|
||||
%{
|
||||
id: :anonymous_actor_init,
|
||||
start: {Task, :start_link, [&Mobilizon.Config.anonymous_actor_id/0]},
|
||||
restart: :temporary
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -97,20 +97,6 @@ defmodule Mobilizon.Actors.Actor do
|
||||
@remote_actor_creation_attrs @remote_actor_creation_required_attrs ++
|
||||
@remote_actor_creation_optional_attrs
|
||||
|
||||
@relay_creation_attrs [
|
||||
:type,
|
||||
:name,
|
||||
:summary,
|
||||
:url,
|
||||
:keys,
|
||||
:preferred_username,
|
||||
:domain,
|
||||
:inbox_url,
|
||||
:followers_url,
|
||||
:following_url,
|
||||
:shared_inbox_url
|
||||
]
|
||||
|
||||
@group_creation_required_attrs [:url, :outbox_url, :inbox_url, :type, :preferred_username]
|
||||
@group_creation_optional_attrs [:shared_inbox_url, :name, :domain, :summary]
|
||||
@group_creation_attrs @group_creation_required_attrs ++ @group_creation_optional_attrs
|
||||
@@ -277,16 +263,6 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|> validate_format(:preferred_username, ~r/[a-z0-9_]+/)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for relay creation.
|
||||
"""
|
||||
@spec relay_creation_changeset(map) :: Ecto.Changeset.t()
|
||||
def relay_creation_changeset(attrs) do
|
||||
relay_creation_attrs = build_relay_creation_attrs(attrs)
|
||||
|
||||
cast(%__MODULE__{}, relay_creation_attrs, @relay_creation_attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for group creation
|
||||
"""
|
||||
@@ -349,6 +325,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|
||||
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
|
||||
|
||||
# Relay has a special URI
|
||||
def build_url("relay", :page, _args),
|
||||
do: Endpoint |> Routes.activity_pub_url(:relay) |> URI.decode()
|
||||
|
||||
def build_url(preferred_username, :page, args) do
|
||||
Endpoint
|
||||
|> Routes.page_url(:actor, preferred_username, args)
|
||||
@@ -362,24 +342,40 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|> URI.decode()
|
||||
end
|
||||
|
||||
@spec build_relay_creation_attrs(map) :: map
|
||||
defp build_relay_creation_attrs(%{url: url, preferred_username: preferred_username}) do
|
||||
%{
|
||||
@spec build_relay_creation_attrs :: Ecto.Changeset.t()
|
||||
def build_relay_creation_attrs do
|
||||
data = %{
|
||||
"name" => Config.get([:instance, :name], "Mobilizon"),
|
||||
"summary" =>
|
||||
Config.get(
|
||||
[:instance, :description],
|
||||
"An internal service actor for this Mobilizon instance"
|
||||
),
|
||||
"url" => url,
|
||||
"keys" => Crypto.generate_rsa_2048_private_key(),
|
||||
"preferred_username" => preferred_username,
|
||||
"preferred_username" => "relay",
|
||||
"domain" => nil,
|
||||
"inbox_url" => "#{Endpoint.url()}/inbox",
|
||||
"followers_url" => "#{url}/followers",
|
||||
"following_url" => "#{url}/following",
|
||||
"shared_inbox_url" => "#{Endpoint.url()}/inbox",
|
||||
"type" => :Application
|
||||
}
|
||||
|
||||
%__MODULE__{}
|
||||
|> Ecto.Changeset.cast(data, @attrs)
|
||||
|> build_urls()
|
||||
|> put_change(:inbox_url, "#{Endpoint.url()}/inbox")
|
||||
end
|
||||
|
||||
@spec build_anonymous_actor_creation_attrs :: Ecto.Changeset.t()
|
||||
def build_anonymous_actor_creation_attrs do
|
||||
data = %{
|
||||
"name" => "Mobilizon Anonymous Actor",
|
||||
"summary" => "A fake person for anonymous participations",
|
||||
"keys" => Crypto.generate_rsa_2048_private_key(),
|
||||
"preferred_username" => "anonymous",
|
||||
"domain" => nil,
|
||||
"type" => :Person
|
||||
}
|
||||
|
||||
%__MODULE__{}
|
||||
|> Ecto.Changeset.cast(data, @attrs)
|
||||
|> build_urls()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -499,17 +499,22 @@ defmodule Mobilizon.Actors do
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@spec get_or_create_instance_actor_by_url(String.t(), String.t()) ::
|
||||
{:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def get_or_create_instance_actor_by_url(url, preferred_username \\ "relay") do
|
||||
case get_actor_by_url(url) do
|
||||
@spec get_or_create_internal_actor(String.t()) :: {:ok, Actor.t()}
|
||||
def get_or_create_internal_actor(username) do
|
||||
case username |> Actor.build_url(:page) |> get_actor_by_url() do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
_ ->
|
||||
%{url: url, preferred_username: preferred_username}
|
||||
|> Actor.relay_creation_changeset()
|
||||
|> Repo.insert()
|
||||
case username do
|
||||
"anonymous" ->
|
||||
Actor.build_anonymous_actor_creation_attrs()
|
||||
|> Repo.insert()
|
||||
|
||||
"relay" ->
|
||||
Actor.build_relay_creation_attrs()
|
||||
|> Repo.insert()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule Mobilizon.Admin do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.{Admin, Users}
|
||||
alias Mobilizon.Admin.ActionLog
|
||||
alias Mobilizon.Admin.Setting
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@@ -18,6 +19,8 @@ defmodule Mobilizon.Admin do
|
||||
"delete"
|
||||
])
|
||||
|
||||
alias Ecto.Multi
|
||||
|
||||
@doc """
|
||||
Creates a action_log.
|
||||
"""
|
||||
@@ -71,4 +74,48 @@ defmodule Mobilizon.Admin do
|
||||
end
|
||||
|
||||
defp stringify_struct(struct), do: struct
|
||||
|
||||
def get_admin_setting_value(group, name, fallback \\ nil)
|
||||
when is_bitstring(group) and is_bitstring(name) do
|
||||
case Repo.get_by(Setting, group: group, name: name) do
|
||||
nil -> fallback
|
||||
%Setting{value: ""} -> fallback
|
||||
%Setting{value: nil} -> fallback
|
||||
%Setting{value: value} -> value
|
||||
end
|
||||
end
|
||||
|
||||
def set_admin_setting_value(group, name, value) do
|
||||
Setting
|
||||
|> Setting.changeset(%{group: group, name: name, value: value})
|
||||
|> Repo.insert(on_conflict: :replace_all, conflict_target: [:group, :name])
|
||||
end
|
||||
|
||||
def save_settings(group, args) do
|
||||
Multi.new()
|
||||
|> do_save_setting(group, args)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
defp do_save_setting(transaction, _group, args) when args == %{}, do: transaction
|
||||
|
||||
defp do_save_setting(transaction, group, args) do
|
||||
key = hd(Map.keys(args))
|
||||
{val, rest} = Map.pop(args, key)
|
||||
|
||||
transaction =
|
||||
Multi.insert(
|
||||
transaction,
|
||||
key,
|
||||
Setting.changeset(%Setting{}, %{
|
||||
group: group,
|
||||
name: Atom.to_string(key),
|
||||
value: to_string(val)
|
||||
}),
|
||||
on_conflict: :replace_all,
|
||||
conflict_target: [:group, :name]
|
||||
)
|
||||
|
||||
do_save_setting(transaction, group, rest)
|
||||
end
|
||||
end
|
||||
|
||||
27
lib/mobilizon/admin/setting.ex
Normal file
27
lib/mobilizon/admin/setting.ex
Normal file
@@ -0,0 +1,27 @@
|
||||
defmodule Mobilizon.Admin.Setting do
|
||||
@moduledoc """
|
||||
A Key-Value settings table for basic settings
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@required_attrs [:group, :name]
|
||||
@optional_attrs [:value]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "admin_settings" do
|
||||
field(:group, :string)
|
||||
field(:name, :string)
|
||||
field(:value, :string)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(setting, attrs) do
|
||||
setting
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:group, name: :admin_settings_group_name_index)
|
||||
end
|
||||
end
|
||||
@@ -3,14 +3,44 @@ defmodule Mobilizon.Config do
|
||||
Configuration wrapper.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
@spec instance_config :: keyword
|
||||
def instance_config, do: Application.get_env(:mobilizon, :instance)
|
||||
|
||||
@spec instance_name :: String.t()
|
||||
def instance_name, do: instance_config()[:name]
|
||||
def instance_name,
|
||||
do:
|
||||
Mobilizon.Admin.get_admin_setting_value(
|
||||
"instance",
|
||||
"instance_name",
|
||||
instance_config()[:name]
|
||||
)
|
||||
|
||||
@spec instance_description :: String.t()
|
||||
def instance_description, do: instance_config()[:description]
|
||||
def instance_description,
|
||||
do:
|
||||
Mobilizon.Admin.get_admin_setting_value(
|
||||
"instance",
|
||||
"instance_description",
|
||||
instance_config()[:description]
|
||||
)
|
||||
|
||||
@spec instance_terms(String.t()) :: String.t()
|
||||
def instance_terms(locale \\ "en") do
|
||||
Mobilizon.Admin.get_admin_setting_value("instance", "instance_terms", generate_terms(locale))
|
||||
end
|
||||
|
||||
@spec instance_terms :: String.t()
|
||||
def instance_terms_type do
|
||||
Mobilizon.Admin.get_admin_setting_value("instance", "instance_terms_type", "DEFAULT")
|
||||
end
|
||||
|
||||
@spec instance_terms :: String.t()
|
||||
def instance_terms_url do
|
||||
Mobilizon.Admin.get_admin_setting_value("instance", "instance_terms_url")
|
||||
end
|
||||
|
||||
@spec instance_version :: String.t()
|
||||
def instance_version, do: Mix.Project.config()[:version]
|
||||
@@ -19,7 +49,15 @@ defmodule Mobilizon.Config do
|
||||
def instance_hostname, do: instance_config()[:hostname]
|
||||
|
||||
@spec instance_registrations_open? :: boolean
|
||||
def instance_registrations_open?, do: to_boolean(instance_config()[:registrations_open])
|
||||
def instance_registrations_open?,
|
||||
do:
|
||||
to_boolean(
|
||||
Mobilizon.Admin.get_admin_setting_value(
|
||||
"instance",
|
||||
"registrations_open",
|
||||
instance_config()[:registrations_open]
|
||||
)
|
||||
)
|
||||
|
||||
@spec instance_registrations_whitelist :: list(String.t())
|
||||
def instance_registrations_whitelist, do: instance_config()[:registration_email_whitelist]
|
||||
@@ -58,6 +96,51 @@ defmodule Mobilizon.Config do
|
||||
def instance_maps_tiles_attribution,
|
||||
do: Application.get_env(:mobilizon, :maps)[:tiles][:attribution]
|
||||
|
||||
@spec anonymous_participation? :: boolean
|
||||
def anonymous_participation?,
|
||||
do: Application.get_env(:mobilizon, :anonymous)[:participation][:allowed]
|
||||
|
||||
@spec anonymous_participation_email_required? :: boolean
|
||||
def anonymous_participation_email_required?,
|
||||
do: Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:email][:enabled]
|
||||
|
||||
@spec anonymous_participation_email_confirmation_required? :: boolean
|
||||
def anonymous_participation_email_confirmation_required?,
|
||||
do:
|
||||
Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:email][
|
||||
:confirmation_required
|
||||
]
|
||||
|
||||
@spec anonymous_participation_email_captcha_required? :: boolean
|
||||
def anonymous_participation_email_captcha_required?,
|
||||
do:
|
||||
Application.get_env(:mobilizon, :anonymous)[:participation][:validation][:captcha][:enabled]
|
||||
|
||||
@spec anonymous_event_creation? :: boolean
|
||||
def anonymous_event_creation?,
|
||||
do: Application.get_env(:mobilizon, :anonymous)[:event_creation][:allowed]
|
||||
|
||||
@spec anonymous_event_creation_email_required? :: boolean
|
||||
def anonymous_event_creation_email_required?,
|
||||
do:
|
||||
Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:email][:enabled]
|
||||
|
||||
@spec anonymous_event_creation_email_confirmation_required? :: boolean
|
||||
def anonymous_event_creation_email_confirmation_required?,
|
||||
do:
|
||||
Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:email][
|
||||
:confirmation_required
|
||||
]
|
||||
|
||||
@spec anonymous_event_creation_email_captcha_required? :: boolean
|
||||
def anonymous_event_creation_email_captcha_required?,
|
||||
do:
|
||||
Application.get_env(:mobilizon, :anonymous)[:event_creation][:validation][:captcha][
|
||||
:enabled
|
||||
]
|
||||
|
||||
def anonymous_actor_id, do: get_cached_value(:anonymous_actor_id)
|
||||
|
||||
@spec get(module | atom) :: any
|
||||
def get(key), do: get(key, nil)
|
||||
|
||||
@@ -99,4 +182,38 @@ defmodule Mobilizon.Config do
|
||||
|
||||
@spec to_boolean(boolean | String.t()) :: boolean
|
||||
defp to_boolean(boolean), do: "true" == String.downcase("#{boolean}")
|
||||
|
||||
defp get_cached_value(key) do
|
||||
case Cachex.fetch(:config, key, fn key ->
|
||||
case create_cache(key) do
|
||||
value when not is_nil(value) -> {:commit, value}
|
||||
err -> {:ignore, err}
|
||||
end
|
||||
end) do
|
||||
{status, value} when status in [:ok, :commit] -> value
|
||||
_err -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_cache(atom()) :: integer()
|
||||
defp create_cache(:anonymous_actor_id) do
|
||||
with {:ok, %Actor{id: actor_id}} <- Actors.get_or_create_internal_actor("anonymous") do
|
||||
actor_id
|
||||
end
|
||||
end
|
||||
|
||||
def clear_config_cache do
|
||||
Cachex.clear(:config)
|
||||
end
|
||||
|
||||
def generate_terms(locale) do
|
||||
import Mobilizon.Web.Gettext
|
||||
put_locale(locale)
|
||||
|
||||
Phoenix.View.render_to_string(
|
||||
Mobilizon.Web.APIView,
|
||||
"terms.html",
|
||||
[]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,6 +17,7 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
maximum_attendee_capacity: integer,
|
||||
remaining_attendee_capacity: integer,
|
||||
show_remaining_attendee_capacity: boolean,
|
||||
anonymous_participation: boolean,
|
||||
attendees: [String.t()],
|
||||
program: String.t(),
|
||||
comment_moderation: CommentModeration.t(),
|
||||
@@ -31,6 +32,7 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
:maximum_attendee_capacity,
|
||||
:remaining_attendee_capacity,
|
||||
:show_remaining_attendee_capacity,
|
||||
:anonymous_participation,
|
||||
:attendees,
|
||||
:program,
|
||||
:comment_moderation,
|
||||
@@ -45,6 +47,7 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
field(:maximum_attendee_capacity, :integer)
|
||||
field(:remaining_attendee_capacity, :integer)
|
||||
field(:show_remaining_attendee_capacity, :boolean)
|
||||
field(:anonymous_participation, :boolean)
|
||||
field(:attendees, {:array, :string})
|
||||
field(:program, :string)
|
||||
field(:comment_moderation, CommentModeration)
|
||||
|
||||
@@ -8,6 +8,7 @@ defmodule Mobilizon.Events.EventParticipantStats do
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
not_approved: integer(),
|
||||
not_confirmed: integer(),
|
||||
rejected: integer(),
|
||||
participant: integer(),
|
||||
moderator: integer(),
|
||||
@@ -17,6 +18,7 @@ defmodule Mobilizon.Events.EventParticipantStats do
|
||||
|
||||
@attrs [
|
||||
:not_approved,
|
||||
:not_confirmed,
|
||||
:rejected,
|
||||
:participant,
|
||||
:moderator,
|
||||
@@ -29,6 +31,7 @@ defmodule Mobilizon.Events.EventParticipantStats do
|
||||
@derive Jason.Encoder
|
||||
embedded_schema do
|
||||
field(:not_approved, :integer, default: 0)
|
||||
field(:not_confirmed, :integer, default: 0)
|
||||
field(:rejected, :integer, default: 0)
|
||||
field(:participant, :integer, default: 0)
|
||||
field(:moderator, :integer, default: 0)
|
||||
@@ -47,6 +50,7 @@ defmodule Mobilizon.Events.EventParticipantStats do
|
||||
defp validate_stats(%Ecto.Changeset{} = changeset) do
|
||||
changeset
|
||||
|> validate_number(:not_approved, greater_than_or_equal_to: 0)
|
||||
|> validate_number(:not_confirmed, greater_than_or_equal_to: 0)
|
||||
|> validate_number(:rejected, greater_than_or_equal_to: 0)
|
||||
|> validate_number(:participant, greater_than_or_equal_to: 0)
|
||||
|> validate_number(:moderator, greater_than_or_equal_to: 0)
|
||||
|
||||
@@ -76,6 +76,7 @@ defmodule Mobilizon.Events do
|
||||
|
||||
defenum(ParticipantRole, :participant_role, [
|
||||
:not_approved,
|
||||
:not_confirmed,
|
||||
:rejected,
|
||||
:participant,
|
||||
:moderator,
|
||||
@@ -661,9 +662,39 @@ defmodule Mobilizon.Events do
|
||||
@doc """
|
||||
Gets a single participation for an event and actor.
|
||||
"""
|
||||
@spec get_participant(integer | String.t(), integer | String.t()) ::
|
||||
@spec get_participant(integer | String.t(), integer | String.t(), map()) ::
|
||||
{:ok, Participant.t()} | {:error, :participant_not_found}
|
||||
def get_participant(event_id, actor_id) do
|
||||
def get_participant(event_id, actor_id, params \\ %{})
|
||||
|
||||
# This one if to check someone doesn't go to the same event twice
|
||||
def get_participant(event_id, actor_id, %{email: email}) do
|
||||
case Participant
|
||||
|> where([p], event_id: ^event_id, actor_id: ^actor_id)
|
||||
|> where([p], fragment("? ->>'email' = ?", p.metadata, ^email))
|
||||
|> Repo.one() do
|
||||
%Participant{} = participant ->
|
||||
{:ok, participant}
|
||||
|
||||
nil ->
|
||||
{:error, :participant_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
# This one if for finding participants by their cancellation token when wanting to cancel a participation
|
||||
def get_participant(event_id, actor_id, %{cancellation_token: cancellation_token}) do
|
||||
case Participant
|
||||
|> where([p], event_id: ^event_id, actor_id: ^actor_id)
|
||||
|> where([p], fragment("? ->>'cancellation_token' = ?", p.metadata, ^cancellation_token))
|
||||
|> Repo.one() do
|
||||
%Participant{} = participant ->
|
||||
{:ok, participant}
|
||||
|
||||
nil ->
|
||||
{:error, :participant_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def get_participant(event_id, actor_id, %{}) do
|
||||
case Repo.get_by(Participant, event_id: event_id, actor_id: actor_id) do
|
||||
%Participant{} = participant ->
|
||||
{:ok, participant}
|
||||
@@ -673,6 +704,14 @@ defmodule Mobilizon.Events do
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_participant_by_confirmation_token(String.t()) :: Participant.t()
|
||||
def get_participant_by_confirmation_token(confirmation_token) do
|
||||
Participant
|
||||
|> where([p], fragment("? ->>'confirmation_token' = ?", p.metadata, ^confirmation_token))
|
||||
|> preload([p], [:actor, :event])
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single participation for an event and actor.
|
||||
|
||||
@@ -706,7 +745,7 @@ defmodule Mobilizon.Events do
|
||||
|
||||
@doc """
|
||||
Returns the list of participants for an event.
|
||||
Default behaviour is to not return :not_approved participants
|
||||
Default behaviour is to not return :not_approved or :not_confirmed participants
|
||||
"""
|
||||
@spec list_participants_for_event(String.t(), list(atom()), integer | nil, integer | nil) ::
|
||||
[Participant.t()]
|
||||
|
||||
@@ -10,6 +10,7 @@ defmodule Mobilizon.Events.Participant do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, ParticipantRole}
|
||||
alias Mobilizon.Web.Email.Checker
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
|
||||
@@ -28,6 +29,12 @@ defmodule Mobilizon.Events.Participant do
|
||||
field(:role, ParticipantRole, default: :participant)
|
||||
field(:url, :string)
|
||||
|
||||
embeds_one :metadata, Metadata, on_replace: :delete do
|
||||
field(:email, :string)
|
||||
field(:confirmation_token, :string)
|
||||
field(:cancellation_token, :string)
|
||||
end
|
||||
|
||||
belongs_to(:event, Event, primary_key: true)
|
||||
belongs_to(:actor, Actor, primary_key: true)
|
||||
|
||||
@@ -55,11 +62,18 @@ defmodule Mobilizon.Events.Participant do
|
||||
def changeset(%__MODULE__{} = participant, attrs) do
|
||||
participant
|
||||
|> cast(attrs, @attrs)
|
||||
|> cast_embed(:metadata, with: &metadata_changeset/2)
|
||||
|> ensure_url()
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:actor_id, name: :participants_event_id_actor_id_index)
|
||||
end
|
||||
|
||||
defp metadata_changeset(schema, params) do
|
||||
schema
|
||||
|> cast(params, [:email, :confirmation_token, :cancellation_token])
|
||||
|> Checker.validate_changeset()
|
||||
end
|
||||
|
||||
# If there's a blank URL that's because we're doing the first insert
|
||||
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do
|
||||
|
||||
@@ -11,8 +11,7 @@ defmodule Mobilizon.Users.User do
|
||||
alias Mobilizon.Crypto
|
||||
alias Mobilizon.Events.FeedToken
|
||||
alias Mobilizon.Users.UserRole
|
||||
|
||||
alias Mobilizon.Web.Email
|
||||
alias Mobilizon.Web.Email.Checker
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
email: String.t(),
|
||||
@@ -79,7 +78,7 @@ defmodule Mobilizon.Users.User do
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:email, message: "This email is already used.")
|
||||
|> validate_email()
|
||||
|> Checker.validate_changeset()
|
||||
|> validate_length(:password, min: 6, max: 200, message: "The chosen password is too short.")
|
||||
|
||||
if Map.has_key?(attrs, :default_actor) do
|
||||
@@ -171,25 +170,6 @@ defmodule Mobilizon.Users.User do
|
||||
|
||||
defp save_confirmation_token(%Ecto.Changeset{} = changeset), do: changeset
|
||||
|
||||
@spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp validate_email(%Ecto.Changeset{} = changeset) do
|
||||
changeset = validate_length(changeset, :email, min: 3, max: 250)
|
||||
|
||||
case changeset do
|
||||
%Ecto.Changeset{valid?: true, changes: %{email: email}} ->
|
||||
case Email.Checker.valid?(email) do
|
||||
false ->
|
||||
add_error(changeset, :email, "Email doesn't fit required format")
|
||||
|
||||
true ->
|
||||
changeset
|
||||
end
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
@spec hash_password(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp hash_password(%Ecto.Changeset{} = changeset) do
|
||||
case changeset do
|
||||
|
||||
@@ -2,7 +2,7 @@ defmodule Mobilizon.Web.GraphQLSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
use Absinthe.Phoenix.Socket,
|
||||
schema: Mobilizon.Web.Schema
|
||||
schema: Mobilizon.GraphQL.Schema
|
||||
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ defmodule Mobilizon.Web.PageController do
|
||||
"""
|
||||
use Mobilizon.Web, :controller
|
||||
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Tombstone
|
||||
alias Mobilizon.Web.Cache
|
||||
|
||||
plug(:put_layout, false)
|
||||
@@ -13,40 +16,65 @@ defmodule Mobilizon.Web.PageController do
|
||||
|
||||
def actor(conn, %{"name" => name}) do
|
||||
{status, actor} = Cache.get_local_actor_by_name(name)
|
||||
render_or_error(conn, &ok_status?/2, status, :actor, actor)
|
||||
render_or_error(conn, &ok_status?/3, status, :actor, actor)
|
||||
end
|
||||
|
||||
def event(conn, %{"uuid" => uuid}) do
|
||||
{status, event} = Cache.get_public_event_by_uuid_with_preload(uuid)
|
||||
render_or_error(conn, &ok_status_and_is_visible?/2, status, :event, event)
|
||||
render_or_error(conn, &checks?/3, status, :event, event)
|
||||
end
|
||||
|
||||
def comment(conn, %{"uuid" => uuid}) do
|
||||
{status, comment} = Cache.get_comment_by_uuid_with_preload(uuid)
|
||||
render_or_error(conn, &ok_status_and_is_visible?/2, status, :comment, comment)
|
||||
render_or_error(conn, &checks?/3, status, :comment, comment)
|
||||
end
|
||||
|
||||
def interact(conn, %{"uri" => uri}) do
|
||||
case ActivityPub.fetch_object_from_url(uri) do
|
||||
{:ok, %Event{uuid: uuid}} -> redirect(conn, to: "/events/#{uuid}")
|
||||
{:ok, %Comment{uuid: uuid}} -> redirect(conn, to: "/comments/#{uuid}")
|
||||
_ -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp render_or_error(conn, check_fn, status, object_type, object) do
|
||||
if check_fn.(status, object) do
|
||||
case object do
|
||||
%Mobilizon.Tombstone{} ->
|
||||
conn
|
||||
|> put_status(:gone)
|
||||
|> render(object_type, object: object)
|
||||
case check_fn.(conn, status, object) do
|
||||
true ->
|
||||
case object do
|
||||
%Tombstone{} ->
|
||||
conn
|
||||
|> put_status(:gone)
|
||||
|> render(object_type, object: object)
|
||||
|
||||
_ ->
|
||||
render(conn, object_type, object: object)
|
||||
end
|
||||
else
|
||||
{:error, :not_found}
|
||||
_ ->
|
||||
render(conn, object_type, object: object)
|
||||
end
|
||||
|
||||
:remote ->
|
||||
redirect(conn, external: object.url)
|
||||
|
||||
false ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp is_visible?(%{visibility: v}), do: v in [:public, :unlisted]
|
||||
defp is_visible?(%Mobilizon.Tombstone{}), do: true
|
||||
defp is_visible?(%Tombstone{}), do: true
|
||||
|
||||
defp ok_status?(status), do: status in [:ok, :commit]
|
||||
defp ok_status?(status, _), do: ok_status?(status)
|
||||
defp ok_status?(_conn, status, _), do: ok_status?(status)
|
||||
|
||||
defp ok_status_and_is_visible?(status, o), do: ok_status?(status) and is_visible?(o)
|
||||
defp ok_status_and_is_visible?(_conn, status, o),
|
||||
do: ok_status?(status) and is_visible?(o)
|
||||
|
||||
defp checks?(conn, status, o) do
|
||||
if ok_status_and_is_visible?(conn, status, o) do
|
||||
if is_local?(o) == :remote && get_format(conn) == "activity-json", do: :remote, else: true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp is_local?(%Event{local: local}), do: if(local, do: true, else: :remote)
|
||||
defp is_local?(%Comment{local: local}), do: if(local, do: true, else: :remote)
|
||||
end
|
||||
|
||||
@@ -10,4 +10,19 @@ defmodule Mobilizon.Web.Email.Checker do
|
||||
"""
|
||||
@spec valid?(String.t()) :: boolean
|
||||
def valid?(email), do: email =~ @email_regex
|
||||
|
||||
@spec validate_changeset(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
|
||||
def validate_changeset(%Ecto.Changeset{} = changeset, key \\ :email) do
|
||||
changeset = Ecto.Changeset.validate_length(changeset, :email, min: 3, max: 250)
|
||||
|
||||
case Ecto.Changeset.fetch_change(changeset, key) do
|
||||
{:ok, email} ->
|
||||
if valid?(email),
|
||||
do: changeset,
|
||||
else: Ecto.Changeset.add_error(changeset, :email, "Email doesn't fit required format")
|
||||
|
||||
:error ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,26 +2,34 @@ defmodule Mobilizon.Web.Email.Participation do
|
||||
@moduledoc """
|
||||
Handles emails sent about participation.
|
||||
"""
|
||||
|
||||
use Bamboo.Phoenix, view: Mobilizon.Web.EmailView
|
||||
|
||||
import Bamboo.Phoenix
|
||||
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Events.Participant
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.Web.{Email, Gettext}
|
||||
|
||||
@doc """
|
||||
Send emails to local user
|
||||
"""
|
||||
def send_emails_to_local_user(
|
||||
%Participant{actor: %Actor{user_id: nil} = _actor} = _participation
|
||||
),
|
||||
do: :ok
|
||||
%Participant{actor: %Actor{user_id: nil, id: actor_id} = _actor} = participation
|
||||
) do
|
||||
if actor_id == Config.anonymous_actor_id() do
|
||||
%{email: email} = Map.get(participation, :metadata)
|
||||
|
||||
email
|
||||
|> participation_updated(participation)
|
||||
|> Email.Mailer.deliver_later()
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Send emails to local user
|
||||
@@ -29,7 +37,7 @@ defmodule Mobilizon.Web.Email.Participation do
|
||||
def send_emails_to_local_user(
|
||||
%Participant{actor: %Actor{user_id: user_id} = _actor} = participation
|
||||
) do
|
||||
with %User{} = user <- Mobilizon.Users.get_user!(user_id) do
|
||||
with %User{} = user <- Users.get_user!(user_id) do
|
||||
user
|
||||
|> participation_updated(participation)
|
||||
|> Email.Mailer.deliver_later()
|
||||
@@ -38,11 +46,21 @@ defmodule Mobilizon.Web.Email.Participation do
|
||||
end
|
||||
end
|
||||
|
||||
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
|
||||
@spec participation_updated(String.t() | User.t(), Participant.t(), String.t()) ::
|
||||
Bamboo.Email.t()
|
||||
def participation_updated(user, participant, locale \\ "en")
|
||||
|
||||
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
|
||||
def participation_updated(
|
||||
%User{email: email},
|
||||
%Participant{} = participant,
|
||||
locale
|
||||
),
|
||||
do: participation_updated(email, participant, locale)
|
||||
|
||||
@spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
|
||||
def participation_updated(
|
||||
email,
|
||||
%Participant{event: event, role: :rejected},
|
||||
locale
|
||||
) do
|
||||
@@ -61,9 +79,9 @@ defmodule Mobilizon.Web.Email.Participation do
|
||||
|> render(:event_participation_rejected)
|
||||
end
|
||||
|
||||
@spec participation_updated(User.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
|
||||
@spec participation_updated(String.t(), Participant.t(), String.t()) :: Bamboo.Email.t()
|
||||
def participation_updated(
|
||||
%User{email: email},
|
||||
email,
|
||||
%Participant{event: event, role: :participant},
|
||||
locale
|
||||
) do
|
||||
@@ -81,4 +99,26 @@ defmodule Mobilizon.Web.Email.Participation do
|
||||
|> assign(:subject, subject)
|
||||
|> render(:event_participation_approved)
|
||||
end
|
||||
|
||||
@spec anonymous_participation_confirmation(String.t(), Participant.t(), String.t()) ::
|
||||
Bamboo.Email.t()
|
||||
def anonymous_participation_confirmation(
|
||||
email,
|
||||
%Participant{event: event, role: :not_confirmed} = participant,
|
||||
locale \\ "en"
|
||||
) do
|
||||
Gettext.put_locale(locale)
|
||||
|
||||
subject =
|
||||
gettext(
|
||||
"Confirm your participation to event %{title}",
|
||||
title: event.title
|
||||
)
|
||||
|
||||
Email.base_email(to: email, subject: subject)
|
||||
|> assign(:locale, locale)
|
||||
|> assign(:participant, participant)
|
||||
|> assign(:subject, subject)
|
||||
|> render(:anonymous_participation_confirmation)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,8 +58,6 @@ defmodule Mobilizon.Web.Router do
|
||||
)
|
||||
end
|
||||
|
||||
## FEDERATION
|
||||
|
||||
scope "/.well-known", Mobilizon.Web do
|
||||
pipe_through(:well_known)
|
||||
|
||||
@@ -110,8 +108,10 @@ defmodule Mobilizon.Web.Router do
|
||||
end
|
||||
|
||||
## MOBILIZON
|
||||
|
||||
forward("/graphiql", Absinthe.Plug.GraphiQL, schema: Mobilizon.Web.Schema)
|
||||
scope "/graphiql" do
|
||||
pipe_through(:graphql)
|
||||
forward("/", Absinthe.Plug.GraphiQL, schema: Mobilizon.GraphQL.Schema)
|
||||
end
|
||||
|
||||
scope "/", Mobilizon.Web do
|
||||
pipe_through(:browser)
|
||||
@@ -125,6 +125,12 @@ defmodule Mobilizon.Web.Router do
|
||||
|
||||
# This is a hack to ease link generation into emails
|
||||
get("/moderation/reports/:id", PageController, :index, as: "moderation_report")
|
||||
|
||||
get("/participation/email/confirm/:token", PageController, :index,
|
||||
as: "participation_email_confirmation"
|
||||
)
|
||||
|
||||
get("/interact", PageController, :interact)
|
||||
end
|
||||
|
||||
scope "/proxy/", Mobilizon.Web do
|
||||
|
||||
163
lib/web/templates/api/terms.html.eex
Normal file
163
lib/web/templates/api/terms.html.eex
Normal file
@@ -0,0 +1,163 @@
|
||||
<h3><%= pgettext("terms", "What information do we collect?") %></h3>
|
||||
<ul>
|
||||
<li>
|
||||
<em><%= pgettext("terms", "Basic account information") %></em>
|
||||
<p><%= pgettext(
|
||||
"terms",
|
||||
"We collect information from you when you register on this server and gather data when you participate in the
|
||||
platform by reading, writing, and interacting with content shared here. If you register on this server, you will
|
||||
be asked to enter an e-mail address, a password and at least an username. Your e-mail address will be verified by
|
||||
an email containing a unique link. If that link is visited, we know that you control the e-mail address. You may
|
||||
also enter additional profile information such as a display name and biography, and upload a profile picture and
|
||||
header image. The username, display name, biography, profile picture and header image are always listed publicly.
|
||||
You may, however, visit this server without registering."
|
||||
) %>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<em><%= pgettext("terms", "Published events and comments") %></em>
|
||||
<p>
|
||||
<%= pgettext(
|
||||
"terms",
|
||||
"Your events and comments are delivered to other instances that follow your own, meaning they are delivered to
|
||||
different servers and copies are stored there. When you delete events or comments, this is likewise delivered to
|
||||
these other instances. The action of joining an event is federated as well. Please keep in mind that the operators
|
||||
of the server and any receiving server may view such messages, and that recipients may screenshot, copy or
|
||||
otherwise re-share them."
|
||||
) %>
|
||||
<em><%= pgettext("terms", "Do not share any dangerous information over Mobilizon.") %></em>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<em><%= pgettext("terms", "IPs and other metadata") %></em>
|
||||
<p>
|
||||
<%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"We also may retain server logs which include the IP address of every request to our server."
|
||||
)
|
||||
%>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h3><%= pgettext("terms", "What do we use your information for?") %></h3>
|
||||
<p><%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"Any of the information we collect from you may be used in the following ways:"
|
||||
)
|
||||
%></p>
|
||||
<ul>
|
||||
<li><%= pgettext("terms", "To provide the core functionality of Mobilizon. Depending on this instance's policy you may only be able to
|
||||
interact with other people's content and post your own content if you are logged in.") %></li>
|
||||
<li><%= pgettext("terms", "To aid moderation of the community, for example comparing your IP address with other known ones to determine ban
|
||||
evasion or other violations.") %></li>
|
||||
<li><%= pgettext("terms", "The email address you provide may be used to send you information, updates and notifications about other people
|
||||
interacting with your content or sending you messages and to respond to inquiries, and/or other requests or
|
||||
questions.") %></li>
|
||||
</ul>
|
||||
<h3 class="title"><%= pgettext("terms", "How do we protect your information?") %></h3>
|
||||
<p>
|
||||
<%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"We implement a variety of security measures to maintain the safety of your personal information when you enter,
|
||||
submit, or access your personal information. Among other things, your browser session, as well as the traffic between
|
||||
your applications and the API, are secured with SSL/TLS, and your password is hashed using a strong one-way
|
||||
algorithm."
|
||||
)
|
||||
%>
|
||||
</p>
|
||||
<h3 class="title"><%= pgettext("terms", "What is our data retention policy?") %></h3>
|
||||
<p><%= pgettext("terms", "We will make a good faith effort to:") %></p>
|
||||
<ul>
|
||||
<li><%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"Retain server logs containing the IP address of all requests to this server, in so far as such logs are kept, no more
|
||||
than 90 days."
|
||||
)%>
|
||||
</li>
|
||||
<li>
|
||||
<%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"Retain the IP addresses associated with registered users no more than 12 months."
|
||||
)
|
||||
%>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"You can request and download an archive of your content, including your posts, media attachments, profile picture,
|
||||
and header image."
|
||||
)
|
||||
%>
|
||||
</p>
|
||||
|
||||
<p><%= pgettext("terms", "You may irreversibly delete your account at any time.") %></p>
|
||||
<h3 class="title"><%= pgettext("terms", "Do we use cookies?") %></h3>
|
||||
<p><%=
|
||||
pgettext("terms", "We store the following information on your device when you connect:")
|
||||
%>
|
||||
</p>
|
||||
<ul>
|
||||
<li><%= pgettext("terms", "An internal user ID") %></li>
|
||||
<li><%= pgettext("terms", "An internal ID for your current selected identity") %></li>
|
||||
<li><%= pgettext("terms", "Tokens to authenticate you") %></li>
|
||||
</ul>
|
||||
<p><%= pgettext("terms", "If you delete these informations, you need to login again.") %></p>
|
||||
<p><%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"If you're not connected, we don't store any information on your device, unless you participate in an event
|
||||
anonymously. In that case we store the hash of the UUID and participation status in your browser so that we may
|
||||
display participation status. Deleting these informations will only stop displaying participation status in your
|
||||
browser."
|
||||
)
|
||||
%>
|
||||
</p>
|
||||
<em>
|
||||
<%= pgettext("terms", "Note: These informations are stored in your localStorage and not your cookies.") %>
|
||||
</em>
|
||||
<h3 class="title"><%=
|
||||
pgettext("terms", "Do we disclose any information to outside parties?")
|
||||
%></h3>
|
||||
<p>
|
||||
<%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This
|
||||
does not include trusted third parties who assist us in operating our site, conducting our business, or servicing
|
||||
you, so long as those parties agree to keep this information confidential. We may also release your information
|
||||
when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or
|
||||
others rights, property, or safety."
|
||||
)
|
||||
%>
|
||||
</p>
|
||||
<p>
|
||||
<%=
|
||||
pgettext(
|
||||
"terms",
|
||||
"Your content may be downloaded by other servers in the network. Your content is delivered to the servers
|
||||
following your instance, and direct messages are delivered to the servers of the recipients, in so far as these
|
||||
recipients reside on a different server than this one."
|
||||
)
|
||||
%>
|
||||
</p>
|
||||
|
||||
<h3 class="title"><%=
|
||||
pgettext("terms", "Site usage by children")
|
||||
%></h3>
|
||||
<p><%= pgettext("terms", "If this server is in the EU or the EEA: Our site, products and services are all directed to people who are at least 16 years old. If you are under the age of 16, per the requirements of the GDPR (<a href=\"https://en.wikipedia.org/wiki/General_Data_Protection_Regulation\">General Data Protection Regulation</a>) do not use this site.") |> raw %></p>
|
||||
<p><%= pgettext("terms", "If this server is in the USA: Our site, products and services are all directed to people who are at least 13 years old. If you are under the age of 13, per the requirements of COPPA (<a href=\"https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act\">Children's Online Privacy Protection Act</a>) do not use this site.") |> raw %></p>
|
||||
<p><%= pgettext("terms", "Law requirements can be different if this server is in another jurisdiction.") %></p>
|
||||
|
||||
<h3 class="title"><%=
|
||||
pgettext("terms", "Changes to our Privacy Policy")
|
||||
%></h3>
|
||||
<p><%= pgettext("terms", "If we decide to change our privacy policy, we will post those changes on this page.") %></p>
|
||||
<p><%= pgettext("terms", "This document is CC-BY-SA. It was last updated January 16, 2020.") %></p>
|
||||
<p><%= pgettext("terms", "Originally adapted from the <a href=\"https://mastodon.social/terms\">Mastodon</a> and <a href=\"https://github.com/discourse/discourse\">Discourse</a> privacy policies.") |> raw %></p>
|
||||
@@ -0,0 +1,81 @@
|
||||
<!-- HERO -->
|
||||
<tr>
|
||||
<td bgcolor="#424056" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "Participation confirmation" %>
|
||||
</h1>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
<!-- COPY BLOCK -->
|
||||
<tr>
|
||||
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||
<tr>
|
||||
<td align="center" valign="top" width="600">
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<!-- COPY -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 0px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0;">
|
||||
<%= gettext "You requested to participate in event %{title}", title: @participant.event.title %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;" >
|
||||
<p style="margin: 0">
|
||||
<%= gettext "If you didn't request this email, you can simply ignore it." %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- BULLETPROOF BUTTON -->
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 3px;" bgcolor="#424056"><a href="<%= participation_email_confirmation_url(Mobilizon.Web.Endpoint, :index, @participant.metadata.confirmation_token) %>" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #424056; display: inline-block;">
|
||||
<%= gettext "Confirm my participation" %>
|
||||
</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #777777; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 20px;" >
|
||||
<p style="margin: 0">
|
||||
<%= gettext "If you need to cancel your participation, just access the event page through link above and click on the participation button." %>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--[if (gte mso 9)|(IE)]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
@@ -0,0 +1,11 @@
|
||||
<%= gettext "Participation confirmation" %>
|
||||
|
||||
==
|
||||
|
||||
<%= gettext "You requested to participate in event %{title}.", title: @participant.event.title %>
|
||||
|
||||
<%= gettext "If you didn't request this email, you can simply ignore it." %>
|
||||
|
||||
<%= participation_email_confirmation_url(Mobilizon.Web.Endpoint, :index, @participant.metadata.confirmation_token) %>
|
||||
|
||||
<%= gettext "If you need to cancel your participation, just access the previous link and click on the participation button." %>
|
||||
@@ -8,7 +8,7 @@
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "All good!" %>
|
||||
</h1>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "Sorry!" %>
|
||||
</h1>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "Event updated!" %>
|
||||
</h1>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "Trouble signing in?" %>
|
||||
</h1>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "Nearly here!" %>
|
||||
</h1>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<![endif]-->
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;" >
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
|
||||
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; line-height: 48px;">
|
||||
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">
|
||||
<%= gettext "New report on %{instance}", instance: @instance[:name] %>
|
||||
</h1>
|
||||
|
||||
7
lib/web/views/api_view.ex
Normal file
7
lib/web/views/api_view.ex
Normal file
@@ -0,0 +1,7 @@
|
||||
defmodule Mobilizon.Web.APIView do
|
||||
@moduledoc """
|
||||
View for our the API terms
|
||||
"""
|
||||
use Mobilizon.Web, :view
|
||||
import Mobilizon.Web.Gettext
|
||||
end
|
||||
Reference in New Issue
Block a user