Add anonymous and remote participations

This commit is contained in:
Thomas Citharel
2019-12-20 13:04:34 +01:00
parent 17e0b3968f
commit 2ed9050a90
135 changed files with 10141 additions and 2271 deletions

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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