Merge branch 'refactoring-based-on-credo-and-dialyzer' into 'master'

Refactoring based on credo and dialyzer

Closes #133

See merge request framasoft/mobilizon!179
This commit is contained in:
Thomas Citharel 2019-09-22 09:34:20 +02:00
commit 9a17998074
140 changed files with 4498 additions and 5039 deletions

View File

@ -7,7 +7,7 @@ use Mix.Config
# General application configuration
config :mobilizon,
ecto_repos: [Mobilizon.Repo]
ecto_repos: [Mobilizon.Storage.Repo]
config :mobilizon, :instance,
name: System.get_env("MOBILIZON_INSTANCE_NAME") || "Localhost",
@ -78,7 +78,7 @@ config :mobilizon, MobilizonWeb.Guardian,
secret_key: "ty0WM7YBE3ojvxoUQxo8AERrNpfbXnIJ82ovkPdqbUFw31T5LcK8wGjaOiReVQjo"
config :guardian, Guardian.DB,
repo: Mobilizon.Repo,
repo: Mobilizon.Storage.Repo,
# default
schema_name: "guardian_tokens",
# store all token types if not set

View File

@ -61,11 +61,11 @@ config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.LocalAdapter
config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.LocalAdapter
# Configure your database
config :mobilizon, Mobilizon.Repo,
types: Mobilizon.PostgresTypes,
config :mobilizon, Mobilizon.Storage.Repo,
types: Mobilizon.Storage.PostgresTypes,
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_dev",

View File

@ -1,16 +0,0 @@
use Mix.Config
alias Dogma.Rule
config :dogma,
# Select a set of rules as a base
rule_set: Dogma.RuleSet.All,
# Pick paths not to lint
exclude: [
~r(\Alib/vendor/)
],
# Override an existing rule configuration
override: [
%Rule.LineLength{enabled: false}
]

View File

@ -12,8 +12,8 @@ config :mobilizon, MobilizonWeb.Endpoint,
cache_static_manifest: "priv/static/manifest.json"
# Configure your database
config :mobilizon, Mobilizon.Repo,
types: Mobilizon.PostgresTypes,
config :mobilizon, Mobilizon.Storage.Repo,
types: Mobilizon.Storage.PostgresTypes,
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_prod",
@ -21,7 +21,7 @@ config :mobilizon, Mobilizon.Repo,
port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432",
pool_size: 15
config :mobilizon, Mobilizon.Mailer,
config :mobilizon, MobilizonWeb.Email.Mailer,
adapter: Bamboo.SMTPAdapter,
server: "localhost",
hostname: "localhost",

View File

@ -22,16 +22,15 @@ config :logger,
level: :info
# Configure your database
config :mobilizon, Mobilizon.Repo,
types: Mobilizon.PostgresTypes,
config :mobilizon, Mobilizon.Storage.Repo,
types: Mobilizon.Storage.PostgresTypes,
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_test",
hostname: System.get_env("MOBILIZON_DATABASE_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox,
types: Mobilizon.PostgresTypes
pool: Ecto.Adapters.SQL.Sandbox
config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.TestAdapter
config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.TestAdapter
config :mobilizon, MobilizonWeb.Upload, filters: [], link_name: false

View File

@ -15,7 +15,7 @@ defmodule Mix.Tasks.Mobilizon.CreateBot do
Mix.Task.run("app.start")
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
actor <- Actors.register_bot_account(%{name: name, summary: summary}),
actor <- Actors.register_bot(%{name: name, summary: summary}),
{:ok, %Bot{} = bot} <-
Actors.create_bot(%{
"type" => type,

View File

@ -1,9 +1,92 @@
defmodule Mobilizon do
@moduledoc """
Mobilizon is a decentralized and federated Meetup-like using [ActivityPub](http://activitypub.rocks/).
Mobilizon is a decentralized and federated Meetup-like using
[ActivityPub](http://activitypub.rocks/).
It consists of an API server build with [Elixir](http://elixir-lang.github.io/) and the [Phoenix Framework](https://hexdocs.pm/phoenix).
It consists of an API server build with [Elixir](http://elixir-lang.github.io/)
and the [Phoenix Framework](https://hexdocs.pm/phoenix).
Mobilizon relies on `Guardian` for auth and `Geo`/Postgis for geographical informations.
Mobilizon relies on `Guardian` for auth and `Geo`/Postgis for geographical
information.
"""
use Application
import Cachex.Spec
alias Mobilizon.Config
alias Mobilizon.Service.Export.{Feed, ICalendar}
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@spec named_version :: String.t()
def named_version, do: "#{@name} #{@version}"
@spec user_agent :: String.t(:w)
def user_agent do
info = "#{MobilizonWeb.Endpoint.url()} <#{Config.get([:instance, :email], "")}>"
"#{named_version()}; #{info}"
end
@spec start(:normal | {:takeover, node} | {:failover, node}, term) ::
{:ok, pid} | {:ok, pid, term} | {:error, term}
def start(_type, _args) do
children = [
# supervisors
Mobilizon.Storage.Repo,
MobilizonWeb.Endpoint,
# workers
Guardian.DB.Token.SweeperServer,
Mobilizon.Service.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)
]
opts = [strategy: :one_for_one, name: Mobilizon.Supervisor]
Supervisor.start_link(children, opts)
end
@spec config_change(keyword, keyword, [atom]) :: :ok
def config_change(changed, _new, removed) do
MobilizonWeb.Endpoint.config_change(changed, removed)
:ok
end
@spec cachex_spec(atom, integer, integer, integer, function | nil) :: Supervisor.child_spec()
defp cachex_spec(name, limit, default, interval, fallback \\ nil) do
%{
id: :"cache_#{name}",
start:
{Cachex, :start_link,
[
name,
Keyword.merge(
cachex_options(limit, default, interval),
fallback_options(fallback)
)
]}
}
end
@spec cachex_options(integer, integer, integer) :: keyword
defp cachex_options(limit, default, interval) do
[
limit: limit,
expiration:
expiration(
default: :timer.minutes(default),
interval: :timer.seconds(interval)
)
]
end
@spec fallback_options(function | nil) :: keyword
defp fallback_options(nil), do: []
defp fallback_options(fallback), do: [fallback: fallback(default: fallback)]
end

View File

@ -1,7 +0,0 @@
defmodule Mobilizon.Activity do
@moduledoc """
Represents an activity
"""
defstruct [:data, :local, :actor, :recipients, :notifications]
end

View File

@ -1,91 +1,56 @@
import EctoEnum
defenum(Mobilizon.Actors.ActorTypeEnum, :actor_type, [
:Person,
:Application,
:Group,
:Organization,
:Service
])
defenum(Mobilizon.Actors.ActorOpennessEnum, :actor_openness, [
:invite_only,
:moderated,
:open
])
defenum(Mobilizon.Actors.ActorVisibilityEnum, :actor_visibility_type, [
:public,
:unlisted,
# Probably unused
:restricted,
:private
])
defmodule Mobilizon.Actors.Actor do
@moduledoc """
Represents an actor (local and remote actors)
Represents an actor (local and remote).
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors
alias Mobilizon.Users.User
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.{Actors, Config, Crypto}
alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member}
alias Mobilizon.Events.{Event, FeedToken}
alias Mobilizon.Media.File
alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Users.User
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
import Ecto.Query
import Mobilizon.Ecto
alias Mobilizon.Repo
require Logger
# @type t :: %Actor{description: String.t, id: integer(), inserted_at: DateTime.t, updated_at: DateTime.t, display_name: String.t, domain: String.t, keys: String.t, suspended: boolean(), url: String.t, username: String.t, organized_events: list(), groups: list(), group_request: list(), user: User.t, field: ActorTypeEnum.t}
@type t :: %__MODULE__{
url: String.t(),
outbox_url: String.t(),
inbox_url: String.t(),
following_url: String.t(),
followers_url: String.t(),
shared_inbox_url: String.t(),
type: ActorType.t(),
name: String.t(),
domain: String.t(),
summary: String.t(),
preferred_username: String.t(),
keys: String.t(),
manually_approves_followers: boolean,
openness: ActorOpenness.t(),
visibility: ActorVisibility.t(),
suspended: boolean,
avatar: File.t(),
banner: File.t(),
user: User.t(),
followers: [Follower.t()],
followings: [Follower.t()],
organized_events: [Event.t()],
feed_tokens: [FeedToken.t()],
created_reports: [Report.t()],
subject_reports: [Report.t()],
report_notes: [Note.t()],
memberships: [t]
}
schema "actors" do
field(:url, :string)
field(:outbox_url, :string)
field(:inbox_url, :string)
field(:following_url, :string)
field(:followers_url, :string)
field(:shared_inbox_url, :string)
field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person)
field(:name, :string)
field(:domain, :string, default: nil)
field(:summary, :string)
field(:preferred_username, :string)
field(:keys, :string)
field(:manually_approves_followers, :boolean, default: false)
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private)
field(:suspended, :boolean, default: false)
# field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
has_many(:followers, Follower, foreign_key: :target_actor_id)
has_many(:followings, Follower, foreign_key: :actor_id)
has_many(:organized_events, Event, foreign_key: :organizer_actor_id)
many_to_many(:memberships, Actor, join_through: Member)
belongs_to(:user, User)
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
embeds_one(:avatar, File, on_replace: :update)
embeds_one(:banner, File, on_replace: :update)
has_many(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id)
timestamps()
end
@doc false
def changeset(%Actor{} = actor, attrs) do
actor
|> Ecto.Changeset.cast(attrs, [
:url,
@required_attrs [:preferred_username, :keys, :suspended, :url]
@optional_attrs [
:outbox_url,
:inbox_url,
:shared_inbox_url,
@ -95,136 +60,40 @@ defmodule Mobilizon.Actors.Actor do
:name,
:domain,
:summary,
:preferred_username,
:keys,
:manually_approves_followers,
:suspended,
:user_id
])
|> build_urls()
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> unique_username_validator()
|> validate_required([:preferred_username, :keys, :suspended, :url])
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|> unique_constraint(:url, name: :actors_url_index)
end
]
@attrs @required_attrs ++ @optional_attrs
@doc false
def update_changeset(%Actor{} = actor, attrs) do
actor
|> Ecto.Changeset.cast(attrs, [
:name,
:summary,
:keys,
:manually_approves_followers,
:suspended,
:user_id
])
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> validate_required([:preferred_username, :keys, :suspended, :url])
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|> unique_constraint(:url, name: :actors_url_index)
end
@update_required_attrs @required_attrs -- [:url]
@update_optional_attrs [:name, :summary, :manually_approves_followers, :user_id]
@update_attrs @update_required_attrs ++ @update_optional_attrs
@doc """
Changeset for person registration
"""
@spec registration_changeset(struct(), map()) :: Ecto.Changeset.t()
def registration_changeset(%Actor{} = actor, attrs) do
actor
|> Ecto.Changeset.cast(attrs, [
:preferred_username,
:domain,
:name,
:summary,
:keys,
:suspended,
:url,
:type,
:user_id
])
|> build_urls()
|> cast_embed(:avatar)
|> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator()
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|> unique_constraint(:url, name: :actors_url_index)
|> validate_required([:preferred_username, :keys, :suspended, :url, :type])
end
@registration_required_attrs [:preferred_username, :keys, :suspended, :url, :type]
@registration_optional_attrs [:domain, :name, :summary, :user_id]
@registration_attrs @registration_required_attrs ++ @registration_optional_attrs
# TODO : Use me !
# @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@doc """
Changeset for remote actor creation
"""
@spec remote_actor_creation(map()) :: Ecto.Changeset.t()
def remote_actor_creation(params) do
changes =
%Actor{}
|> Ecto.Changeset.cast(params, [
:url,
:outbox_url,
:inbox_url,
:shared_inbox_url,
:following_url,
:followers_url,
:type,
:name,
:domain,
:summary,
:preferred_username,
:keys,
:manually_approves_followers
])
|> validate_required([
@remote_actor_creation_required_attrs [
:url,
:inbox_url,
:type,
:domain,
:preferred_username,
:keys
])
|> cast_embed(:avatar)
|> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator()
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|> unique_constraint(:url, name: :actors_url_index)
|> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100)
]
@remote_actor_creation_optional_attrs [
:outbox_url,
:shared_inbox_url,
:following_url,
:followers_url,
:name,
:summary,
:manually_approves_followers
]
@remote_actor_creation_attrs @remote_actor_creation_required_attrs ++
@remote_actor_creation_optional_attrs
Logger.debug("Remote actor creation")
Logger.debug(inspect(changes))
changes
end
def relay_creation(%{url: url, preferred_username: preferred_username} = _params) do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing()
vars = %{
"name" => Mobilizon.CommonConfig.get([:instance, :name], "Mobilizon"),
"summary" =>
Mobilizon.CommonConfig.get(
[:instance, :description],
"An internal service actor for this Mobilizon instance"
),
"url" => url,
"keys" => pem,
"preferred_username" => preferred_username,
"domain" => nil,
"inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
"followers_url" => "#{url}/followers",
"following_url" => "#{url}/following",
"shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
"type" => :Application
}
cast(%Actor{}, vars, [
@relay_creation_attrs [
:type,
:name,
:summary,
@ -236,77 +105,225 @@ defmodule Mobilizon.Actors.Actor do
: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
schema "actors" do
field(:url, :string)
field(:outbox_url, :string)
field(:inbox_url, :string)
field(:following_url, :string)
field(:followers_url, :string)
field(:shared_inbox_url, :string)
field(:type, ActorType, default: :Person)
field(:name, :string)
field(:domain, :string, default: nil)
field(:summary, :string)
field(:preferred_username, :string)
field(:keys, :string)
field(:manually_approves_followers, :boolean, default: false)
field(:openness, ActorOpenness, default: :moderated)
field(:visibility, ActorVisibility, default: :private)
field(:suspended, :boolean, default: false)
embeds_one(:avatar, File, on_replace: :update)
embeds_one(:banner, File, on_replace: :update)
belongs_to(:user, User)
has_many(:followers, Follower, foreign_key: :target_actor_id)
has_many(:followings, Follower, foreign_key: :actor_id)
has_many(:organized_events, Event, foreign_key: :organizer_actor_id)
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
has_many(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id)
many_to_many(:memberships, __MODULE__, join_through: Member)
timestamps()
end
@doc """
Checks whether actor visibility is public.
"""
@spec is_public_visibility(t) :: boolean
def is_public_visibility(%__MODULE__{visibility: visibility}) do
visibility in [:public, :unlisted]
end
@doc """
Returns the display name if available, or the preferred username
(with the eventual @domain suffix if it's a distant actor).
"""
@spec display_name(t) :: String.t()
def display_name(%__MODULE__{name: name} = actor) when name in [nil, ""] do
preferred_username_and_domain(actor)
end
def display_name(%__MODULE__{name: name}), do: name
@doc """
Returns display name and username.
"""
@spec display_name_and_username(t) :: String.t()
def display_name_and_username(%__MODULE__{name: name} = actor) when name in [nil, ""] do
preferred_username_and_domain(actor)
end
def display_name_and_username(%__MODULE__{name: name} = actor) do
"#{name} (#{preferred_username_and_domain(actor)})"
end
@doc """
Returns the preferred username with the eventual @domain suffix if it's
a distant actor.
"""
@spec preferred_username_and_domain(t) :: String.t()
def preferred_username_and_domain(%__MODULE__{
preferred_username: preferred_username,
domain: nil
}) do
preferred_username
end
def preferred_username_and_domain(%__MODULE__{
preferred_username: preferred_username,
domain: domain
}) do
"#{preferred_username}@#{domain}"
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = actor, attrs) do
actor
|> cast(attrs, @attrs)
|> build_urls()
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> unique_username_validator()
|> validate_required(@required_attrs)
|> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index)
end
@doc false
@spec update_changeset(t, map) :: Ecto.Changeset.t()
def update_changeset(%__MODULE__{} = actor, attrs) do
actor
|> cast(attrs, @update_attrs)
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> validate_required(@update_required_attrs)
|> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index)
end
@doc """
Changeset for person registration.
"""
@spec registration_changeset(t, map) :: Ecto.Changeset.t()
def registration_changeset(%__MODULE__{} = actor, attrs) do
actor
|> cast(attrs, @registration_attrs)
|> build_urls()
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> unique_username_validator()
|> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index)
|> validate_required(@registration_required_attrs)
end
@doc """
Changeset for remote actor creation.
"""
@spec remote_actor_creation_changeset(map) :: Ecto.Changeset.t()
def remote_actor_creation_changeset(attrs) do
changeset =
%__MODULE__{}
|> cast(attrs, @remote_actor_creation_attrs)
|> validate_required(@remote_actor_creation_required_attrs)
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> unique_username_validator()
|> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index)
|> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100)
Logger.debug("Remote actor creation: #{inspect(changeset)}")
changeset
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
"""
@spec group_creation(struct(), map()) :: Ecto.Changeset.t()
def group_creation(%Actor{} = actor, params) do
@spec group_creation_changeset(t, map) :: Ecto.Changeset.t()
def group_creation_changeset(%__MODULE__{} = actor, params) do
actor
|> Ecto.Changeset.cast(params, [
:url,
:outbox_url,
:inbox_url,
:shared_inbox_url,
:type,
:name,
:domain,
:summary,
:preferred_username
])
|> cast(params, @group_creation_attrs)
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> build_urls(:Group)
|> put_change(:domain, nil)
|> put_change(:keys, Actors.create_keys())
|> put_change(:keys, Crypto.generate_rsa_2048_private_key())
|> put_change(:type, :Group)
|> unique_username_validator()
|> validate_required([:url, :outbox_url, :inbox_url, :type, :preferred_username])
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|> validate_required(@group_creation_required_attrs)
|> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index)
|> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100)
end
# Needed because following constraint can't work for domain null values (local)
@spec unique_username_validator(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp unique_username_validator(
%Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset
) do
with nil <- Map.get(changes, :domain, nil),
%Actor{preferred_username: _username} <- Actors.get_local_actor_by_name(username) do
changeset |> add_error(:preferred_username, "Username is already taken")
%__MODULE__{preferred_username: _} <- Actors.get_local_actor_by_name(username) do
add_error(changeset, :preferred_username, "Username is already taken")
else
_ -> changeset
end
end
# When we don't even have any preferred_username, don't even try validating preferred_username
defp unique_username_validator(changeset) do
changeset
end
defp unique_username_validator(changeset), do: changeset
@spec build_urls(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
@spec build_urls(Ecto.Changeset.t(), ActorType.t()) :: Ecto.Changeset.t()
defp build_urls(changeset, type \\ :Person)
defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do
changeset
|> put_change(
:outbox_url,
build_url(username, :outbox)
)
|> put_change(
:followers_url,
build_url(username, :followers)
)
|> put_change(
:following_url,
build_url(username, :following)
)
|> put_change(
:inbox_url,
build_url(username, :inbox)
)
|> put_change(:outbox_url, build_url(username, :outbox))
|> put_change(:followers_url, build_url(username, :followers))
|> put_change(:following_url, build_url(username, :following))
|> put_change(:inbox_url, build_url(username, :inbox))
|> put_change(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox")
|> put_change(:url, build_url(username, :page))
end
@ -314,19 +331,19 @@ defmodule Mobilizon.Actors.Actor do
defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset
@doc """
Build an AP URL for an actor
Builds an AP URL for an actor.
"""
@spec build_url(String.t(), atom()) :: String.t()
@spec build_url(String.t(), atom, keyword) :: String.t()
def build_url(preferred_username, endpoint, args \\ [])
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
def build_url(preferred_username, :page, args) do
Endpoint
|> Routes.page_url(:actor, preferred_username, args)
|> URI.decode()
end
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
def build_url(preferred_username, endpoint, args)
when endpoint in [:outbox, :following, :followers] do
Endpoint
@ -334,267 +351,24 @@ defmodule Mobilizon.Actors.Actor do
|> URI.decode()
end
@doc """
Get a public key for a given ActivityPub actor ID (url)
"""
@spec get_public_key_for_url(String.t()) :: {:ok, String.t()} | {:error, atom()}
def get_public_key_for_url(url) do
with {:ok, %Actor{keys: keys}} <- Actors.get_or_fetch_by_url(url),
{:ok, public_key} <- prepare_public_key(keys) do
{:ok, public_key}
else
{:error, :pem_decode_error} ->
Logger.error("Error while decoding PEM")
{:error, :pem_decode_error}
_ ->
Logger.error("Unable to fetch actor, so no keys for you")
{:error, :actor_fetch_error}
end
end
@doc """
Convert internal PEM encoded keys to public key format
"""
@spec prepare_public_key(String.t()) :: {:ok, tuple()} | {:error, :pem_decode_error}
def prepare_public_key(public_key_code) do
case :public_key.pem_decode(public_key_code) do
[public_key_entry] ->
{:ok, :public_key.pem_entry_decode(public_key_entry)}
_err ->
{:error, :pem_decode_error}
end
end
@doc """
Get followers from an actor
If actor A and C both follow actor B, actor B's followers are A and C
"""
@spec get_followers(struct(), number(), number()) :: map()
def get_followers(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
query =
from(
a in Actor,
join: f in Follower,
on: a.id == f.actor_id,
where: f.target_actor_id == ^actor_id
)
total = Task.async(fn -> Repo.aggregate(query, :count, :id) end)
elements = Task.async(fn -> Repo.all(paginate(query, page, limit)) end)
%{total: Task.await(total), elements: Task.await(elements)}
end
defp get_full_followers_query(%Actor{id: actor_id} = _actor) do
from(
a in Actor,
join: f in Follower,
on: a.id == f.actor_id,
where: f.target_actor_id == ^actor_id
)
end
@spec get_full_followers(struct()) :: list()
def get_full_followers(%Actor{} = actor) do
actor
|> get_full_followers_query()
|> Repo.all()
end
@spec get_full_external_followers(struct()) :: list()
def get_full_external_followers(%Actor{} = actor) do
actor
|> get_full_followers_query()
|> where([a], not is_nil(a.domain))
|> Repo.all()
end
@doc """
Get followings from an actor
If actor A follows actor B and C, actor A's followings are B and B
"""
@spec get_followings(struct(), number(), number()) :: list()
def get_followings(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
query =
from(
a in Actor,
join: f in Follower,
on: a.id == f.target_actor_id,
where: f.actor_id == ^actor_id
)
total = Task.async(fn -> Repo.aggregate(query, :count, :id) end)
elements = Task.async(fn -> Repo.all(paginate(query, page, limit)) end)
%{total: Task.await(total), elements: Task.await(elements)}
end
@spec get_full_followings(struct()) :: list()
def get_full_followings(%Actor{id: actor_id} = _actor) do
Repo.all(
from(
a in Actor,
join: f in Follower,
on: a.id == f.target_actor_id,
where: f.actor_id == ^actor_id
)
)
end
@doc """
Returns the groups an actor is member of
"""
@spec get_groups_member_of(struct()) :: list()
def get_groups_member_of(%Actor{id: actor_id}) do
Repo.all(
from(
a in Actor,
join: m in Member,
on: a.id == m.parent_id,
where: m.actor_id == ^actor_id
)
)
end
@doc """
Returns the members for a group actor
"""
@spec get_members_for_group(struct()) :: list()
def get_members_for_group(%Actor{id: actor_id}) do
Repo.all(
from(
a in Actor,
join: m in Member,
on: a.id == m.actor_id,
where: m.parent_id == ^actor_id
)
)
end
@doc """
Make an actor follow another
"""
@spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()}
def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do
with {:suspended, false} <- {:suspended, followed.suspended},
# Check if followed has blocked follower
{:already_following, false} <- {:already_following, following?(follower, followed)} do
do_follow(follower, followed, approved, url)
else
{:already_following, %Follower{}} ->
{:error, :already_following,
"Could not follow actor: you are already following #{followed.preferred_username}"}
{:suspended, _} ->
{:error, :suspended,
"Could not follow actor: #{followed.preferred_username} has been suspended"}
end
end
@doc """
Unfollow an actor (remove a `Mobilizon.Actors.Follower`)
"""
@spec unfollow(struct(), struct()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
def unfollow(%Actor{} = followed, %Actor{} = follower) do
case {:already_following, following?(follower, followed)} do
{:already_following, %Follower{} = follow} ->
Actors.delete_follower(follow)
{:already_following, false} ->
{:error, "Could not unfollow actor: you are not following #{followed.preferred_username}"}
end
end
@spec do_follow(struct(), struct(), boolean(), String.t()) ::
{:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved, url) do
Logger.info(
"Making #{follower.preferred_username} follow #{followed.preferred_username} (approved: #{
approved
})"
)
Actors.create_follower(%{
"actor_id" => follower.id,
"target_actor_id" => followed.id,
"approved" => approved,
"url" => url
})
end
@doc """
Returns whether an actor is following another
"""
@spec following?(struct(), struct()) :: Follower.t() | false
def following?(
%Actor{} = follower_actor,
%Actor{} = followed_actor
) do
case Actors.get_follower(followed_actor, follower_actor) do
nil -> false
%Follower{} = follow -> follow
end
end
@spec public_visibility?(struct()) :: boolean()
def public_visibility?(%Actor{visibility: visibility}), do: visibility in [:public, :unlisted]
@doc """
Return the preferred_username with the eventual @domain suffix if it's a distant actor
"""
@spec actor_acct_from_actor(struct()) :: String.t()
def actor_acct_from_actor(%Actor{preferred_username: preferred_username, domain: domain}) do
if is_nil(domain) do
preferred_username
else
"#{preferred_username}@#{domain}"
end
end
@doc """
Returns the display name if available, or the preferred_username (with the eventual @domain suffix if it's a distant actor).
"""
@spec display_name(struct()) :: String.t()
def display_name(%Actor{name: name} = actor) do
case name do
nil -> actor_acct_from_actor(actor)
"" -> actor_acct_from_actor(actor)
name -> name
end
end
@doc """
Return display name and username
## Examples
iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: nil})
"Thomas (tcit)"
iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: "framapiaf.org"})
"Thomas (tcit@framapiaf.org)"
iex> display_name_and_username(%Actor{name: nil, preferred_username: "tcit", domain: "framapiaf.org"})
"tcit@framapiaf.org"
"""
@spec display_name_and_username(struct()) :: String.t()
def display_name_and_username(%Actor{name: nil} = actor), do: actor_acct_from_actor(actor)
def display_name_and_username(%Actor{name: ""} = actor), do: actor_acct_from_actor(actor)
def display_name_and_username(%Actor{name: name} = actor),
do: name <> " (" <> actor_acct_from_actor(actor) <> ")"
@doc """
Clear multiple caches for an actor
"""
@spec clear_cache(struct()) :: {:ok, true}
def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do
Cachex.del(:activity_pub, "actor_" <> preferred_username)
Cachex.del(:feed, "actor_" <> preferred_username)
Cachex.del(:ics, "actor_" <> preferred_username)
@spec build_relay_creation_attrs(map) :: map
defp build_relay_creation_attrs(%{url: url, preferred_username: preferred_username}) do
%{
"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,
"domain" => nil,
"inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
"followers_url" => "#{url}/followers",
"following_url" => "#{url}/following",
"shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
"type" => :Application
}
end
end

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,30 @@
defmodule Mobilizon.Actors.Bot do
@moduledoc """
Represents a local bot
Represents a local bot.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
@type t :: %__MODULE__{
source: String.t(),
type: String.t(),
actor: Actor.t(),
user: User.t()
}
@required_attrs [:source]
@optional_attrs [:type, :actor_id, :user_id]
@attrs @required_attrs ++ @optional_attrs
schema "bots" do
field(:source, :string)
field(:type, :string, default: :ics)
belongs_to(:actor, Actor)
belongs_to(:user, User)
@ -17,9 +32,10 @@ defmodule Mobilizon.Actors.Bot do
end
@doc false
def changeset(bot, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = bot, attrs) do
bot
|> cast(attrs, [:source, :type, :actor_id, :user_id])
|> validate_required([:source])
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View File

@ -1,52 +1,66 @@
defmodule Mobilizon.Actors.Follower do
@moduledoc """
Represents the following of an actor to another actor
Represents the following of an actor to another actor.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Follower
alias Mobilizon.Actors.Actor
@primary_key {:id, :binary_id, autogenerate: true}
@type t :: %__MODULE__{
approved: boolean,
url: String.t(),
target_actor: Actor.t(),
actor: Actor.t()
}
@required_attrs [:url, :approved, :target_actor_id, :actor_id]
@attrs @required_attrs
@primary_key {:id, :binary_id, autogenerate: true}
schema "followers" do
field(:approved, :boolean, default: false)
field(:url, :string)
belongs_to(:target_actor, Actor)
belongs_to(:actor, Actor)
end
@doc false
def changeset(%Follower{} = member, attrs) do
member
|> cast(attrs, [:url, :approved, :target_actor_id, :actor_id])
|> generate_url()
|> validate_required([:url, :approved, :target_actor_id, :actor_id])
|> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index)
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(follower, attrs) do
follower
|> cast(attrs, @attrs)
|> ensure_url()
|> validate_required(@required_attrs)
|> unique_constraint(:target_actor_id,
name: :followers_actor_target_actor_unique_index
)
end
# If there's a blank URL that's because we're doing the first insert
defp generate_url(%Ecto.Changeset{data: %Follower{url: nil}} = changeset) do
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do
case fetch_change(changeset, :url) do
{:ok, _url} -> changeset
:error -> do_generate_url(changeset)
{:ok, _url} ->
changeset
:error ->
generate_url(changeset)
end
end
# Most time just go with the given URL
defp generate_url(%Ecto.Changeset{} = changeset), do: changeset
defp ensure_url(%Ecto.Changeset{} = changeset), do: changeset
defp do_generate_url(%Ecto.Changeset{} = changeset) do
@spec generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp generate_url(%Ecto.Changeset{} = changeset) do
uuid = Ecto.UUID.generate()
changeset
|> put_change(
:url,
"#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}"
)
|> put_change(
:id,
uuid
)
|> put_change(:id, uuid)
|> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}")
end
end

View File

@ -1,101 +1,59 @@
import EctoEnum
defenum(Mobilizon.Actors.MemberRoleEnum, :member_role_type, [
:not_approved,
:member,
:moderator,
:administrator,
:creator
])
defmodule Mobilizon.Actors.Member do
@moduledoc """
Represents the membership of an actor to a group
Represents the membership of an actor to a group.
"""
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query, warn: false
import Mobilizon.Ecto
alias Mobilizon.Actors.Member
alias Mobilizon.Actors.Actor
alias Mobilizon.Repo
alias Mobilizon.Actors.{Actor, MemberRole}
@type t :: %__MODULE__{
role: MemberRole.t(),
parent: Actor.t(),
actor: Actor.t()
}
@required_attrs [:parent_id, :actor_id]
@optional_attrs [:role]
@attrs @required_attrs ++ @optional_attrs
schema "members" do
field(:role, Mobilizon.Actors.MemberRoleEnum, default: :member)
field(:role, MemberRole, default: :member)
belongs_to(:parent, Actor)
belongs_to(:actor, Actor)
timestamps()
end
@doc false
def changeset(%Member{} = member, attrs) do
member
|> cast(attrs, [:role, :parent_id, :actor_id])
|> validate_required([:parent_id, :actor_id])
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
end
@doc """
Gets a single member of an actor (for example a group)
Gets the default member role depending on the actor openness.
"""
def get_member(actor_id, parent_id) do
case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do
nil -> {:error, :member_not_found}
member -> {:ok, member}
end
end
@spec get_default_member_role(Actor.t()) :: atom
def get_default_member_role(%Actor{openness: :open}), do: :member
def get_default_member_role(%Actor{}), do: :not_approved
@doc """
Gets a single member of an actor (for example a group)
Checks whether the actor can be joined to the group.
"""
def can_be_joined(%Actor{type: :Group, openness: :invite_only}), do: false
def can_be_joined(%Actor{type: :Group}), do: true
@doc """
Returns the list of administrator members for a group.
Checks whether the member is an administrator (admin or creator) of the group.
"""
def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do
Repo.all(
from(
m in Member,
where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator),
preload: [:actor]
)
|> paginate(page, limit)
)
end
def is_administrator(%__MODULE__{role: :administrator}), do: {:is_admin, true}
def is_administrator(%__MODULE__{role: :creator}), do: {:is_admin, true}
def is_administrator(%__MODULE__{}), do: {:is_admin, false}
@doc """
Get all group ids where the actor_id is the last administrator
"""
def list_group_id_where_last_administrator(actor_id) do
in_query =
from(
m in Member,
where: m.actor_id == ^actor_id and (m.role == ^:creator or m.role == ^:administrator),
select: m.parent_id
)
Repo.all(
from(
m in Member,
where: m.role == ^:creator or m.role == ^:administrator,
join: m2 in subquery(in_query),
on: m.parent_id == m2.parent_id,
group_by: m.parent_id,
select: m.parent_id,
having: count(m.actor_id) == 1
)
)
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = member, attrs) do
member
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
end
@doc """
Returns true if the member is an administrator (admin or creator) of the group
"""
def is_administrator(%Member{role: :administrator}), do: {:is_admin, true}
def is_administrator(%Member{role: :creator}), do: {:is_admin, true}
def is_administrator(%Member{}), do: {:is_admin, false}
end

View File

@ -1,12 +1,30 @@
defmodule Mobilizon.Addresses.Address do
@moduledoc "An address for an event or a group"
@moduledoc """
Represents an address for an event or a group.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event
# alias Mobilizon.Actors.Actor
@attrs [
@type t :: %__MODULE__{
country: String.t(),
locality: String.t(),
region: String.t(),
description: String.t(),
floor: String.t(),
geom: Geo.PostGIS.Geometry.t(),
postal_code: String.t(),
street: String.t(),
url: String.t(),
origin_id: String.t(),
events: [Event.t()]
}
@required_attrs [:url]
@optional_attrs [
:description,
:floor,
:geom,
@ -15,12 +33,9 @@ defmodule Mobilizon.Addresses.Address do
:region,
:postal_code,
:street,
:url,
:origin_id
]
@required [
:url
]
@attrs @required_attrs ++ @optional_attrs
schema "addresses" do
field(:country, :string)
@ -33,22 +48,25 @@ defmodule Mobilizon.Addresses.Address do
field(:street, :string)
field(:url, :string)
field(:origin_id, :string)
has_many(:event, Event, foreign_key: :physical_address_id)
has_many(:events, Event, foreign_key: :physical_address_id)
timestamps()
end
@doc false
def changeset(%Address{} = address, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = address, attrs) do
address
|> cast(attrs, @attrs)
|> set_url()
|> validate_required(@required)
|> validate_required(@required_attrs)
end
@spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
url =
Map.get(changes, :url, MobilizonWeb.Endpoint.url() <> "/address/#{Ecto.UUID.generate()}")
uuid = Ecto.UUID.generate()
url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{uuid}")
put_change(changeset, :url, url)
end

View File

@ -3,82 +3,36 @@ defmodule Mobilizon.Addresses do
The Addresses context.
"""
import Ecto.Query, warn: false
alias Mobilizon.Repo
require Logger
import Ecto.Query
alias Mobilizon.Addresses.Address
alias Mobilizon.Storage.Repo
@geom_types [:point]
@doc false
def data() do
Dataloader.Ecto.new(Repo, query: &query/2)
end
@doc false
def query(queryable, _params) do
queryable
end
@doc """
Returns the list of addresses.
## Examples
iex> list_addresses()
[%Address{}, ...]
"""
def list_addresses do
Repo.all(Address)
end
require Logger
@doc """
Gets a single address.
Raises `Ecto.NoResultsError` if the Address does not exist.
## Examples
iex> get_address!(123)
%Address{}
iex> get_address!(456)
** (Ecto.NoResultsError)
"""
def get_address!(id), do: Repo.get!(Address, id)
@spec get_address(integer | String.t()) :: Address.t() | nil
def get_address(id), do: Repo.get(Address, id)
@doc """
Gets a single address by it's url
## Examples
iex> get_address_by_url("https://mobilizon.social/addresses/4572")
%Address{}
iex> get_address_by_url("https://mobilizon.social/addresses/099")
nil
Gets a single address.
Raises `Ecto.NoResultsError` if the address does not exist.
"""
def get_address_by_url(url) do
Repo.get_by(Address, url: url)
end
@spec get_address!(integer | String.t()) :: Address.t()
def get_address!(id), do: Repo.get!(Address, id)
@doc """
Creates a address.
## Examples
iex> create_address(%{field: value})
{:ok, %Address{}}
iex> create_address(%{field: bad_value})
{:error, %Ecto.Changeset{}}
Gets a single address by its url.
"""
@spec get_address_by_url(String.t()) :: Address.t() | nil
def get_address_by_url(url), do: Repo.get_by(Address, url: url)
@doc """
Creates an address.
"""
@spec create_address(map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def create_address(attrs \\ %{}) do
%Address{}
|> Address.changeset(attrs)
@ -89,17 +43,9 @@ defmodule Mobilizon.Addresses do
end
@doc """
Updates a address.
## Examples
iex> update_address(address, %{field: new_value})
{:ok, %Address{}}
iex> update_address(address, %{field: bad_value})
{:error, %Ecto.Changeset{}}
Updates an address.
"""
@spec update_address(Address.t(), map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def update_address(%Address{} = address, attrs) do
address
|> Address.changeset(attrs)
@ -107,131 +53,96 @@ defmodule Mobilizon.Addresses do
end
@doc """
Deletes a Address.
## Examples
iex> delete_address(address)
{:ok, %Address{}}
iex> delete_address(address)
{:error, %Ecto.Changeset{}}
Deletes an address.
"""
def delete_address(%Address{} = address) do
Repo.delete(address)
end
@spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def delete_address(%Address{} = address), do: Repo.delete(address)
@doc """
Returns an `%Ecto.Changeset{}` for tracking address changes.
## Examples
iex> change_address(address)
%Ecto.Changeset{source: %Address{}}
Returns the list of addresses.
"""
def change_address(%Address{} = address) do
Address.changeset(address, %{})
end
@spec list_addresses :: [Address.t()]
def list_addresses, do: Repo.all(Address)
@doc """
Processes raw geo data informations and return a `Geo` geometry which can be one of `Geo.Point`.
Searches addresses.
We only look at the description for now, and eventually order by object distance.
"""
# TODO: Unused, remove me
def process_geom(%{"type" => type_input, "data" => data}) do
type =
if !is_atom(type_input) && type_input != nil do
try do
String.to_existing_atom(type_input)
rescue
e in ArgumentError ->
Logger.error("#{type_input} is not an existing atom : #{inspect(e)}")
:invalid_type
end
else
type_input
end
if Enum.member?(@geom_types, type) do
case type do
:point ->
process_point(data["latitude"], data["longitude"])
end
else
{:error, :invalid_type}
end
end
@doc false
def process_geom(nil) do
{:error, nil}
end
@spec process_point(number(), number()) :: tuple()
defp process_point(latitude, longitude) when is_number(latitude) and is_number(longitude) do
{:ok, %Geo.Point{coordinates: {latitude, longitude}, srid: 4326}}
end
defp process_point(_, _) do
{:error, "Latitude and longitude must be numbers"}
end
@doc """
Search addresses in our database
We only look at the description for now, and eventually order by object distance
"""
@spec search_addresses(String.t(), list()) :: list(Address.t())
@spec search_addresses(String.t(), keyword) :: [Address.t()]
def search_addresses(search, options \\ []) do
limit = Keyword.get(options, :limit, 5)
query = from(a in Address, where: ilike(a.description, ^"%#{search}%"), limit: ^limit)
query =
if coords = Keyword.get(options, :coords, false),
do:
from(a in query,
order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")]
),
else: query
search
|> search_addresses_query(Keyword.get(options, :limit, 5))
|> order_by_coords(Keyword.get(options, :coords))
|> filter_by_contry(Keyword.get(options, :country))
query =
if country = Keyword.get(options, :country, nil),
do: from(a in query, where: ilike(a.country, ^"%#{country}%")),
else: query
if Keyword.get(options, :single, false) == true, do: Repo.one(query), else: Repo.all(query)
end
@doc """
Reverse geocode from coordinates in our database
We only take addresses 50km around and sort them by distance
"""
@spec reverse_geocode(number(), number(), list()) :: list(Address.t())
def reverse_geocode(lon, lat, options) do
limit = Keyword.get(options, :limit, 5)
radius = Keyword.get(options, :radius, 50_000)
country = Keyword.get(options, :country, nil)
srid = Keyword.get(options, :srid, 4326)
import Geo.PostGIS
with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do
query =
from(a in Address,
order_by: [fragment("? <-> ?", a.geom, ^point)],
limit: ^limit,
where: st_dwithin_in_meters(^point, a.geom, ^radius)
)
query =
if country,
do: from(a in query, where: ilike(a.country, ^"%#{country}%")),
else: query
case Keyword.get(options, :single, false) do
true ->
Repo.one(query)
false ->
Repo.all(query)
end
end
@doc """
Reverse geocode from coordinates.
We only take addresses 50km around and sort them by distance.
"""
@spec reverse_geocode(number, number, keyword) :: [Address.t()]
def reverse_geocode(lon, lat, options) do
limit = Keyword.get(options, :limit, 5)
radius = Keyword.get(options, :radius, 50_000)
country = Keyword.get(options, :country)
srid = Keyword.get(options, :srid, 4326)
with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do
point
|> addresses_around_query(radius, limit)
|> filter_by_contry(country)
|> Repo.all()
end
end
@spec search_addresses_query(String.t(), integer) :: Ecto.Query.t()
defp search_addresses_query(search, limit) do
from(
a in Address,
where: ilike(a.description, ^"%#{search}%"),
limit: ^limit
)
end
@spec order_by_coords(Ecto.Query.t(), map | nil) :: Ecto.Query.t()
defp order_by_coords(query, nil), do: query
defp order_by_coords(query, coords) do
from(
a in query,
order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")]
)
end
@spec filter_by_contry(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t()
defp filter_by_contry(query, nil), do: query
defp filter_by_contry(query, country) do
from(
a in query,
where: ilike(a.country, ^"%#{country}%")
)
end
@spec addresses_around_query(Geo.geometry(), integer, integer) :: Ecto.Query.t()
defp addresses_around_query(point, radius, limit) do
import Geo.PostGIS
from(a in Address,
where: st_dwithin_in_meters(^point, a.geom, ^radius),
order_by: [fragment("? <-> ?", a.geom, ^point)],
limit: ^limit
)
end
end

View File

@ -1,49 +0,0 @@
defmodule Mobilizon.Admin do
@moduledoc """
The Admin context.
"""
import Ecto.Query, warn: false
alias Mobilizon.Repo
import Mobilizon.Ecto
alias Mobilizon.Admin.ActionLog
@doc """
Returns the list of action_logs.
## Examples
iex> list_action_logs()
[%ActionLog{}, ...]
"""
@spec list_action_logs(integer(), integer()) :: list(ActionLog.t())
def list_action_logs(page \\ nil, limit \\ nil) do
from(
r in ActionLog,
preload: [:actor],
order_by: [desc: :id]
)
|> paginate(page, limit)
|> Repo.all()
end
@doc """
Creates a action_log.
## Examples
iex> create_action_log(%{field: value})
{:ok, %ActionLog{}}
iex> create_action_log(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_action_log(attrs \\ %{}) do
%ActionLog{}
|> ActionLog.changeset(attrs)
|> Repo.insert()
end
end

View File

@ -8,30 +8,45 @@ defenum(Mobilizon.Admin.ActionLogAction, [
defmodule Mobilizon.Admin.ActionLog do
@moduledoc """
ActionLog entity schema
Represents an action log entity.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Admin.ActionLogAction
@timestamps_opts [type: :utc_datetime]
@type t :: %__MODULE__{
action: String.t(),
target_type: String.t(),
target_id: integer,
changes: map,
actor: Actor.t()
}
@required_attrs [:action, :target_type, :target_id, :changes, :actor_id]
@attrs @required_attrs
@timestamps_opts [type: :utc_datetime]
schema "admin_action_logs" do
field(:action, ActionLogAction)
field(:target_type, :string)
field(:target_id, :integer)
field(:changes, :map)
belongs_to(:actor, Actor)
timestamps()
end
@doc false
def changeset(action_log, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = action_log, attrs) do
action_log
|> cast(attrs, @required_attrs)
|> validate_required(@required_attrs -- [:changes])
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View File

@ -0,0 +1,35 @@
defmodule Mobilizon.Admin do
@moduledoc """
The Admin context.
"""
import Ecto.Query
alias Mobilizon.Admin.ActionLog
alias Mobilizon.Storage.{Page, Repo}
@doc """
Creates a action_log.
"""
@spec create_action_log(map) :: {:ok, ActionLog.t()} | {:error, Ecto.Changeset.t()}
def create_action_log(attrs \\ %{}) do
%ActionLog{}
|> ActionLog.changeset(attrs)
|> Repo.insert()
end
@doc """
Returns the list of action logs.
"""
@spec list_action_logs(integer | nil, integer | nil) :: [ActionLog.t()]
def list_action_logs(page \\ nil, limit \\ nil) do
list_action_logs_query()
|> Page.paginate(page, limit)
|> Repo.all()
end
@spec list_action_logs_query :: Ecto.Query.t()
defp list_action_logs_query do
from(r in ActionLog, preload: [:actor], order_by: [desc: :id])
end
end

View File

@ -1,112 +0,0 @@
defmodule Mobilizon.Application do
@moduledoc """
The Mobilizon application
"""
use Application
import Cachex.Spec
alias Mobilizon.Service.Export.{Feed, ICalendar}
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(Mobilizon.Repo, []),
# Start the endpoint when the application starts
supervisor(MobilizonWeb.Endpoint, []),
# Start your own worker by calling: Mobilizon.Worker.start_link(arg1, arg2, arg3)
# worker(Mobilizon.Worker, [arg1, arg2, arg3]),
worker(
Cachex,
[
:feed,
[
limit: 2500,
expiration:
expiration(
default: :timer.minutes(60),
interval: :timer.seconds(60)
),
fallback: fallback(default: &Feed.create_cache/1)
]
],
id: :cache_feed
),
worker(
Cachex,
[
:ics,
[
limit: 2500,
expiration:
expiration(
default: :timer.minutes(60),
interval: :timer.seconds(60)
),
fallback: fallback(default: &ICalendar.create_cache/1)
]
],
id: :cache_ics
),
worker(
Cachex,
[
:statistics,
[
limit: 10,
expiration:
expiration(
default: :timer.minutes(60),
interval: :timer.seconds(60)
)
]
],
id: :cache_statistics
),
worker(
Cachex,
[
:activity_pub,
[
limit: 2500,
expiration:
expiration(
default: :timer.minutes(3),
interval: :timer.seconds(15)
)
]
],
id: :cache_activity_pub
),
worker(Guardian.DB.Token.SweeperServer, []),
worker(Mobilizon.Service.Federator, [])
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Mobilizon.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
MobilizonWeb.Endpoint.config_change(changed, removed)
:ok
end
def named_version, do: @name <> " " <> @version
def user_agent do
info =
"#{MobilizonWeb.Endpoint.url()} <#{Mobilizon.CommonConfig.get([:instance, :email], "")}>"
named_version() <> "; " <> info
end
end

View File

@ -1,71 +0,0 @@
defmodule Mobilizon.CommonConfig do
@moduledoc """
Instance configuration wrapper
"""
def registrations_open?() do
instance_config()
|> get_in([:registrations_open])
|> to_bool
end
def instance_name() do
instance_config()
|> get_in([:name])
end
def instance_description() do
instance_config()
|> get_in([:description])
end
def instance_hostname() do
instance_config()
|> get_in([:hostname])
end
def instance_config(), do: Application.get_env(:mobilizon, :instance)
defp to_bool(v), do: v == true or v == "true" or v == "True"
def get(key), do: get(key, nil)
def get([key], default), do: get(key, default)
def get([parent_key | keys], default) do
case :mobilizon
|> Application.get_env(parent_key)
|> get_in(keys) do
nil -> default
any -> any
end
end
def get(key, default) do
Application.get_env(:mobilizon, key, default)
end
def get!(key) do
value = get(key, nil)
if value == nil do
raise("Missing configuration value: #{inspect(key)}")
else
value
end
end
def put([key], value), do: put(key, value)
def put([parent_key | keys], value) do
parent =
Application.get_env(:mobilizon, parent_key)
|> put_in(keys, value)
Application.put_env(:mobilizon, parent_key, parent)
end
def put(key, value) do
Application.put_env(:mobilizon, key, value)
end
end

77
lib/mobilizon/config.ex Normal file
View File

@ -0,0 +1,77 @@
defmodule Mobilizon.Config do
@moduledoc """
Configuration wrapper.
"""
@spec instance_config :: keyword
def instance_config, do: Application.get_env(:mobilizon, :instance)
@spec instance_url :: String.t()
def instance_url, do: instance_config()[:instance]
@spec instance_name :: String.t()
def instance_name, do: instance_config()[:name]
@spec instance_description :: String.t()
def instance_description, do: instance_config()[:description]
@spec instance_version :: String.t()
def instance_version, do: instance_config()[:version]
@spec instance_hostname :: String.t()
def instance_hostname, do: instance_config()[:hostname]
@spec instance_registrations_open? :: boolean
def instance_registrations_open?, do: to_boolean(instance_config()[:registrations_open])
@spec instance_repository :: String.t()
def instance_repository, do: instance_config()[:repository]
@spec instance_email_from :: String.t()
def instance_email_from, do: instance_config()[:email_from]
@spec instance_email_reply_to :: String.t()
def instance_email_reply_to, do: instance_config()[:email_reply_to]
@spec get(module | atom) :: any
def get(key), do: get(key, nil)
@spec get([module | atom]) :: any
def get([key], default), do: get(key, default)
def get([parent_key | keys], default) do
case get_in(Application.get_env(:mobilizon, parent_key), keys) do
nil -> default
any -> any
end
end
@spec get(module | atom, any) :: any
def get(key, default), do: Application.get_env(:mobilizon, key, default)
@spec get!(module | atom) :: any
def get!(key) do
value = get(key, nil)
if value == nil do
raise("Missing configuration value: #{inspect(key)}")
else
value
end
end
@spec put([module | atom], any) :: any
def put([key], value), do: put(key, value)
def put([parent_key | keys], value) do
parent = put_in(Application.get_env(:mobilizon, parent_key), keys, value)
Application.put_env(:mobilizon, parent_key, parent)
end
@spec put(module | atom, any) :: any
def put(key, value), do: Application.put_env(:mobilizon, key, value)
@spec to_boolean(boolean | String.t()) :: boolean
defp to_boolean(boolean), do: "true" == String.downcase("#{boolean}")
end

28
lib/mobilizon/crypto.ex Normal file
View File

@ -0,0 +1,28 @@
defmodule Mobilizon.Crypto do
@moduledoc """
Utility module which contains cryptography related functions.
"""
@doc """
Returns random byte sequence of the length encoded to Base64.
"""
@spec random_string(integer) :: String.t()
def random_string(length) do
length
|> :crypto.strong_rand_bytes()
|> Base.url_encode64()
end
@doc """
Generate RSA 2048-bit private key.
"""
@spec generate_rsa_2048_private_key :: String.t()
def generate_rsa_2048_private_key do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
[entry]
|> :public_key.pem_encode()
|> String.trim_trailing()
end
end

View File

@ -1,44 +0,0 @@
defmodule Mobilizon.Ecto do
@moduledoc """
Mobilizon Ecto utils
"""
import Ecto.Query, warn: false
@doc """
Add limit and offset to the query
"""
def paginate(query, page \\ 1, size \\ 10)
def paginate(query, page, _size) when is_nil(page), do: paginate(query)
def paginate(query, page, size) when is_nil(size), do: paginate(query, page)
def paginate(query, page, size) do
from(query,
limit: ^size,
offset: ^((page - 1) * size)
)
end
@doc """
Add sort to the query
"""
def sort(query, sort, direction) do
from(
query,
order_by: [{^direction, ^sort}]
)
end
def increment_slug(slug) do
case List.pop_at(String.split(slug, "-"), -1) do
{nil, _} ->
slug
{suffix, slug_parts} ->
case Integer.parse(suffix) do
{id, _} -> Enum.join(slug_parts, "-") <> "-" <> Integer.to_string(id + 1)
:error -> slug <> "-1"
end
end
end
end

View File

@ -1,38 +0,0 @@
defmodule Mobilizon.Email.Admin do
@moduledoc """
Handles emails sent to admins
"""
alias Mobilizon.Users.User
import Bamboo.Email
import Bamboo.Phoenix
use Bamboo.Phoenix, view: Mobilizon.EmailView
import MobilizonWeb.Gettext
alias Mobilizon.Reports.Report
def report(%User{email: email} = _user, %Report{} = report, locale \\ "en") do
Gettext.put_locale(locale)
instance_url = get_config(:hostname)
base_email()
|> to(email)
|> subject(gettext("Mobilizon: New report on instance %{instance}", instance: instance_url))
|> put_header("Reply-To", get_config(:email_reply_to))
|> assign(:report, report)
|> assign(:instance, instance_url)
|> render(:report)
end
defp base_email do
# Here you can set a default from, default headers, etc.
new_email()
|> from(get_config(:email_from))
|> put_html_layout({Mobilizon.EmailView, "email.html"})
|> put_text_layout({Mobilizon.EmailView, "email.text"})
end
@spec get_config(atom()) :: any()
defp get_config(key) do
Mobilizon.CommonConfig.instance_config() |> Keyword.get(key)
end
end

View File

@ -1,57 +0,0 @@
defmodule Mobilizon.Email.User do
@moduledoc """
Handles emails sent to users
"""
alias Mobilizon.Users.User
import Bamboo.Email
import Bamboo.Phoenix
use Bamboo.Phoenix, view: Mobilizon.EmailView
import MobilizonWeb.Gettext
def confirmation_email(%User{} = user, locale \\ "en") do
Gettext.put_locale(locale)
instance_url = get_config(:instance)
base_email()
|> to(user.email)
|> subject(
gettext("Mobilizon: Confirmation instructions for %{instance}", instance: instance_url)
)
|> put_header("Reply-To", get_config(:email_reply_to))
|> assign(:token, user.confirmation_token)
|> assign(:instance, instance_url)
|> render(:registration_confirmation)
end
def reset_password_email(%User{} = user, locale \\ "en") do
Gettext.put_locale(locale)
instance_url = get_config(:hostname)
base_email()
|> to(user.email)
|> subject(
gettext(
"Mobilizon: Reset your password on %{instance} instructions",
instance: instance_url
)
)
|> put_header("Reply-To", get_config(:email_reply_to))
|> assign(:token, user.reset_password_token)
|> assign(:instance, instance_url)
|> render(:password_reset)
end
defp base_email do
# Here you can set a default from, default headers, etc.
new_email()
|> from(get_config(:email_from))
|> put_html_layout({Mobilizon.EmailView, "email.html"})
|> put_text_layout({Mobilizon.EmailView, "email.text"})
end
@spec get_config(atom()) :: any()
defp get_config(key) do
Mobilizon.CommonConfig.instance_config() |> Keyword.get(key)
end
end

View File

@ -1,33 +1,42 @@
import EctoEnum
defenum(Mobilizon.Events.CommentVisibilityEnum, :comment_visibility_type, [
:public,
:unlisted,
:private,
:moderated,
:invite
])
defmodule Mobilizon.Events.Comment do
@moduledoc """
An actor comment (for instance on an event or on a group)
Represents an actor comment (for instance on an event or on a group).
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment
alias Mobilizon.Events.{Comment, CommentVisibility, Event}
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
@type t :: %__MODULE__{
text: String.t(),
url: String.t(),
local: boolean,
visibility: CommentVisibility.t(),
uuid: Ecto.UUID.t(),
actor: Actor.t(),
attributed_to: Actor.t(),
event: Event.t(),
in_reply_to_comment: t,
origin_comment: t
}
@required_attrs [:text, :actor_id, :url]
@optional_attrs [:event_id, :in_reply_to_comment_id, :origin_comment_id, :attributed_to_id]
@attrs @required_attrs ++ @optional_attrs
schema "comments" do
field(:text, :string)
field(:url, :string)
field(:local, :boolean, default: true)
field(:visibility, Mobilizon.Events.CommentVisibilityEnum, default: :public)
field(:visibility, CommentVisibility, default: :public)
field(:uuid, Ecto.UUID)
belongs_to(:actor, Actor, foreign_key: :actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
belongs_to(:event, Event, foreign_key: :event_id)
@ -37,38 +46,27 @@ defmodule Mobilizon.Events.Comment do
timestamps(type: :utc_datetime)
end
@doc false
def changeset(comment, attrs) do
uuid =
if Map.has_key?(attrs, "uuid"),
do: attrs["uuid"],
else: Ecto.UUID.generate()
# TODO : really change me right away
url =
if Map.has_key?(attrs, "url"),
do: attrs["url"],
else: Routes.page_url(Endpoint, :comment, uuid)
comment
|> Ecto.Changeset.cast(attrs, [
:url,
:text,
:actor_id,
:event_id,
:in_reply_to_comment_id,
:origin_comment_id,
:attributed_to_id
])
|> put_change(:uuid, uuid)
|> put_change(:url, url)
|> validate_required([:text, :actor_id, :url])
end
@doc """
Returns the id of the first comment in the conversation
Returns the id of the first comment in the conversation.
"""
def get_thread_id(%Comment{id: id, origin_comment_id: origin_comment_id}) do
@spec get_thread_id(t) :: integer
def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do
origin_comment_id || id
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = comment, attrs) do
uuid = attrs["uuid"] || Ecto.UUID.generate()
url = attrs["url"] || generate_url(uuid)
comment
|> cast(attrs, @attrs)
|> put_change(:uuid, uuid)
|> put_change(:url, url)
|> validate_required(@required_attrs)
end
@spec generate_url(String.t()) :: String.t()
defp generate_url(uuid), do: Routes.page_url(Endpoint, :comment, uuid)
end

View File

@ -1,43 +1,87 @@
import EctoEnum
defenum(Mobilizon.Events.EventVisibilityEnum, :event_visibility_type, [
:public,
:unlisted,
:restricted,
:private
])
defenum(Mobilizon.Events.JoinOptionsEnum, :event_join_options_type, [
:free,
:restricted,
:invite
])
defenum(Mobilizon.Events.EventStatusEnum, :event_status_type, [
:tentative,
:confirmed,
:cancelled
])
defenum(Mobilizon.Event.EventCategoryEnum, :event_category_type, [
:business,
:conference,
:birthday,
:demonstration,
:meeting
])
defmodule Mobilizon.Events.Event do
@moduledoc """
Represents an event
Represents an event.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.{Event, Participant, Tag, Session, Track}
alias Mobilizon.Actors.Actor
alias Mobilizon.Media.Picture
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.{
EventOptions,
EventStatus,
EventVisibility,
JoinOptions,
Participant,
Tag,
Session,
Track
}
alias Mobilizon.Media.Picture
@type t :: %__MODULE__{
url: String.t(),
local: boolean,
begins_on: DateTime.t(),
slug: String.t(),
description: String.t(),
ends_on: DateTime.t(),
title: String.t(),
status: EventStatus.t(),
visibility: EventVisibility.t(),
join_options: JoinOptions.t(),
publish_at: DateTime.t(),
uuid: Ecto.UUID.t(),
online_address: String.t(),
phone_address: String.t(),
category: String.t(),
options: EventOptions.t(),
organizer_actor: Actor.t(),
attributed_to: Actor.t(),
physical_address: Address.t(),
picture: Picture.t(),
tracks: [Track.t()],
sessions: [Session.t()],
tags: [Tag.t()],
participants: [Actor.t()]
}
@required_attrs [:title, :begins_on, :organizer_actor_id, :url, :uuid]
@optional_attrs [
:slug,
:description,
:ends_on,
:category,
:status,
:visibility,
:publish_at,
:online_address,
:phone_address,
:picture_id,
:physical_address_id
]
@attrs @required_attrs ++ @optional_attrs
@update_required_attrs @required_attrs
@update_optional_attrs [
:slug,
:description,
:ends_on,
:category,
:status,
:visibility,
:publish_at,
:online_address,
:phone_address,
:picture_id,
:physical_address_id
]
@update_attrs @update_required_attrs ++ @update_optional_attrs
schema "events" do
field(:url, :string)
field(:local, :boolean, default: true)
@ -46,96 +90,59 @@ defmodule Mobilizon.Events.Event do
field(:description, :string)
field(:ends_on, :utc_datetime)
field(:title, :string)
field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed)
field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public)
field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free)
field(:status, EventStatus, default: :confirmed)
field(:visibility, EventVisibility, default: :public)
field(:join_options, JoinOptions, default: :free)
field(:publish_at, :utc_datetime)
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
field(:online_address, :string)
field(:phone_address, :string)
field(:category, :string)
embeds_one(:options, Mobilizon.Events.EventOptions, on_replace: :update)
embeds_one(:options, EventOptions, on_replace: :update)
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
many_to_many(:participants, Actor, join_through: Participant)
has_many(:tracks, Track)
has_many(:sessions, Session)
belongs_to(:physical_address, Address)
belongs_to(:picture, Picture)
has_many(:tracks, Track)
has_many(:sessions, Session)
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
many_to_many(:participants, Actor, join_through: Participant)
timestamps(type: :utc_datetime)
end
@doc false
def changeset(%Event{} = event, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = event, attrs) do
event
|> Ecto.Changeset.cast(attrs, [
:title,
:slug,
:description,
:url,
:begins_on,
:ends_on,
:organizer_actor_id,
:category,
:status,
:visibility,
:publish_at,
:online_address,
:phone_address,
:uuid,
:picture_id,
:physical_address_id
])
|> cast(attrs, @attrs)
|> cast_embed(:options)
|> validate_required([
:title,
:begins_on,
:organizer_actor_id,
:url,
:uuid
])
|> validate_required(@required_attrs)
end
@doc false
def update_changeset(%Event{} = event, attrs) do
@spec update_changeset(t, map) :: Ecto.Changeset.t()
def update_changeset(%__MODULE__{} = event, attrs) do
event
|> Ecto.Changeset.cast(attrs, [
:title,
:slug,
:description,
:begins_on,
:ends_on,
:category,
:status,
:visibility,
:publish_at,
:online_address,
:phone_address,
:picture_id,
:physical_address_id
])
|> Ecto.Changeset.cast(attrs, @update_attrs)
|> cast_embed(:options)
|> put_tags(attrs)
|> validate_required([
:title,
:begins_on,
:organizer_actor_id,
:url,
:uuid
])
|> validate_required(@update_required_attrs)
end
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
defp put_tags(changeset, _), do: changeset
def can_event_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id)
@doc """
Checks whether an event can be managed.
"""
@spec can_be_managed_by(t, integer | String.t()) :: boolean
def can_be_managed_by(%__MODULE__{organizer_actor_id: organizer_actor_id}, actor_id)
when organizer_actor_id == actor_id do
{:event_can_be_managed, true}
end
def can_event_be_managed_by(_event, _actor) do
{:event_can_be_managed, false}
end
def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false}
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
defp put_tags(changeset, _), do: changeset
end

View File

@ -0,0 +1,19 @@
defmodule Mobilizon.Events.EventOffer do
@moduledoc """
Represents an event offer.
"""
use Ecto.Schema
@type t :: %__MODULE__{
price: float,
price_currency: String.t(),
url: String.t()
}
embedded_schema do
field(:price, :float)
field(:price_currency, :string)
field(:url, :string)
end
end

View File

@ -1,63 +1,31 @@
import EctoEnum
defenum(Mobilizon.Events.CommentModeration, :comment_moderation, [:allow_all, :moderated, :closed])
defmodule Mobilizon.Events.EventOffer do
@moduledoc """
Represents an event offer
"""
use Ecto.Schema
embedded_schema do
field(:price, :float)
field(:price_currency, :string)
field(:url, :string)
end
end
defmodule Mobilizon.Events.EventParticipationCondition do
@moduledoc """
Represents an event participation condition
"""
use Ecto.Schema
embedded_schema do
field(:title, :string)
field(:content, :string)
field(:url, :string)
end
end
defmodule Mobilizon.Events.EventOptions do
@moduledoc """
Represents an event options
Represents an event options.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.{
EventOptions,
EventOffer,
EventParticipationCondition,
CommentModeration
}
@primary_key false
@derive Jason.Encoder
embedded_schema do
field(:maximum_attendee_capacity, :integer)
field(:remaining_attendee_capacity, :integer)
field(:show_remaining_attendee_capacity, :boolean)
embeds_many(:offers, EventOffer)
embeds_many(:participation_condition, EventParticipationCondition)
field(:attendees, {:array, :string})
field(:program, :string)
field(:comment_moderation, CommentModeration)
field(:show_participation_price, :boolean)
end
@type t :: %__MODULE__{
maximum_attendee_capacity: integer,
remaining_attendee_capacity: integer,
show_remaining_attendee_capacity: boolean,
attendees: [String.t()],
program: String.t(),
comment_moderation: CommentModeration.t(),
show_participation_price: boolean,
offers: [EventOffer.t()],
participation_condition: [EventParticipationCondition.t()]
}
def changeset(%EventOptions{} = event_options, attrs) do
event_options
|> Ecto.Changeset.cast(attrs, [
@attrs [
:maximum_attendee_capacity,
:remaining_attendee_capacity,
:show_remaining_attendee_capacity,
@ -65,6 +33,26 @@ defmodule Mobilizon.Events.EventOptions do
:program,
:comment_moderation,
:show_participation_price
])
]
@primary_key false
@derive Jason.Encoder
embedded_schema do
field(:maximum_attendee_capacity, :integer)
field(:remaining_attendee_capacity, :integer)
field(:show_remaining_attendee_capacity, :boolean)
field(:attendees, {:array, :string})
field(:program, :string)
field(:comment_moderation, CommentModeration)
field(:show_participation_price, :boolean)
embeds_many(:offers, EventOffer)
embeds_many(:participation_condition, EventParticipationCondition)
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = event_options, attrs) do
cast(event_options, attrs, @attrs)
end
end

View File

@ -0,0 +1,19 @@
defmodule Mobilizon.Events.EventParticipationCondition do
@moduledoc """
Represents an event participation condition.
"""
use Ecto.Schema
@type t :: %__MODULE__{
title: String.t(),
content: String.t(),
url: String.t()
}
embedded_schema do
field(:title, :string)
field(:content, :string)
field(:url, :string)
end
end

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,29 @@
defmodule Mobilizon.Events.FeedToken do
@moduledoc """
Represents a Token for a Feed of events
Represents a token for a feed of events.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.FeedToken
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
@type t :: %__MODULE__{
token: Ecto.UUID.t(),
actor: Actor.t(),
user: User.t()
}
@required_attrs [:token, :user_id]
@optional_attrs [:actor_id]
@attrs @required_attrs ++ @optional_attrs
@primary_key false
schema "feed_tokens" do
field(:token, Ecto.UUID, primary_key: true)
belongs_to(:actor, Actor)
belongs_to(:user, User)
@ -18,9 +31,10 @@ defmodule Mobilizon.Events.FeedToken do
end
@doc false
def changeset(%FeedToken{} = feed_token, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = feed_token, attrs) do
feed_token
|> Ecto.Changeset.cast(attrs, [:token, :actor_id, :user_id])
|> validate_required([:token, :user_id])
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View File

@ -1,78 +1,87 @@
import EctoEnum
defenum(Mobilizon.Events.ParticipantRoleEnum, :participant_role_type, [
:not_approved,
:participant,
:moderator,
:administrator,
:creator
])
defmodule Mobilizon.Events.Participant do
@moduledoc """
Represents a participant, an actor participating to an event
Represents a participant, an actor participating to an event.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.{Participant, Event}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Event, ParticipantRole}
alias MobilizonWeb.Endpoint
@type t :: %__MODULE__{
role: ParticipantRole.t(),
url: String.t(),
event: Event.t(),
actor: Actor.t()
}
@required_attrs [:url, :role, :event_id, :actor_id]
@attrs @required_attrs
@primary_key {:id, :binary_id, autogenerate: true}
schema "participants" do
field(:role, Mobilizon.Events.ParticipantRoleEnum, default: :participant)
field(:role, ParticipantRole, default: :participant)
field(:url, :string)
belongs_to(:event, Event, primary_key: true)
belongs_to(:actor, Actor, primary_key: true)
timestamps()
end
@doc false
def changeset(%Participant{} = participant, attrs) do
participant
|> Ecto.Changeset.cast(attrs, [:url, :role, :event_id, :actor_id])
|> generate_url()
|> validate_required([:url, :role, :event_id, :actor_id])
end
# If there's a blank URL that's because we're doing the first insert
defp generate_url(%Ecto.Changeset{data: %Participant{url: nil}} = changeset) do
case fetch_change(changeset, :url) do
{:ok, _url} -> changeset
:error -> do_generate_url(changeset)
end
end
# Most time just go with the given URL
defp generate_url(%Ecto.Changeset{} = changeset), do: changeset
defp do_generate_url(%Ecto.Changeset{} = changeset) do
uuid = Ecto.UUID.generate()
changeset
|> put_change(
:url,
"#{MobilizonWeb.Endpoint.url()}/join/event/#{uuid}"
)
|> put_change(
:id,
uuid
)
end
@doc """
We check that the actor asking to leave the event is not it's only organizer
We check that the actor asking to leave the event is not it's only organizer.
We start by fetching the list of organizers and if there's only one of them
and that it's the actor requesting leaving the event we return true
and that it's the actor requesting leaving the event we return true.
"""
@spec check_that_participant_is_not_only_organizer(integer(), integer()) :: boolean()
def check_that_participant_is_not_only_organizer(event_id, actor_id) do
case Mobilizon.Events.list_organizers_participants_for_event(event_id) do
[%Participant{actor: %Actor{id: participant_actor_id}}] ->
@spec is_not_only_organizer(integer | String.t(), integer | String.t()) :: boolean
def is_not_only_organizer(event_id, actor_id) do
case Events.list_organizers_participants_for_event(event_id) do
[%__MODULE__{actor: %Actor{id: participant_actor_id}}] ->
participant_actor_id == actor_id
_ ->
false
end
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = participant, attrs) do
participant
|> cast(attrs, @attrs)
|> ensure_url()
|> validate_required(@required_attrs)
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
case fetch_change(changeset, :url) do
{:ok, _url} ->
changeset
:error ->
update_url(changeset)
end
end
defp ensure_url(%Ecto.Changeset{} = changeset), do: changeset
defp update_url(%Ecto.Changeset{} = changeset) do
uuid = Ecto.UUID.generate()
url = generate_url(uuid)
changeset
|> put_change(:id, uuid)
|> put_change(:url, url)
end
@spec generate_url(String.t()) :: String.t()
defp generate_url(uuid), do: "#{Endpoint.url()}/join/event/#{uuid}"
end

View File

@ -1,10 +1,41 @@
defmodule Mobilizon.Events.Session do
@moduledoc """
Represents a session for an event (such as a talk at a conference)
Represents a session for an event (such as a talk at a conference).
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.{Session, Event, Track}
alias Mobilizon.Events.{Event, Track}
@type t :: %__MODULE__{
audios_urls: String.t(),
language: String.t(),
long_abstract: String.t(),
short_abstract: String.t(),
slides_url: String.t(),
subtitle: String.t(),
title: String.t(),
videos_urls: String.t(),
begins_on: DateTime.t(),
ends_on: DateTime.t(),
event: Event.t(),
track: Track.t()
}
@required_attrs [
:title,
:subtitle,
:short_abstract,
:long_abstract,
:language,
:slides_url,
:videos_urls,
:audios_urls
]
@optional_attrs [:event_id, :track_id]
@attrs @required_attrs ++ @optional_attrs
schema "sessions" do
field(:audios_urls, :string)
@ -17,6 +48,7 @@ defmodule Mobilizon.Events.Session do
field(:videos_urls, :string)
field(:begins_on, :utc_datetime)
field(:ends_on, :utc_datetime)
belongs_to(:event, Event)
belongs_to(:track, Track)
@ -24,29 +56,10 @@ defmodule Mobilizon.Events.Session do
end
@doc false
def changeset(%Session{} = session, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = session, attrs) do
session
|> cast(attrs, [
:title,
:subtitle,
:short_abstract,
:long_abstract,
:language,
:slides_url,
:videos_urls,
:audios_urls,
:event_id,
:track_id
])
|> validate_required([
:title,
:subtitle,
:short_abstract,
:long_abstract,
:language,
:slides_url,
:videos_urls,
:audios_urls
])
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View File

@ -1,60 +1,40 @@
defmodule Mobilizon.Events.Tag.TitleSlug do
@moduledoc """
Generates slugs for tags
"""
alias Mobilizon.Events.Tag
import Ecto.Query
alias Mobilizon.Repo
use EctoAutoslugField.Slug, from: :title, to: :slug
def build_slug(sources, changeset) do
slug = super(sources, changeset)
build_unique_slug(slug, changeset)
end
defp build_unique_slug(slug, changeset) do
query =
from(
t in Tag,
where: t.slug == ^slug
)
case Repo.one(query) do
nil ->
slug
_tag ->
slug
|> Mobilizon.Ecto.increment_slug()
|> build_unique_slug(changeset)
end
end
end
defmodule Mobilizon.Events.Tag do
@moduledoc """
Represents a tag for events
Represents a tag for events.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.Tag
alias Mobilizon.Events.Tag.TitleSlug
alias Mobilizon.Events.TagRelation
alias Mobilizon.Events.Tag.TitleSlug
@type t :: %__MODULE__{
title: String.t(),
slug: TitleSlug.Type.t(),
related_tags: [t]
}
@required_attrs [:title, :slug]
@attrs @required_attrs
schema "tags" do
field(:title, :string)
field(:slug, TitleSlug.Type)
many_to_many(:related_tags, Tag, join_through: TagRelation)
many_to_many(:related_tags, __MODULE__, join_through: TagRelation)
timestamps()
end
@doc false
def changeset(%Tag{} = tag, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = tag, attrs) do
tag
|> cast(attrs, [:title])
|> cast(attrs, @attrs)
|> TitleSlug.maybe_generate_slug()
|> validate_required([:title, :slug])
|> validate_required(@required_attrs)
|> TitleSlug.unique_constraint()
end
end

View File

@ -0,0 +1,53 @@
defmodule Mobilizon.Events.Tag.TitleSlug do
@moduledoc """
Generates slugs for tags.
"""
use EctoAutoslugField.Slug, from: :title, to: :slug
alias Mobilizon.Events
@slug_separator "-"
@doc """
Builds a slug.
"""
@spec build_slug(keyword, Ecto.Changeset.t()) :: String.t()
def build_slug(sources, changeset) do
slug = super(sources, changeset)
build_unique_slug(slug, changeset)
end
@spec build_unique_slug(String.t(), Ecto.Changeset.t()) :: String.t()
defp build_unique_slug(slug, changeset) do
case Events.get_tag_by_slug(slug) do
nil ->
slug
_tag ->
slug
|> increment_slug()
|> build_unique_slug(changeset)
end
end
@spec increment_slug(String.t()) :: String.t()
defp increment_slug(slug) do
case List.pop_at(String.split(slug, @slug_separator), -1) do
{nil, _} ->
slug
{suffix, slug_parts} ->
case Integer.parse(suffix) do
{id, _} ->
Enum.join(slug_parts, @slug_separator) <>
@slug_separator <>
Integer.to_string(id + 1)
:error ->
"#{slug}#{@slug_separator}1"
end
end
end
end

View File

@ -0,0 +1,48 @@
defmodule Mobilizon.Events.TagRelation do
@moduledoc """
Represents a tag relation.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.Tag
@type t :: %__MODULE__{
weight: integer,
tag: Tag.t(),
link: Tag.t()
}
@required_attrs [:tag_id, :link_id]
@optional_attrs [:weight]
@attrs @required_attrs ++ @optional_attrs
@primary_key false
schema "tag_relations" do
field(:weight, :integer, default: 1)
belongs_to(:tag, Tag, primary_key: true)
belongs_to(:link, Tag, primary_key: true)
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = tag, attrs) do
# Return if tag_id or link_id are not set because it will fail later otherwise
with %Ecto.Changeset{errors: [], changes: changes} = changeset <-
tag
|> cast(attrs, @attrs)
|> validate_required(@required_attrs) do
changeset
|> put_change(:tag_id, min(changes.tag_id, changes.link_id))
|> put_change(:link_id, max(changes.tag_id, changes.link_id))
|> unique_constraint(:tag_id, name: :tag_relations_pkey)
|> check_constraint(:tag_id,
name: :no_self_loops_check,
message: "Can't add a relation on self"
)
end
end
end

View File

@ -1,41 +0,0 @@
defmodule Mobilizon.Events.TagRelation do
@moduledoc """
Represents a tag for events
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.Tag
alias Mobilizon.Events.TagRelation
@primary_key false
schema "tag_relations" do
belongs_to(:tag, Tag, primary_key: true)
belongs_to(:link, Tag, primary_key: true)
field(:weight, :integer, default: 1)
end
@doc false
def changeset(%TagRelation{} = tag, attrs) do
changeset =
tag
|> cast(attrs, [:tag_id, :link_id, :weight])
|> validate_required([:tag_id, :link_id])
# Return if tag_id or link_id are not set because it will fail later otherwise
with %Ecto.Changeset{errors: []} <- changeset do
changes = changeset.changes
changeset =
changeset
|> put_change(:tag_id, min(changes.tag_id, changes.link_id))
|> put_change(:link_id, max(changes.tag_id, changes.link_id))
changeset
|> unique_constraint(:tag_id, name: :tag_relations_pkey)
|> check_constraint(:tag_id,
name: :no_self_loops_check,
message: "Can't add a relation on self"
)
end
end
end

View File

@ -1,15 +1,31 @@
defmodule Mobilizon.Events.Track do
@moduledoc """
Represents a track for an event (such as a theme) having multiple sessions
Represents a track for an event (such as a theme) having multiple sessions.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.{Track, Event, Session}
alias Mobilizon.Events.{Event, Session}
@type t :: %__MODULE__{
color: String.t(),
description: String.t(),
name: String.t(),
event: Event.t(),
sessions: [Session.t()]
}
@required_attrs [:name, :description, :color]
@optional_attrs [:event_id]
@attrs @required_attrs ++ @optional_attrs
schema "tracks" do
field(:color, :string)
field(:description, :string)
field(:name, :string)
belongs_to(:event, Event)
has_many(:sessions, Session)
@ -17,9 +33,10 @@ defmodule Mobilizon.Events.Track do
end
@doc false
def changeset(%Track{} = track, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = track, attrs) do
track
|> cast(attrs, [:name, :description, :color, :event_id])
|> validate_required([:name, :description, :color])
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View File

@ -1,125 +0,0 @@
defmodule Mobilizon.Media do
@moduledoc """
The Media context.
"""
import Ecto.Query, warn: false
alias Mobilizon.Repo
alias Mobilizon.Media.Picture
alias Mobilizon.Media.File
alias Ecto.Multi
@doc false
def data() do
Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2)
end
@doc false
def query(queryable, _params) do
queryable
end
@doc """
Gets a single picture.
Raises `Ecto.NoResultsError` if the Picture does not exist.
## Examples
iex> get_picture!(123)
%Picture{}
iex> get_picture!(456)
** (Ecto.NoResultsError)
"""
def get_picture!(id), do: Repo.get!(Picture, id)
def get_picture(id), do: Repo.get(Picture, id)
@doc """
Get a picture by it's URL
"""
@spec get_picture_by_url(String.t()) :: Picture.t() | nil
def get_picture_by_url(url) do
from(
p in Picture,
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
)
|> Repo.one()
end
@doc """
Creates a picture.
## Examples
iex> create_picture(%{field: value})
{:ok, %Picture{}}
iex> create_picture(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_picture(attrs \\ %{}) do
%Picture{}
|> Picture.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a picture.
## Examples
iex> update_picture(picture, %{field: new_value})
{:ok, %Picture{}}
iex> update_picture(picture, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_picture(%Picture{} = picture, attrs) do
picture
|> Picture.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a Picture.
## Examples
iex> delete_picture(picture)
{:ok, %Picture{}}
iex> delete_picture(picture)
{:error, %Ecto.Changeset{}}
"""
def delete_picture(%Picture{} = picture) do
case Multi.new()
|> Multi.delete(:picture, picture)
|> Multi.run(:remove, fn _repo, %{picture: %Picture{file: %File{url: url}}} = _picture ->
MobilizonWeb.Upload.remove(url)
end)
|> Repo.transaction() do
{:ok, %{picture: %Picture{} = picture}} -> {:ok, picture}
{:error, :remove, error, _} -> {:error, error}
end
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking picture changes.
## Examples
iex> change_picture(picture)
%Ecto.Changeset{source: %Picture{}}
"""
def change_picture(%Picture{} = picture) do
Picture.changeset(picture, %{})
end
end

View File

@ -1,9 +1,22 @@
defmodule Mobilizon.Media.File do
@moduledoc """
Represents a file entity
Represents a file entity.
"""
use Ecto.Schema
import Ecto.Changeset
import Ecto.Changeset, only: [cast: 3, validate_required: 2]
@type t :: %__MODULE__{
name: String.t(),
url: String.t(),
content_type: String.t(),
size: integer
}
@required_attrs [:name, :url]
@optional_attrs [:content_type, :size]
@attrs @required_attrs ++ @optional_attrs
embedded_schema do
field(:name, :string)
@ -15,9 +28,10 @@ defmodule Mobilizon.Media.File do
end
@doc false
def changeset(picture, attrs) do
picture
|> cast(attrs, [:name, :url, :content_type, :size])
|> validate_required([:name, :url])
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = file, attrs) do
file
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View File

@ -0,0 +1,85 @@
defmodule Mobilizon.Media do
@moduledoc """
The Media context.
"""
import Ecto.Query
alias Ecto.Multi
alias Mobilizon.Media.{File, Picture}
alias Mobilizon.Storage.Repo
@doc """
Gets a single picture.
"""
@spec get_picture(integer | String.t()) :: Picture.t() | nil
def get_picture(id), do: Repo.get(Picture, id)
@doc """
Gets a single picture.
Raises `Ecto.NoResultsError` if the picture does not exist.
"""
@spec get_picture!(integer | String.t()) :: Picture.t()
def get_picture!(id), do: Repo.get!(Picture, id)
@doc """
Get a picture by it's URL.
"""
@spec get_picture_by_url(String.t()) :: Picture.t() | nil
def get_picture_by_url(url) do
url
|> picture_by_url_query()
|> Repo.one()
end
@doc """
Creates a picture.
"""
@spec create_picture(map) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()}
def create_picture(attrs \\ %{}) do
%Picture{}
|> Picture.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a picture.
"""
@spec update_picture(Picture.t(), map) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()}
def update_picture(%Picture{} = picture, attrs) do
picture
|> Picture.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a picture.
"""
@spec delete_picture(Picture.t()) :: {:ok, Picture.t()} | {:error, Ecto.Changeset.t()}
def delete_picture(%Picture{} = picture) do
transaction =
Multi.new()
|> Multi.delete(:picture, picture)
|> Multi.run(:remove, fn _repo, %{picture: %Picture{file: %File{url: url}}} ->
MobilizonWeb.Upload.remove(url)
end)
|> Repo.transaction()
case transaction do
{:ok, %{picture: %Picture{} = picture}} ->
{:ok, picture}
{:error, :remove, error, _} ->
{:error, error}
end
end
@spec picture_by_url_query(String.t()) :: Ecto.Query.t()
defp picture_by_url_query(url) do
from(
p in Picture,
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
)
end
end

View File

@ -1,11 +1,19 @@
defmodule Mobilizon.Media.Picture do
@moduledoc """
Represents a picture entity
Represents a picture entity.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Media.File
import Ecto.Changeset, only: [cast: 3, cast_embed: 2]
alias Mobilizon.Actors.Actor
alias Mobilizon.Media.File
@type t :: %__MODULE__{
file: File.t(),
actor: Actor.t()
}
schema "pictures" do
embeds_one(:file, File, on_replace: :update)
@ -15,7 +23,8 @@ defmodule Mobilizon.Media.Picture do
end
@doc false
def changeset(picture, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = picture, attrs) do
picture
|> cast(attrs, [:actor_id])
|> cast_embed(:file)

View File

@ -1,5 +0,0 @@
Postgrex.Types.define(
Mobilizon.PostgresTypes,
[Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(),
json: Jason
)

View File

@ -1,248 +0,0 @@
defmodule Mobilizon.Reports do
@moduledoc """
The Reports context.
"""
import Ecto.Query, warn: false
alias Mobilizon.Repo
import Mobilizon.Ecto
alias Mobilizon.Reports.Report
alias Mobilizon.Reports.Note
@doc false
def data() do
Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2)
end
@doc false
def query(queryable, _params) do
queryable
end
@doc """
Returns the list of reports.
## Examples
iex> list_reports()
[%Report{}, ...]
"""
@spec list_reports(integer(), integer(), atom(), atom()) :: list(Report.t())
def list_reports(
page \\ nil,
limit \\ nil,
sort \\ :updated_at,
direction \\ :desc,
status \\ :open
) do
from(
r in Report,
preload: [:reported, :reporter, :manager, :event, :comments, :notes],
where: r.status == ^status
)
|> paginate(page, limit)
|> sort(sort, direction)
|> Repo.all()
end
def count_opened_reports() do
query = from(r in Report, where: r.status == ^:open)
Repo.aggregate(query, :count, :id)
end
@doc """
Gets a single report.
Raises `Ecto.NoResultsError` if the Report does not exist.
## Examples
iex> get_report!(123)
%Report{}
iex> get_report!(456)
** (Ecto.NoResultsError)
"""
def get_report!(id) do
with %Report{} = report <- Repo.get!(Report, id) do
Repo.preload(report, [:reported, :reporter, :manager, :event, :comments, :notes])
end
end
@doc """
Gets a single report.
Returns `nil` if the Report does not exist.
## Examples
iex> get_report(123)
%Report{}
iex> get_report(456)
nil
"""
def get_report(id) do
with %Report{} = report <- Repo.get(Report, id) do
Repo.preload(report, [:reported, :reporter, :manager, :event, :comments, :notes])
end
end
@doc """
Get a report by it's URL
"""
@spec get_report_by_url(String.t()) :: Report.t() | nil
def get_report_by_url(url) do
from(
r in Report,
where: r.uri == ^url
)
|> Repo.one()
end
@doc """
Creates a report.
## Examples
iex> create_report(%{field: value})
{:ok, %Report{}}
iex> create_report(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_report(attrs \\ %{}) do
with {:ok, %Report{} = report} <-
%Report{}
|> Report.creation_changeset(attrs)
|> Repo.insert() do
{:ok, Repo.preload(report, [:event, :reported, :reporter, :comments])}
end
end
@doc """
Updates a report.
## Examples
iex> update_report(report, %{field: new_value})
{:ok, %Report{}}
iex> update_report(report, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_report(%Report{} = report, attrs) do
report
|> Report.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a Report.
## Examples
iex> delete_report(report)
{:ok, %Report{}}
iex> delete_report(report)
{:error, %Ecto.Changeset{}}
"""
def delete_report(%Report{} = report) do
Repo.delete(report)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking report changes.
## Examples
iex> change_report(report)
%Ecto.Changeset{source: %Report{}}
"""
def change_report(%Report{} = report) do
Report.changeset(report, %{})
end
@doc """
Returns the list of notes for a report.
## Examples
iex> list_notes_for_report(%Report{id: 1})
[%Note{}, ...]
"""
@spec list_notes_for_report(Report.t()) :: list(Report.t())
def list_notes_for_report(%Report{id: report_id}) do
from(
n in Note,
where: n.report_id == ^report_id,
preload: [:report, :moderator]
)
|> Repo.all()
end
@doc """
Gets a single note.
Raises `Ecto.NoResultsError` if the Note does not exist.
## Examples
iex> get_note!(123)
%Note{}
iex> get_note!(456)
** (Ecto.NoResultsError)
"""
def get_note!(id), do: Repo.get!(Note, id)
def get_note(id), do: Repo.get(Note, id)
@doc """
Creates a note report.
## Examples
iex> create_report_note(%{field: value})
{:ok, %Note{}}
iex> create_report_note(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_report_note(attrs \\ %{}) do
with {:ok, %Note{} = note} <-
%Note{}
|> Note.changeset(attrs)
|> Repo.insert() do
{:ok, Repo.preload(note, [:report, :moderator])}
end
end
@doc """
Deletes a note report.
## Examples
iex> delete_report_note(note)
{:ok, %Note{}}
iex> delete_report_note(note)
{:error, %Ecto.Changeset{}}
"""
def delete_report_note(%Note{} = note) do
Repo.delete(note)
end
end

View File

@ -1,28 +1,41 @@
defmodule Mobilizon.Reports.Note do
@moduledoc """
Report Note entity
Represents a note entity.
"""
use Ecto.Schema
import Ecto.Changeset
import Ecto.Changeset, only: [cast: 3, validate_required: 2]
alias Mobilizon.Actors.Actor
alias Mobilizon.Reports.Report
@required_attrs [:content, :moderator_id, :report_id]
@attrs @required_attrs
@timestamps_opts [type: :utc_datetime]
@attrs [:content, :moderator_id, :report_id]
@type t :: %__MODULE__{
content: String.t(),
report: Report.t(),
moderator: Actor.t()
}
@derive {Jason.Encoder, only: [:content]}
schema "report_notes" do
field(:content, :string)
belongs_to(:moderator, Actor)
belongs_to(:report, Report)
belongs_to(:moderator, Actor)
timestamps()
end
@doc false
def changeset(note, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = note, attrs) do
note
|> cast(attrs, @attrs)
|> validate_required(@attrs)
|> validate_required(@required_attrs)
end
end

View File

@ -1,45 +1,50 @@
import EctoEnum
defenum(Mobilizon.Reports.ReportStateEnum, :report_state, [
:open,
:closed,
:resolved
])
defmodule Mobilizon.Reports.Report do
@moduledoc """
Report entity
Represents a report entity.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor
alias Mobilizon.Reports.Note
alias Mobilizon.Events.{Comment, Event}
alias Mobilizon.Reports.{Note, ReportStatus}
@type t :: %__MODULE__{
content: String.t(),
status: ReportStatus.t(),
uri: String.t(),
reported: Actor.t(),
reporter: Actor.t(),
manager: Actor.t(),
event: Event.t(),
comments: [Comment.t()],
notes: [Note.t()]
}
@required_attrs [:uri, :reported_id, :reporter_id]
@optional_attrs [:content, :status, :manager_id, :event_id]
@attrs @required_attrs ++ @optional_attrs
@timestamps_opts [type: :utc_datetime]
@derive {Jason.Encoder, only: [:status, :uri]}
schema "reports" do
field(:content, :string)
field(:status, Mobilizon.Reports.ReportStateEnum, default: :open)
field(:status, ReportStatus, default: :open)
field(:uri, :string)
# The reported actor
belongs_to(:reported, Actor)
# The actor who reported
belongs_to(:reporter, Actor)
# The actor who last acted on this report
belongs_to(:manager, Actor)
# The eventual Event inside the report
belongs_to(:event, Event)
# The eventual Comments inside the report
many_to_many(:comments, Comment, join_through: "reports_comments", on_replace: :delete)
# The notes associated to the report
has_many(:notes, Note, foreign_key: :report_id)
@ -47,13 +52,16 @@ defmodule Mobilizon.Reports.Report do
end
@doc false
def changeset(report, attrs) do
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = report, attrs) do
report
|> cast(attrs, [:content, :status, :uri, :reported_id, :reporter_id, :manager_id, :event_id])
|> validate_required([:uri, :reported_id, :reporter_id])
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
def creation_changeset(report, attrs) do
@doc false
@spec creation_changeset(t, map) :: Ecto.Changeset.t()
def creation_changeset(%__MODULE__{} = report, attrs) do
report
|> changeset(attrs)
|> put_assoc(:comments, attrs["comments"])

View File

@ -0,0 +1,171 @@
defmodule Mobilizon.Reports do
@moduledoc """
The Reports context.
"""
import Ecto.Query
import EctoEnum
import Mobilizon.Storage.Ecto
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Storage.{Page, Repo}
defenum(ReportStatus, :report_status, [:open, :closed, :resolved])
@doc """
Gets a single report.
"""
@spec get_report(integer | String.t()) :: Report.t() | nil
def get_report(id) do
Report
|> Repo.get(id)
|> Repo.preload([:reported, :reporter, :manager, :event, :comments, :notes])
end
@doc """
Gets a single report.
Raises `Ecto.NoResultsError` if the report does not exist.
"""
@spec get_report!(integer | String.t()) :: Report.t()
def get_report!(id) do
Report
|> Repo.get!(id)
|> Repo.preload([:reported, :reporter, :manager, :event, :comments, :notes])
end
@doc """
Get a report by its URL
"""
@spec get_report_by_url(String.t()) :: Report.t() | nil
def get_report_by_url(url) do
url
|> report_by_url_query()
|> Repo.one()
end
@doc """
Creates a report.
"""
@spec create_report(map) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()}
def create_report(attrs \\ %{}) do
with {:ok, %Report{} = report} <-
%Report{}
|> Report.changeset(attrs)
|> Repo.insert() do
{:ok, Repo.preload(report, [:event, :reported, :reporter, :comments])}
end
end
@doc """
Updates a report.
"""
@spec update_report(Report.t(), map) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()}
def update_report(%Report{} = report, attrs) do
report
|> Report.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a report.
"""
@spec delete_report(Report.t()) :: {:ok, Report.t()} | {:error, Ecto.Changeset.t()}
def delete_report(%Report{} = report), do: Repo.delete(report)
@doc """
Returns the list of reports.
"""
@spec list_reports(integer | nil, integer | nil, atom, atom, ReportStatus) :: [Report.t()]
def list_reports(
page \\ nil,
limit \\ nil,
sort \\ :updated_at,
direction \\ :asc,
status \\ :open
) do
status
|> list_reports_query()
|> Page.paginate(page, limit)
|> sort(sort, direction)
|> Repo.all()
end
@doc """
Counts opened reports.
"""
@spec count_opened_reports :: integer
def count_opened_reports do
Repo.aggregate(count_reports_query(), :count, :id)
end
@doc """
Gets a single note.
"""
@spec get_note(integer | String.t()) :: Note.t() | nil
def get_note(id), do: Repo.get(Note, id)
@doc """
Gets a single note.
Raises `Ecto.NoResultsError` if the Note does not exist.
"""
@spec get_note!(integer | String.t()) :: Note.t()
def get_note!(id), do: Repo.get!(Note, id)
@doc """
Creates a note.
"""
@spec create_note(map) :: {:ok, Note.t()} | {:error, Ecto.Changeset.t()}
def create_note(attrs \\ %{}) do
with {:ok, %Note{} = note} <-
%Note{}
|> Note.changeset(attrs)
|> Repo.insert() do
{:ok, Repo.preload(note, [:report, :moderator])}
end
end
@doc """
Deletes a note.
"""
@spec delete_note(Note.t()) :: {:ok, Note.t()} | {:error, Ecto.Changeset.t()}
def delete_note(%Note{} = note), do: Repo.delete(note)
@doc """
Returns the list of notes for a report.
"""
@spec list_notes_for_report(Report.t()) :: [Note.t()]
def list_notes_for_report(%Report{id: report_id}) do
report_id
|> list_notes_for_report_query()
|> Repo.all()
end
@spec report_by_url_query(String.t()) :: Ecto.Query.t()
defp report_by_url_query(url) do
from(r in Report, where: r.uri == ^url)
end
@spec list_reports_query(ReportStatus.t()) :: Ecto.Query.t()
defp list_reports_query(status) do
from(
r in Report,
preload: [:reported, :reporter, :manager, :event, :comments, :notes],
where: r.status == ^status
)
end
@spec count_reports_query :: Ecto.Query.t()
defp count_reports_query do
from(r in Report, where: r.status == ^:open)
end
@spec list_notes_for_report_query(integer | String.t()) :: Ecto.Query.t()
defp list_notes_for_report_query(report_id) do
from(
n in Note,
where: n.report_id == ^report_id,
preload: [:report, :moderator]
)
end
end

View File

@ -0,0 +1,15 @@
defmodule Mobilizon.Storage.Ecto do
@moduledoc """
Mobilizon Ecto utils
"""
import Ecto.Query, warn: false
@doc """
Adds sort to the query.
"""
@spec sort(Ecto.Query.t(), atom, atom) :: Ecto.Query.t()
def sort(query, sort, direction) do
from(query, order_by: [{^direction, ^sort}])
end
end

View File

@ -0,0 +1,48 @@
defmodule Mobilizon.Storage.Page do
@moduledoc """
Module for pagination of queries.
"""
import Ecto.Query
alias Mobilizon.Storage.Repo
defstruct [
:total,
:elements
]
@type t :: %__MODULE__{
total: integer,
elements: struct
}
@doc """
Returns a Page struct for a query.
"""
@spec build_page(Ecto.Query.t(), integer | nil, integer | nil) :: t
def build_page(query, page, limit) do
[total, elements] =
[
fn -> Repo.aggregate(query, :count, :id) end,
fn -> Repo.all(paginate(query, page, limit)) end
]
|> Enum.map(&Task.async/1)
|> Enum.map(&Task.await/1)
%__MODULE__{total: total, elements: elements}
end
@doc """
Add limit and offset to the query.
"""
@spec paginate(Ecto.Query.t() | struct, integer | nil, integer | nil) :: Ecto.Query.t()
def paginate(query, page \\ 1, size \\ 10)
def paginate(query, page, _size) when is_nil(page), do: paginate(query)
def paginate(query, page, size) when is_nil(size), do: paginate(query, page)
def paginate(query, page, size) do
from(query, limit: ^size, offset: ^((page - 1) * size))
end
end

View File

@ -0,0 +1,5 @@
Postgrex.Types.define(
Mobilizon.Storage.PostgresTypes,
[Geo.PostGIS.Extension | Ecto.Adapters.Postgres.extensions()],
json: Jason
)

View File

@ -1,14 +1,14 @@
defmodule Mobilizon.Repo do
defmodule Mobilizon.Storage.Repo do
@moduledoc """
Mobilizon Repo
Mobilizon Repo.
"""
use Ecto.Repo,
otp_app: :mobilizon,
adapter: Ecto.Adapters.Postgres
@doc """
Dynamically loads the repository url from the
DATABASE_URL environment variable.
Dynamically loads the repository url from the DATABASE_URL environment variable.
"""
def init(_, opts) do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}

View File

@ -1,45 +1,35 @@
import EctoEnum
defenum(Mobilizon.Users.UserRoleEnum, :user_role_type, [
:administrator,
:moderator,
:user
])
defmodule Mobilizon.Users.User do
@moduledoc """
Represents a local user
Represents a local user.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
alias Mobilizon.Service.EmailChecker
alias Mobilizon.Crypto
alias Mobilizon.Events.FeedToken
alias Mobilizon.Service.EmailChecker
alias Mobilizon.Users.UserRole
schema "users" do
field(:email, :string)
field(:password_hash, :string)
field(:password, :string, virtual: true)
field(:role, Mobilizon.Users.UserRoleEnum, default: :user)
has_many(:actors, Actor)
belongs_to(:default_actor, Actor)
field(:confirmed_at, :utc_datetime)
field(:confirmation_sent_at, :utc_datetime)
field(:confirmation_token, :string)
field(:reset_password_sent_at, :utc_datetime)
field(:reset_password_token, :string)
has_many(:feed_tokens, FeedToken, foreign_key: :user_id)
@type t :: %__MODULE__{
email: String.t(),
password_hash: String.t(),
password: String.t(),
role: UserRole.t(),
confirmed_at: DateTime.t(),
confirmation_sent_at: DateTime.t(),
confirmation_token: String.t(),
reset_password_sent_at: DateTime.t(),
reset_password_token: String.t(),
default_actor: Actor.t(),
actors: [Actor.t()],
feed_tokens: [FeedToken.t()]
}
timestamps()
end
@doc false
def changeset(%User{} = user, attrs) do
changeset =
user
|> cast(attrs, [
:email,
@required_attrs [:email]
@optional_attrs [
:role,
:password,
:password_hash,
@ -48,16 +38,43 @@ defmodule Mobilizon.Users.User do
:confirmation_token,
:reset_password_sent_at,
:reset_password_token
])
|> validate_required([:email])
]
@attrs @required_attrs ++ @optional_attrs
@registration_required_attrs [:email, :password]
@password_reset_required_attrs [:password, :reset_password_token, :reset_password_sent_at]
@confirmation_token_length 30
schema "users" do
field(:email, :string)
field(:password_hash, :string)
field(:password, :string, virtual: true)
field(:role, UserRole, default: :user)
field(:confirmed_at, :utc_datetime)
field(:confirmation_sent_at, :utc_datetime)
field(:confirmation_token, :string)
field(:reset_password_sent_at, :utc_datetime)
field(:reset_password_token, :string)
belongs_to(:default_actor, Actor)
has_many(:actors, Actor)
has_many(:feed_tokens, FeedToken, foreign_key: :user_id)
timestamps()
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = user, attrs) do
changeset =
user
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
|> unique_constraint(:email, message: "This email is already used.")
|> validate_email()
|> validate_length(
:password,
min: 6,
max: 100,
message: "The chosen password is too short."
)
|> validate_length(:password, min: 6, max: 100, message: "The chosen password is too short.")
if Map.has_key?(attrs, :default_actor) do
put_assoc(changeset, :default_actor, attrs.default_actor)
@ -66,11 +83,13 @@ defmodule Mobilizon.Users.User do
end
end
def registration_changeset(struct, params) do
struct
|> changeset(params)
@doc false
@spec registration_changeset(t, map) :: Ecto.Changeset.t()
def registration_changeset(%__MODULE__{} = user, attrs) do
user
|> changeset(attrs)
|> cast_assoc(:default_actor)
|> validate_required([:email, :password])
|> validate_required(@registration_required_attrs)
|> hash_password()
|> save_confirmation_token()
|> unique_constraint(
@ -79,16 +98,18 @@ defmodule Mobilizon.Users.User do
)
end
def send_password_reset_changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:reset_password_token, :reset_password_sent_at])
@doc false
@spec send_password_reset_changeset(t, map) :: Ecto.Changeset.t()
def send_password_reset_changeset(%__MODULE__{} = user, attrs) do
cast(user, attrs, [:reset_password_token, :reset_password_sent_at])
end
def password_reset_changeset(%User{} = user, attrs) do
@doc false
@spec password_reset_changeset(t, map) :: Ecto.Changeset.t()
def password_reset_changeset(%__MODULE__{} = user, attrs) do
user
|> cast(attrs, [:password, :reset_password_token, :reset_password_sent_at])
|> validate_length(
:password,
|> cast(attrs, @password_reset_required_attrs)
|> validate_length(:password,
min: 6,
max: 100,
message: "registration.error.password_too_short"
@ -96,28 +117,48 @@ defmodule Mobilizon.Users.User do
|> hash_password()
end
@doc """
Checks whether an user is confirmed.
"""
@spec is_confirmed(t) :: boolean
def is_confirmed(%__MODULE__{confirmed_at: nil}), do: false
def is_confirmed(%__MODULE__{}), do: true
@doc """
Returns whether an user owns an actor.
"""
@spec owns_actor(t, integer | String.t()) :: {:is_owned, Actor.t() | nil}
def owns_actor(%__MODULE__{actors: actors}, actor_id) do
user_actor = Enum.find(actors, fn actor -> "#{actor.id}" == "#{actor_id}" end)
{:is_owned, user_actor}
end
@spec save_confirmation_token(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp save_confirmation_token(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{email: _email}} ->
changeset = put_change(changeset, :confirmation_token, random_string(30))
now = DateTime.utc_now()
put_change(
changeset,
:confirmation_sent_at,
DateTime.utc_now() |> DateTime.truncate(:second)
)
changeset
|> put_change(:confirmation_token, Crypto.random_string(@confirmation_token_length))
|> put_change(:confirmation_sent_at, DateTime.truncate(now, :second))
_ ->
changeset
end
end
@spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_email(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{email: email}} ->
case EmailChecker.valid?(email) do
false -> add_error(changeset, :email, "Email doesn't fit required format")
_ -> changeset
false ->
add_error(changeset, :email, "Email doesn't fit required format")
true ->
changeset
end
_ ->
@ -125,46 +166,14 @@ defmodule Mobilizon.Users.User do
end
end
defp random_string(length) do
length
|> :crypto.strong_rand_bytes()
|> Base.url_encode64()
end
# Hash password when it's changed
@spec hash_password(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp hash_password(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
put_change(
changeset,
:password_hash,
Argon2.hash_pwd_salt(password)
)
put_change(changeset, :password_hash, Argon2.hash_pwd_salt(password))
_ ->
changeset
end
end
def is_confirmed(%User{confirmed_at: nil} = _user), do: {:error, :unconfirmed}
def is_confirmed(%User{} = user), do: {:ok, user}
@doc """
Returns whether an user owns an actor
"""
@spec owns_actor(struct(), String.t()) :: {:is_owned, false} | {:is_owned, true, Actor.t()}
def owns_actor(%User{} = user, actor_id) when is_binary(actor_id) do
case Integer.parse(actor_id) do
{actor_id, ""} -> owns_actor(user, actor_id)
_ -> {:is_owned, false}
end
end
@spec owns_actor(struct(), integer()) :: {:is_owned, false} | {:is_owned, true, Actor.t()}
def owns_actor(%User{actors: actors}, actor_id) do
case Enum.find(actors, fn a -> a.id == actor_id end) do
nil -> {:is_owned, false}
actor -> {:is_owned, true, actor}
end
end
end

View File

@ -3,116 +3,86 @@ defmodule Mobilizon.Users do
The Users context.
"""
import Ecto.Query, warn: false
import Ecto.Query
import EctoEnum
alias Mobilizon.Repo
import Mobilizon.Ecto
import Mobilizon.Storage.Ecto
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.User
@doc false
def data() do
Dataloader.Ecto.new(Repo, query: &query/2)
end
@type tokens :: %{
required(:access_token) => String.t(),
required(:refresh_token) => String.t()
}
@doc false
def query(queryable, _params) do
queryable
end
defenum(UserRole, :user_role, [:administrator, :moderator, :user])
@doc """
Register user
Registers an user.
"""
@spec register(map()) :: {:ok, User.t()} | {:error, String.t()}
@spec register(map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def register(%{email: _email, password: _password} = args) do
with {:ok, %User{} = user} <-
%User{}
|> User.registration_changeset(args)
|> Mobilizon.Repo.insert() do
Mobilizon.Events.create_feed_token(%{"user_id" => user.id})
|> Repo.insert() do
Events.create_feed_token(%{"user_id" => user.id})
{:ok, user}
end
end
@doc """
Gets an user by it's email
## Examples
iex> get_user_by_email("test@test.tld", true)
{:ok, %Mobilizon.Users.User{}}
iex> get_user_by_email("test@notfound.tld", false)
{:error, :user_not_found}
Gets a single user.
Raises `Ecto.NoResultsError` if the user does not exist.
"""
@spec get_user!(integer | String.t()) :: User.t()
def get_user!(id), do: Repo.get!(User, id)
@doc """
Gets an user by its email.
"""
@spec get_user_by_email(String.t(), boolean | nil) ::
{:ok, User.t()} | {:error, :user_not_found}
def get_user_by_email(email, activated \\ nil) do
query =
case activated do
nil ->
from(u in User, where: u.email == ^email, preload: :default_actor)
true ->
from(
u in User,
where: u.email == ^email and not is_nil(u.confirmed_at),
preload: :default_actor
)
false ->
from(
u in User,
where: u.email == ^email and is_nil(u.confirmed_at),
preload: :default_actor
)
end
query = user_by_email_query(email, activated)
case Repo.one(query) do
nil -> {:error, :user_not_found}
user -> {:ok, user}
nil ->
{:error, :user_not_found}
user ->
{:ok, user}
end
end
@doc """
Get an user by it's activation token
Get an user by its activation token.
"""
@spec get_user_by_activation_token(String.t()) :: Actor.t()
@spec get_user_by_activation_token(String.t()) :: Actor.t() | nil
def get_user_by_activation_token(token) do
Repo.one(
from(
u in User,
where: u.confirmation_token == ^token,
preload: [:default_actor]
)
)
token
|> user_by_activation_token_query()
|> Repo.one()
end
@doc """
Get an user by it's reset password token
Get an user by its reset password token.
"""
@spec get_user_by_reset_password_token(String.t()) :: Actor.t()
def get_user_by_reset_password_token(token) do
Repo.one(
from(
u in User,
where: u.reset_password_token == ^token,
preload: [:default_actor]
)
)
token
|> user_by_reset_password_token_query()
|> Repo.one()
end
@doc """
Updates a user.
## Examples
iex> update_user(User{}, %{password: "coucou"})
{:ok, %Mobilizon.Users.User{}}
iex> update_user(User{}, %{password: nil})
{:error, %Ecto.Changeset{}}
Updates an user.
"""
@spec update_user(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update_user(%User{} = user, attrs) do
with {:ok, %User{} = user} <-
user
@ -123,65 +93,26 @@ defmodule Mobilizon.Users do
end
@doc """
Deletes a User.
## Examples
iex> delete_user(%User{email: "test@test.tld"})
{:ok, %Mobilizon.Users.User{}}
iex> delete_user(%User{})
{:error, %Ecto.Changeset{}}
Deletes an user.
"""
def delete_user(%User{} = user) do
Repo.delete(user)
end
# @doc """
# Returns an `%Ecto.Changeset{}` for tracking user changes.
# ## Examples
# iex> change_user(%Mobilizon.Users.User{})
# %Ecto.Changeset{data: %Mobilizon.Users.User{}}
# """
# def change_user(%User{} = user) do
# User.changeset(user, %{})
# end
@spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def delete_user(%User{} = user), do: Repo.delete(user)
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%Mobilizon.Users.User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
Get an user with its actors
Raises `Ecto.NoResultsError` if the user does not exist.
"""
def get_user!(id), do: Repo.get!(User, id)
@doc """
Get an user with it's actors
Raises `Ecto.NoResultsError` if the User does not exist.
"""
@spec get_user_with_actors!(integer()) :: User.t()
@spec get_user_with_actors!(integer | String.t()) :: User.t()
def get_user_with_actors!(id) do
user = Repo.get!(User, id)
Repo.preload(user, [:actors, :default_actor])
id
|> get_user!()
|> Repo.preload([:actors, :default_actor])
end
@doc """
Get user with it's actors by ID
Get user with its actors.
"""
@spec get_user_with_actors(integer()) :: User.t()
@spec get_user_with_actors(integer()) :: {:ok, User.t()} | {:error, String.t()}
def get_user_with_actors(id) do
case Repo.get(User, id) do
nil ->
@ -198,23 +129,24 @@ defmodule Mobilizon.Users do
end
@doc """
Returns the associated actor for an user, either the default set one or the first found
Gets the associated actor for an user, either the default set one or the first
found.
"""
@spec get_actor_for_user(Mobilizon.Users.User.t()) :: Mobilizon.Actors.Actor.t()
def get_actor_for_user(%Mobilizon.Users.User{} = user) do
case Repo.one(
from(
a in Actor,
join: u in User,
on: u.default_actor_id == a.id,
where: u.id == ^user.id
)
) do
@spec get_actor_for_user(User.t()) :: Actor.t() | nil
def get_actor_for_user(%User{} = user) do
actor =
user
|> actor_for_user_query()
|> Repo.one()
case actor do
nil ->
case user
|> get_actors_for_user() do
[] -> nil
actors -> hd(actors)
case get_actors_for_user(user) do
[] ->
nil
actors ->
hd(actors)
end
actor ->
@ -222,94 +154,48 @@ defmodule Mobilizon.Users do
end
end
def get_actors_for_user(%User{id: user_id}) do
Repo.all(from(a in Actor, where: a.user_id == ^user_id))
@doc """
Gets actors for an user.
"""
@spec get_actors_for_user(User.t()) :: [Actor.t()]
def get_actors_for_user(%User{} = user) do
user
|> actors_for_user_query()
|> Repo.all()
end
@doc """
Authenticate user
Updates user's default actor.
Raises `Ecto.NoResultsError` if the user does not exist.
"""
def authenticate(%{user: user, password: password}) do
# Does password match the one stored in the database?
with true <- Argon2.verify_pass(password, user.password_hash),
# Yes, create and return the token
{:ok, tokens} <- generate_tokens(user) do
{:ok, tokens}
else
_ ->
# No, return an error
{:error, :unauthorized}
end
end
@doc """
Generate access token and refresh token
"""
def generate_tokens(user) do
with {:ok, access_token} <- generate_access_token(user),
{:ok, refresh_token} <- generate_refresh_token(user) do
{:ok, %{access_token: access_token, refresh_token: refresh_token}}
end
end
defp generate_access_token(user) do
with {:ok, access_token, _claims} <-
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "access") do
{:ok, access_token}
end
end
def generate_refresh_token(user) do
with {:ok, refresh_token, _claims} <-
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
{:ok, refresh_token}
end
end
@spec update_user_default_actor(integer | String.t(), integer | String.t()) :: User.t()
def update_user_default_actor(user_id, actor_id) do
with _ <-
from(
u in User,
where: u.id == ^user_id,
update: [
set: [
default_actor_id: ^actor_id
]
]
)
user_id
|> update_user_default_actor_query(actor_id)
|> Repo.update_all([]) do
Repo.get!(User, user_id)
user_id
|> get_user!()
|> Repo.preload([:default_actor])
end
end
@doc """
Returns the list of users.
## Examples
iex> list_users()
[%Mobilizon.Users.User{}]
"""
@spec list_users(integer | nil, integer | nil, atom | nil, atom | nil) :: [User.t()]
def list_users(page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil) do
Repo.all(
User
|> paginate(page, limit)
|> Page.paginate(page, limit)
|> sort(sort, direction)
)
|> Repo.all()
end
@doc """
Returns the list of administrators.
## Examples
iex> list_admins()
[%Mobilizon.Users.User{role: :administrator}]
"""
def list_admins() do
@spec list_admins :: [User.t()]
def list_admins do
User
|> where([u], u.role == ^:administrator)
|> Repo.all()
@ -317,25 +203,127 @@ defmodule Mobilizon.Users do
@doc """
Returns the list of moderators.
## Examples
iex> list_moderators()
[%Mobilizon.Users.User{role: :moderator}, %Mobilizon.Users.User{role: :administrator}]
"""
def list_moderators() do
@spec list_moderators :: [User.t()]
def list_moderators do
User
|> where([u], u.role in ^[:administrator, :moderator])
|> Repo.all()
end
def count_users() do
Repo.one(
@doc """
Counts users.
"""
@spec count_users :: integer
def count_users, do: Repo.one(from(u in User, select: count(u.id)))
@doc """
Authenticate an user.
"""
@spec authenticate(User.t()) :: {:ok, tokens} | {:error, :unauthorized}
def authenticate(%{user: %User{password_hash: password_hash} = user, password: password}) do
# Does password match the one stored in the database?
if Argon2.verify_pass(password, password_hash) do
{:ok, _tokens} = generate_tokens(user)
else
{:error, :unauthorized}
end
end
@doc """
Generates access token and refresh token for an user.
"""
@spec generate_tokens(User.t()) :: {:ok, tokens}
def generate_tokens(user) do
with {:ok, access_token} <- generate_access_token(user),
{:ok, refresh_token} <- generate_refresh_token(user) do
{:ok, %{access_token: access_token, refresh_token: refresh_token}}
end
end
@doc """
Generates access token for an user.
"""
@spec generate_access_token(User.t()) :: {:ok, String.t()}
def generate_access_token(user) do
with {:ok, access_token, _claims} <-
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "access") do
{:ok, access_token}
end
end
@doc """
Generates refresh token for an user.
"""
@spec generate_refresh_token(User.t()) :: {:ok, String.t()}
def generate_refresh_token(user) do
with {:ok, refresh_token, _claims} <-
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
{:ok, refresh_token}
end
end
@spec user_by_email_query(String.t(), boolean | nil) :: Ecto.Query.t()
defp user_by_email_query(email, nil) do
from(u in User, where: u.email == ^email, preload: :default_actor)
end
defp user_by_email_query(email, true) do
from(
u in User,
select: count(u.id)
where: u.email == ^email and not is_nil(u.confirmed_at),
preload: :default_actor
)
end
defp user_by_email_query(email, false) do
from(
u in User,
where: u.email == ^email and is_nil(u.confirmed_at),
preload: :default_actor
)
end
@spec user_by_activation_token_query(String.t()) :: Ecto.Query.t()
defp user_by_activation_token_query(token) do
from(
u in User,
where: u.confirmation_token == ^token,
preload: [:default_actor]
)
end
@spec user_by_reset_password_token_query(String.t()) :: Ecto.Query.t()
defp user_by_reset_password_token_query(token) do
from(
u in User,
where: u.reset_password_token == ^token,
preload: [:default_actor]
)
end
@spec actor_for_user_query(User.t()) :: Ecto.Query.t()
defp actor_for_user_query(%User{id: user_id}) do
from(
a in Actor,
join: u in User,
on: u.default_actor_id == a.id,
where: u.id == ^user_id
)
end
@spec actors_for_user_query(User.t()) :: Ecto.Query.t()
defp actors_for_user_query(%User{id: user_id}) do
from(a in Actor, where: a.user_id == ^user_id)
end
@spec update_user_default_actor_query(integer | String.t(), integer | String.t()) ::
Ecto.Query.t()
defp update_user_default_actor_query(user_id, actor_id) do
from(
u in User,
where: u.id == ^user_id,
update: [set: [default_actor_id: ^actor_id]]
)
end
end

View File

@ -5,6 +5,7 @@ defmodule MobilizonWeb.API.Events do
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias Mobilizon.Service.ActivityPub.Activity
alias MobilizonWeb.API.Utils
@doc """

View File

@ -32,7 +32,7 @@ defmodule MobilizonWeb.API.Follows do
def accept(%Actor{} = follower, %Actor{} = followed) do
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <-
Actor.following?(follower, followed),
Actors.is_following(follower, followed),
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}",
data <-
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),

View File

@ -3,6 +3,7 @@ defmodule MobilizonWeb.API.Groups do
API for Events
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
@ -22,21 +23,13 @@ defmodule MobilizonWeb.API.Groups do
banner: _banner
} = args
) do
with {:is_owned, true, actor} <- User.owns_actor(user, creator_actor_id),
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id),
title <- String.trim(title),
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
visibility <- Map.get(args, :visibility, :public),
{content_html, tags, to, cc} <-
Utils.prepare_content(actor, summary, visibility, [], nil),
group <-
ActivityPubUtils.make_group_data(
actor.url,
to,
title,
content_html,
tags,
cc
) do
group <- ActivityPubUtils.make_group_data(actor.url, to, title, content_html, tags, cc) do
ActivityPub.create(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: actor,
@ -47,7 +40,7 @@ defmodule MobilizonWeb.API.Groups do
{:existing_group, _} ->
{:error, "A group with this name already exists"}
{:is_owned, _} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end

View File

@ -3,17 +3,18 @@ defmodule MobilizonWeb.API.Reports do
API for Reports
"""
import MobilizonWeb.API.Utils
import Mobilizon.Service.Admin.ActionLogService
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Activity
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Reports, as: ReportsAction
alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Users
alias Mobilizon.Users.User
import MobilizonWeb.API.Utils
import Mobilizon.Service.Admin.ActionLogService
@doc """
Create a report/flag on an actor, and optionally on an event or on comments.
@ -61,7 +62,7 @@ defmodule MobilizonWeb.API.Reports do
"""
def update_report_status(%Actor{} = actor, %Report{} = report, state) do
with {:valid_state, true} <-
{:valid_state, Mobilizon.Reports.ReportStateEnum.valid_value?(state)},
{:valid_state, Mobilizon.Reports.ReportStatus.valid_value?(state)},
{:ok, report} <- ReportsAction.update_report(report, %{"status" => state}),
{:ok, _} <- log_action(actor, "update", report) do
{:ok, report}
@ -72,7 +73,7 @@ defmodule MobilizonWeb.API.Reports do
defp get_report_comments(%Actor{id: actor_id}, comment_ids) do
{:get_report_comments,
Events.get_all_comments_by_actor_and_ids(actor_id, comment_ids) |> Enum.map(& &1.url)}
Events.list_comments_by_actor_and_ids(actor_id, comment_ids) |> Enum.map(& &1.url)}
end
defp get_report_comments(_, _), do: {:get_report_comments, nil}
@ -89,7 +90,7 @@ defmodule MobilizonWeb.API.Reports do
with %User{role: role} <- Users.get_user!(user_id),
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <-
Mobilizon.Reports.create_report_note(%{
Mobilizon.Reports.create_note(%{
"report_id" => report_id,
"moderator_id" => moderator_id,
"content" => content
@ -114,7 +115,7 @@ defmodule MobilizonWeb.API.Reports do
%User{role: role} <- Users.get_user!(user_id),
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <-
Mobilizon.Reports.delete_report_note(note),
Mobilizon.Reports.delete_note(note),
{:ok, _} <- log_action(moderator, "delete", note) do
{:ok, note}
else

View File

@ -1,20 +1,21 @@
defmodule MobilizonWeb.API.Search do
@moduledoc """
API for Search
API for search.
"""
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Actors.ActorType
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Storage.Page
require Logger
@doc """
Search actors
Searches actors.
"""
@spec search_actors(String.t(), integer(), integer(), String.t()) ::
{:ok, %{total: integer(), elements: list(Actor.t())}} | {:error, any()}
@spec search_actors(String.t(), integer | nil, integer | nil, ActorType.t()) ::
{:ok, Page.t()} | {:error, String.t()}
def search_actors(search, page \\ 1, limit \\ 10, result_type) do
search = String.trim(search)
@ -22,31 +23,33 @@ defmodule MobilizonWeb.API.Search do
search == "" ->
{:error, "Search can't be empty"}
# Some URLs could be domain.tld/@username, so keep this condition above handle_search? function
url_search?(search) ->
# If this is not an actor, skip
# Some URLs could be domain.tld/@username, so keep this condition above
# the `is_handle` function
is_url(search) ->
# skip, if it's not an actor
case process_from_url(search) do
%{:total => total, :elements => [%Actor{}] = elements} ->
{:ok, %{total: total, elements: elements}}
%Page{total: _total, elements: _elements} = page ->
{:ok, page}
_ ->
{:ok, %{total: 0, elements: []}}
end
handle_search?(search) ->
is_handle(search) ->
{:ok, process_from_username(search)}
true ->
{:ok,
Actors.find_and_count_actors_by_username_or_name(search, [result_type], page, limit)}
page = Actors.build_actors_by_username_or_name_page(search, [result_type], page, limit)
{:ok, page}
end
end
@doc """
Search events
"""
@spec search_events(String.t(), integer(), integer()) ::
{:ok, %{total: integer(), elements: list(Event.t())}} | {:error, any()}
@spec search_events(String.t(), integer | nil, integer | nil) ::
{:ok, Page.t()} | {:error, String.t()}
def search_events(search, page \\ 1, limit \\ 10) do
search = String.trim(search)
@ -54,59 +57,52 @@ defmodule MobilizonWeb.API.Search do
search == "" ->
{:error, "Search can't be empty"}
url_search?(search) ->
# If this is not an event, skip
is_url(search) ->
# skip, if it's w not an actor
case process_from_url(search) do
{total = total, [%Event{} = elements]} ->
{:ok, %{total: total, elements: elements}}
%Page{total: _total, elements: _elements} = page ->
{:ok, page}
_ ->
{:ok, %{total: 0, elements: []}}
end
true ->
{:ok, Events.find_and_count_events_by_name(search, page, limit)}
{:ok, Events.build_events_by_name(search, page, limit)}
end
end
# If the search string is an username
@spec process_from_username(String.t()) :: %{total: integer(), elements: [Actor.t()]}
@spec process_from_username(String.t()) :: Page.t()
defp process_from_username(search) do
case ActivityPub.find_or_make_actor_from_nickname(search) do
{:ok, actor} ->
%{total: 1, elements: [actor]}
%Page{total: 1, elements: [actor]}
{:error, _err} ->
Logger.debug(fn -> "Unable to find or make actor '#{search}'" end)
%{total: 0, elements: []}
%Page{total: 0, elements: []}
end
end
# If the search string is an URL
@spec process_from_url(String.t()) :: %{
total: integer(),
elements: [Actor.t() | Event.t() | Comment.t()]
}
@spec process_from_url(String.t()) :: Page.t()
defp process_from_url(search) do
case ActivityPub.fetch_object_from_url(search) do
{:ok, object} ->
%{total: 1, elements: [object]}
%Page{total: 1, elements: [object]}
{:error, _err} ->
Logger.debug(fn -> "Unable to find or make object from URL '#{search}'" end)
%{total: 0, elements: []}
%Page{total: 0, elements: []}
end
end
# Is the search an URL search?
@spec url_search?(String.t()) :: boolean
defp url_search?(search) do
String.starts_with?(search, "https://") or String.starts_with?(search, "http://")
end
@spec is_url(String.t()) :: boolean
defp is_url(search), do: String.starts_with?(search, ["http://", "https://"])
# Is the search an handle search?
@spec handle_search?(String.t()) :: boolean
defp handle_search?(search) do
String.match?(search, ~r/@/)
end
@spec is_handle(String.t()) :: boolean
defp is_handle(search), do: String.match?(search, ~r/@/)
end

View File

@ -2,7 +2,9 @@ defmodule MobilizonWeb.API.Utils do
@moduledoc """
Utils for API
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Config
alias Mobilizon.Service.Formatter
@doc """
@ -125,7 +127,7 @@ defmodule MobilizonWeb.API.Utils do
def make_report_content_text(nil), do: {:ok, nil}
def make_report_content_text(comment) do
max_size = Mobilizon.CommonConfig.get([:instance, :max_report_comment_size], 1000)
max_size = Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
{:ok, Formatter.html_escape(comment, "text/plain")}

View File

@ -0,0 +1,24 @@
defmodule MobilizonWeb.Cache do
@moduledoc """
Facade module which provides access to all cached data.
"""
alias Mobilizon.Actors.Actor
alias MobilizonWeb.Cache.ActivityPub
@caches [:activity_pub, :feed, :ics]
@doc """
Clears all caches for an actor.
"""
@spec clear_cache(Actor.t()) :: {:ok, true}
def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do
Enum.each(@caches, &Cachex.del(&1, "actor_" <> preferred_username))
end
defdelegate get_local_actor_by_name(name), to: ActivityPub
defdelegate get_public_event_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_comment_by_uuid_with_preload(uuid), to: ActivityPub
defdelegate get_relay, to: ActivityPub
end

70
lib/mobilizon_web/cache/activity_pub.ex vendored Normal file
View File

@ -0,0 +1,70 @@
defmodule MobilizonWeb.Cache.ActivityPub do
@moduledoc """
The ActivityPub related functions.
"""
alias Mobilizon.{Actors, Events, Service}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Comment, Event}
@cache :activity_pub
@doc """
Gets a local actor by username.
"""
@spec get_local_actor_by_name(String.t()) ::
{:commit, Actor.t()} | {:ignore, nil}
def get_local_actor_by_name(name) do
Cachex.fetch(@cache, "actor_" <> name, fn "actor_" <> name ->
case Actors.get_local_actor_by_name(name) do
%Actor{} = actor ->
{:commit, actor}
nil ->
{:ignore, nil}
end
end)
end
@doc """
Gets a public event by its UUID, with all associations loaded.
"""
@spec get_public_event_by_uuid_with_preload(String.t()) ::
{:commit, Event.t()} | {:ignore, nil}
def get_public_event_by_uuid_with_preload(uuid) do
Cachex.fetch(@cache, "event_" <> uuid, fn "event_" <> uuid ->
case Events.get_public_event_by_uuid_with_preload(uuid) do
%Event{} = event ->
{:commit, event}
nil ->
{:ignore, nil}
end
end)
end
@doc """
Gets a comment by its UUID, with all associations loaded.
"""
@spec get_comment_by_uuid_with_preload(String.t()) ::
{:commit, Comment.t()} | {:ignore, nil}
def get_comment_by_uuid_with_preload(uuid) do
Cachex.fetch(@cache, "comment_" <> uuid, fn "comment_" <> uuid ->
case Events.get_comment_from_uuid_with_preload(uuid) do
%Comment{} = comment ->
{:commit, comment}
nil ->
{:ignore, nil}
end
end)
end
@doc """
Gets a relay.
"""
@spec get_relay :: {:commit, Actor.t()} | {:ignore, nil}
def get_relay do
Cachex.fetch(@cache, "relay_actor", &Service.ActivityPub.Relay.get_actor/0)
end
end

View File

@ -5,11 +5,14 @@
defmodule MobilizonWeb.ActivityPubController do
use MobilizonWeb, :controller
alias Mobilizon.{Actors, Actors.Actor}
alias MobilizonWeb.ActivityPub.ActorView
alias Mobilizon.{Actors, Actors.Actor, Config}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.Federator
alias MobilizonWeb.ActivityPub.ActorView
alias MobilizonWeb.Cache
require Logger
action_fallback(:errors)
@ -17,7 +20,7 @@ defmodule MobilizonWeb.ActivityPubController do
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
if Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
if Config.get([:instance, :allow_relay]) do
conn
else
conn
@ -29,7 +32,7 @@ defmodule MobilizonWeb.ActivityPubController do
def following(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
%Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("following.json", %{actor: actor, page: page}))
@ -37,7 +40,7 @@ defmodule MobilizonWeb.ActivityPubController do
end
def following(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("following.json", %{actor: actor}))
@ -46,7 +49,7 @@ defmodule MobilizonWeb.ActivityPubController do
def followers(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page),
%Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
%Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("followers.json", %{actor: actor, page: page}))
@ -54,7 +57,7 @@ defmodule MobilizonWeb.ActivityPubController do
end
def followers(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("followers.json", %{actor: actor}))
@ -111,13 +114,7 @@ defmodule MobilizonWeb.ActivityPubController do
end
def relay(conn, _params) do
with {status, actor} <-
Cachex.fetch(
:activity_pub,
"relay_actor",
&Mobilizon.Service.ActivityPub.Relay.get_actor/0
),
true <- status in [:ok, :commit] do
with {:commit, %Actor{} = actor} <- Cache.get_relay() do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("actor.json", %{actor: actor}))

View File

@ -8,60 +8,60 @@ defmodule MobilizonWeb.FeedController do
def actor(conn, %{"name" => name, "format" => "atom"}) do
case Cachex.fetch(:feed, "actor_" <> name) do
{status, data} when status in [:ok, :commit] ->
{:commit, data} ->
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, data)
_err ->
_ ->
{:error, :not_found}
end
end
def actor(conn, %{"name" => name, "format" => "ics"}) do
case Cachex.fetch(:ics, "actor_" <> name) do
{status, data} when status in [:ok, :commit] ->
{:commit, data} ->
conn
|> put_resp_content_type("text/calendar")
|> send_resp(200, data)
_err ->
_ ->
{:error, :not_found}
end
end
def event(conn, %{"uuid" => uuid, "format" => "ics"}) do
case Cachex.fetch(:ics, "event_" <> uuid) do
{status, data} when status in [:ok, :commit] ->
{:commit, data} ->
conn
|> put_resp_content_type("text/calendar")
|> send_resp(200, data)
_err ->
_ ->
{:error, :not_found}
end
end
def going(conn, %{"token" => token, "format" => "ics"}) do
case Cachex.fetch(:ics, "token_" <> token) do
{status, data} when status in [:ok, :commit] ->
{:commit, data} ->
conn
|> put_resp_content_type("text/calendar")
|> send_resp(200, data)
_err ->
_ ->
{:error, :not_found}
end
end
def going(conn, %{"token" => token, "format" => "atom"}) do
case Cachex.fetch(:feed, "token_" <> token) do
{status, data} when status in [:ok, :commit] ->
{:commit, data} ->
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, data)
_err ->
{:ignore, _} ->
{:error, :not_found}
end
end

View File

@ -5,13 +5,16 @@
defmodule MobilizonWeb.MediaProxyController do
use MobilizonWeb, :controller
alias Mobilizon.Config
alias MobilizonWeb.ReverseProxy
alias MobilizonWeb.MediaProxy
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
with config <- Mobilizon.CommonConfig.get([:media_proxy], []),
with config <- Config.get([:media_proxy], []),
true <- Keyword.get(config, :enabled, false),
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
:ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do

View File

@ -6,10 +6,9 @@
defmodule MobilizonWeb.NodeInfoController do
use MobilizonWeb, :controller
alias Mobilizon.CommonConfig
alias Mobilizon.Config
alias Mobilizon.Service.Statistics
@instance Application.get_env(:mobilizon, :instance)
@node_info_supported_versions ["2.0", "2.1"]
@node_info_schema_uri "http://nodeinfo.diaspora.software/ns/schema/"
@ -35,14 +34,14 @@ defmodule MobilizonWeb.NodeInfoController do
version: version,
software: %{
name: "Mobilizon",
version: Keyword.get(@instance, :version)
version: Config.instance_version()
},
protocols: ["activitypub"],
services: %{
inbound: [],
outbound: ["atom1.0"]
},
openRegistrations: CommonConfig.registrations_open?(),
openRegistrations: Config.instance_registrations_open?(),
usage: %{
users: %{
total: Statistics.get_cached_value(:local_users)
@ -51,14 +50,14 @@ defmodule MobilizonWeb.NodeInfoController do
localComments: Statistics.get_cached_value(:local_comments)
},
metadata: %{
nodeName: CommonConfig.instance_name(),
nodeDescription: CommonConfig.instance_description()
nodeName: Config.instance_name(),
nodeDescription: Config.instance_description()
}
}
response =
if version == "2.1" do
put_in(response, [:software, :repository], Keyword.get(@instance, :repository))
put_in(response, [:software, :repository], Config.instance_repository())
else
response
end

View File

@ -3,8 +3,8 @@ defmodule MobilizonWeb.PageController do
Controller to load our webapp
"""
use MobilizonWeb, :controller
alias Mobilizon.Actors
alias Mobilizon.Events
alias MobilizonWeb.Cache
plug(:put_layout, false)
action_fallback(MobilizonWeb.FallbackController)
@ -12,17 +12,17 @@ defmodule MobilizonWeb.PageController do
def index(conn, _params), do: render(conn, :index)
def actor(conn, %{"name" => name}) do
{status, actor} = Actors.get_cached_local_actor_by_name(name)
{status, actor} = Cache.get_local_actor_by_name(name)
render_or_error(conn, &ok_status?/2, status, :actor, actor)
end
def event(conn, %{"uuid" => uuid}) do
{status, event} = Events.get_cached_event_full_by_uuid(uuid)
{status, event} = Cache.get_public_event_by_uuid_with_preload(uuid)
render_or_error(conn, &ok_status_and_is_visible?/2, status, :event, event)
end
def comment(conn, %{"uuid" => uuid}) do
{status, comment} = Events.get_cached_comment_full_by_uuid(uuid)
{status, comment} = Cache.get_comment_by_uuid_with_preload(uuid)
render_or_error(conn, &ok_status_and_is_visible?/2, status, :comment, comment)
end

View File

@ -0,0 +1,38 @@
defmodule MobilizonWeb.Email.Admin do
@moduledoc """
Handles emails sent to admins.
"""
use Bamboo.Phoenix, view: MobilizonWeb.EmailView
import Bamboo.{Email, Phoenix}
import MobilizonWeb.Gettext
alias Mobilizon.Config
alias Mobilizon.Reports.Report
alias Mobilizon.Users.User
alias MobilizonWeb.Email
@spec report(User.t(), Report.t(), String.t()) :: Bamboo.Email.t()
def report(%User{email: email}, %Report{} = report, locale \\ "en") do
Gettext.put_locale(locale)
instance_url = Config.instance_url()
subject =
gettext(
"Mobilizon: New report on instance %{instance}",
instance: instance_url
)
Email.base_email()
|> to(email)
|> subject(subject)
|> put_header("Reply-To", Config.instance_email_reply_to())
|> assign(:report, report)
|> assign(:instance, instance_url)
|> render(:report)
end
end

View File

@ -0,0 +1,17 @@
defmodule MobilizonWeb.Email do
@moduledoc """
The Email context.
"""
use Bamboo.Phoenix, view: MobilizonWeb.EmailView
alias Mobilizon.Config
@spec base_email :: Bamboo.Email.t()
def base_email do
new_email()
|> from(Config.instance_email_from())
|> put_html_layout({MobilizonWeb.EmailView, "email.html"})
|> put_text_layout({MobilizonWeb.EmailView, "email.text"})
end
end

View File

@ -1,6 +1,6 @@
defmodule Mobilizon.Mailer do
defmodule MobilizonWeb.Email.Mailer do
@moduledoc """
Mailer
Mobilizon Mailer.
"""
use Bamboo.Mailer, otp_app: :mobilizon
end

View File

@ -0,0 +1,64 @@
defmodule MobilizonWeb.Email.User do
@moduledoc """
Handles emails sent to users.
"""
use Bamboo.Phoenix, view: MobilizonWeb.EmailView
import Bamboo.{Email, Phoenix}
import MobilizonWeb.Gettext
alias Mobilizon.Config
alias Mobilizon.Users.User
alias MobilizonWeb.Email
@spec confirmation_email(User.t(), String.t()) :: Bamboo.Email.t()
def confirmation_email(
%User{email: email, confirmation_token: confirmation_token},
locale \\ "en"
) do
Gettext.put_locale(locale)
instance_url = Config.instance_url()
subject =
gettext(
"Mobilizon: Confirmation instructions for %{instance}",
instance: instance_url
)
Email.base_email()
|> to(email)
|> subject(subject)
|> put_header("Reply-To", Config.instance_email_reply_to())
|> assign(:token, confirmation_token)
|> assign(:instance, instance_url)
|> render(:registration_confirmation)
end
@spec reset_password_email(User.t(), String.t()) :: Bamboo.Email.t()
def reset_password_email(
%User{email: email, reset_password_token: reset_password_token},
locale \\ "en"
) do
Gettext.put_locale(locale)
instance_url = Config.instance_url()
subject =
gettext(
"Mobilizon: Reset your password on %{instance} instructions",
instance: instance_url
)
Email.base_email()
|> to(email)
|> subject(subject)
|> put_header("Reply-To", Config.instance_email_reply_to())
|> assign(:token, reset_password_token)
|> assign(:instance, instance_url)
|> render(:password_reset)
end
end

View File

@ -7,6 +7,9 @@ defmodule MobilizonWeb.MediaProxy do
@moduledoc """
Handles proxifying media files
"""
alias Mobilizon.Config
@base64_opts [padding: false]
def url(nil), do: nil
@ -66,7 +69,7 @@ defmodule MobilizonWeb.MediaProxy do
def build_url(sig_base64, url_base64, filename \\ nil) do
[
Mobilizon.CommonConfig.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()),
Config.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()),
"proxy",
sig_base64,
url_base64,

View File

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/mime.ex
defmodule Mobilizon.MIME do
defmodule MobilizonWeb.MIME do
@moduledoc """
Returns the mime-type of a binary and optionally a normalized file-name.
"""

View File

@ -8,10 +8,14 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do
Serves uploaded media files
"""
@behaviour Plug
import Plug.Conn
alias Mobilizon.Config
require Logger
@behaviour Plug
# no slashes
@path "media"
@ -38,7 +42,7 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do
conn
end
config = Mobilizon.CommonConfig.get([MobilizonWeb.Upload])
config = Config.get([MobilizonWeb.Upload])
with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false),
@ -75,7 +79,7 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do
conn
|> MobilizonWeb.ReverseProxy.call(
url,
Mobilizon.CommonConfig.get([Mobilizon.Upload, :proxy_opts], [])
Config.get([Mobilizon.Upload, :proxy_opts], [])
)
end

View File

@ -2,12 +2,14 @@ defmodule MobilizonWeb.Resolvers.Comment do
@moduledoc """
Handles the comment-related GraphQL calls
"""
require Logger
alias Mobilizon.Events.Comment
alias Mobilizon.Activity
alias Mobilizon.Users.User
alias Mobilizon.Service.ActivityPub.Activity
alias MobilizonWeb.API.Comments
require Logger
def create_comment(_parent, %{text: comment, actor_username: username}, %{
context: %{current_user: %User{} = _user}
}) do

View File

@ -1,19 +1,19 @@
defmodule MobilizonWeb.Resolvers.Config do
@moduledoc """
Handles the config-related GraphQL calls
Handles the config-related GraphQL calls.
"""
import Mobilizon.CommonConfig
alias Mobilizon.Config
@doc """
Get config
Gets config.
"""
def get_config(_parent, _params, _context) do
{:ok,
%{
name: instance_name(),
registrations_open: registrations_open?(),
description: instance_description()
name: Config.instance_name(),
registrations_open: Config.instance_registrations_open?(),
description: Config.instance_description()
}}
end
end

View File

@ -2,16 +2,17 @@ defmodule MobilizonWeb.Resolvers.Event do
@moduledoc """
Handles the event-related GraphQL calls
"""
alias Mobilizon.Activity
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses
alias Mobilizon.Addresses.Address
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant, EventOptions}
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Media.Picture
alias Mobilizon.Users.User
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias MobilizonWeb.Resolvers.Person
alias Mobilizon.Service.ActivityPub.Activity
import Mobilizon.Service.Admin.ActionLogService
# We limit the max number of events that can be retrieved
@ -28,7 +29,7 @@ defmodule MobilizonWeb.Resolvers.Event do
end
def find_event(_parent, %{uuid: uuid}, _resolution) do
case Mobilizon.Events.get_event_full_by_uuid(uuid) do
case Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid) do
nil ->
{:error, "Event with UUID #{uuid} not found"}
@ -69,17 +70,14 @@ defmodule MobilizonWeb.Resolvers.Event do
) do
# We get the organizer's next public event
events =
[Events.get_actor_upcoming_public_event(organizer_actor, uuid)]
[Events.get_upcoming_public_event_for_actor(organizer_actor, uuid)]
|> Enum.filter(&is_map/1)
# We find similar events with the same tags
# uniq_by : It's possible event_from_same_actor is inside events_from_tags
events =
(events ++
Events.find_similar_events_by_common_tags(
tags,
@number_of_related_events
))
events
|> Enum.concat(Events.list_events_by_tags(tags, @number_of_related_events))
|> uniq_events()
# TODO: We should use tag_relations to find more appropriate events
@ -87,8 +85,10 @@ defmodule MobilizonWeb.Resolvers.Event do
# We've considered all recommended events, so we fetch the latest events
events =
if @number_of_related_events - length(events) > 0 do
(events ++
Events.list_events(1, @number_of_related_events, :begins_on, :asc, true, true))
events
|> Enum.concat(
Events.list_events(1, @number_of_related_events, :begins_on, :asc, true, true)
)
|> uniq_events()
else
events
@ -112,26 +112,23 @@ defmodule MobilizonWeb.Resolvers.Event do
def actor_join_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{
context: %{
current_user: user
}
}
%{context: %{current_user: user}}
) do
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_full(event_id)},
{: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} <- MobilizonWeb.API.Participations.join(event, actor),
participant <-
Map.put(participant, :event, event)
participant
|> Map.put(:event, event)
|> Map.put(:actor, Person.proxify_pictures(actor)) do
{:ok, participant}
else
{:has_event, _} ->
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :event_not_found} ->
@ -152,32 +149,18 @@ defmodule MobilizonWeb.Resolvers.Event do
def actor_leave_event(
_parent,
%{actor_id: actor_id, event_id: event_id},
%{
context: %{
current_user: user
}
}
%{context: %{current_user: user}}
) do
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:has_event, {:ok, %Event{} = event}} <-
{:has_event, Mobilizon.Events.get_event_full(event_id)},
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
{:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do
{
:ok,
%{
event: %{
id: event_id
},
actor: %{
id: actor_id
}
}
}
{: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, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:only_organizer, true} ->
@ -198,31 +181,19 @@ defmodule MobilizonWeb.Resolvers.Event do
def create_event(
_parent,
%{organizer_actor_id: organizer_actor_id} = args,
%{
context: %{
current_user: user
}
} = _resolution
%{context: %{current_user: user}} = _resolution
) do
# See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}),
{:is_owned, true, organizer_actor} <- User.owns_actor(user, organizer_actor_id),
{:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, args_with_organizer} <- save_attached_picture(args_with_organizer),
{:ok, args_with_organizer} <- save_physical_address(args_with_organizer),
{
:ok,
%Activity{
data: %{
"object" => %{"type" => "Event"} = _object
}
},
%Event{} = event
} <-
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.create_event(args_with_organizer) do
{:ok, event}
else
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Organizer actor id is not owned by the user"}
end
end
@ -237,35 +208,24 @@ defmodule MobilizonWeb.Resolvers.Event do
def update_event(
_parent,
%{event_id: event_id} = args,
%{
context: %{
current_user: user
}
} = _resolution
%{context: %{current_user: user}} = _resolution
) do
# See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}),
{:ok, %Event{} = event} <- Mobilizon.Events.get_event_full(event_id),
{:is_owned, true, organizer_actor} <- User.owns_actor(user, event.organizer_actor_id),
{:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
{:is_owned, %Actor{} = organizer_actor} <-
User.owns_actor(user, event.organizer_actor_id),
args <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, args} <- save_attached_picture(args),
{:ok, args} <- save_physical_address(args),
{
:ok,
%Activity{
data: %{
"object" => %{"type" => "Event"} = _object
}
},
%Event{} = event
} <-
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.update_event(args, event) do
{:ok, event}
else
{:error, :event_not_found} ->
{:error, "Event not found"}
{:is_owned, _} ->
{:is_owned, nil} ->
{:error, "User doesn't own actor"}
end
end
@ -279,24 +239,14 @@ defmodule MobilizonWeb.Resolvers.Event do
# However, we need to pass it's actor ID
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(
%{
picture: %{
picture: %{file: %Plug.Upload{} = _picture} = all_pic
}
} = args
%{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args
) do
{:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor.id))}
end
# Otherwise if we use a previously uploaded picture we need to fetch it from database
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(
%{
picture: %{
picture_id: picture_id
}
} = args
) do
defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do
with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
{:ok, Map.put(args, :picture, picture)}
end
@ -306,13 +256,7 @@ defmodule MobilizonWeb.Resolvers.Event do
defp save_attached_picture(args), do: {:ok, args}
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(
%{
physical_address: %{
url: physical_address_url
}
} = args
)
defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args)
when not is_nil(physical_address_url) do
with %Address{} = address <- Addresses.get_address_by_url(physical_address_url),
args <- Map.put(args, :physical_address, address.url) do
@ -337,23 +281,20 @@ defmodule MobilizonWeb.Resolvers.Event do
def delete_event(
_parent,
%{event_id: event_id, actor_id: actor_id},
%{
context: %{
current_user: %User{role: role} = user
}
}
%{context: %{current_user: %User{role: role} = user}}
) do
with {:ok, %Event{local: is_local} = event} <- Mobilizon.Events.get_event_full(event_id),
with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id),
{actor_id, ""} <- Integer.parse(actor_id),
{:is_owned, true, _} <- User.owns_actor(user, actor_id) do
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id) do
cond do
Event.can_event_be_managed_by(event, actor_id) == {:event_can_be_managed, true} ->
{:event_can_be_managed, true} == Event.can_be_managed_by(event, actor_id) ->
do_delete_event(event)
role in [:moderator, :administrator] ->
with {:ok, res} <- do_delete_event(event, !is_local),
%Actor{} = actor <- Actors.get_actor(actor_id) do
log_action(actor, "delete", event)
{:ok, res}
end
@ -364,7 +305,7 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, :event_not_found} ->
{:error, "Event not found"}
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end

View File

@ -2,10 +2,11 @@ defmodule MobilizonWeb.Resolvers.FeedToken do
@moduledoc """
Handles the feed tokens-related GraphQL calls
"""
require Logger
alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User
alias Mobilizon.Events
alias Mobilizon.Events.FeedToken
require Logger
@doc """
Create an feed token for an user and a defined actor
@ -14,11 +15,11 @@ defmodule MobilizonWeb.Resolvers.FeedToken do
def create_feed_token(_parent, %{actor_id: actor_id}, %{
context: %{current_user: %User{id: id} = user}
}) do
with {:is_owned, true, _actor} <- User.owns_actor(user, actor_id),
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, feed_token} <- Events.create_feed_token(%{"user_id" => id, "actor_id" => actor_id}) do
{:ok, feed_token}
else
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end

View File

@ -4,10 +4,12 @@ defmodule MobilizonWeb.Resolvers.Group do
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Activity
alias MobilizonWeb.Resolvers.Person
require Logger
@doc """
@ -40,19 +42,11 @@ defmodule MobilizonWeb.Resolvers.Group do
def create_group(
_parent,
args,
%{
context: %{
current_user: user
}
}
%{context: %{current_user: user}}
) do
with {
:ok,
%Activity{
data: %{
"object" => %{"type" => "Group"} = _object
}
},
%Activity{data: %{"object" => %{"type" => "Group"} = _object}},
%Actor{} = group
} <-
MobilizonWeb.API.Groups.create_group(
@ -66,10 +60,7 @@ defmodule MobilizonWeb.Resolvers.Group do
banner: Map.get(args, "banner")
}
) do
{
:ok,
group
}
{:ok, group}
end
end
@ -83,17 +74,13 @@ defmodule MobilizonWeb.Resolvers.Group do
def delete_group(
_parent,
%{group_id: group_id, actor_id: actor_id},
%{
context: %{
current_user: user
}
}
%{context: %{current_user: user}}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:is_owned, true, _} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Member.get_member(actor_id, group.id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- Member.is_administrator(member),
group <- Actors.delete_group!(group) do
{:ok, %{id: group.id}}
@ -101,7 +88,7 @@ defmodule MobilizonWeb.Resolvers.Group do
{:error, :group_not_found} ->
{:error, "Group not found"}
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :member_not_found} ->
@ -122,39 +109,26 @@ defmodule MobilizonWeb.Resolvers.Group do
def join_group(
_parent,
%{group_id: group_id, actor_id: actor_id},
%{
context: %{
current_user: user
}
}
%{context: %{current_user: user}}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:is_owned, true, actor} <- User.owns_actor(user, actor_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:error, :member_not_found} <- Member.get_member(actor.id, group.id),
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
role <- Mobilizon.Actors.get_default_member_role(group),
{:ok, _} <-
Actors.create_member(%{
parent_id: group.id,
actor_id: actor.id,
role: role
}) do
role <- Member.get_default_member_role(group),
{:ok, _} <- Actors.create_member(%{parent_id: group.id, actor_id: actor.id, role: role}) do
{
:ok,
%{
parent:
group
|> Person.proxify_pictures(),
actor:
actor
|> Person.proxify_pictures(),
parent: Person.proxify_pictures(group),
actor: Person.proxify_pictures(actor),
role: role
}
}
else
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :group_not_found} ->
@ -178,33 +152,19 @@ defmodule MobilizonWeb.Resolvers.Group do
def leave_group(
_parent,
%{group_id: group_id, actor_id: actor_id},
%{
context: %{
current_user: user
}
}
%{context: %{current_user: user}}
) do
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:is_owned, true, actor} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Member.get_member(actor.id, group_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id),
{:only_administrator, false} <-
{:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)},
{:ok, _} <-
Mobilizon.Actors.delete_member(member) do
{
:ok,
%{
parent: %{
id: group_id
},
actor: %{
id: actor_id
}
}
}
{:ok, %{parent: %{id: group_id}, actor: %{id: actor_id}}}
else
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:error, :member_not_found} ->
@ -224,14 +184,8 @@ defmodule MobilizonWeb.Resolvers.Group do
# and that it's the actor requesting leaving the group we return true
@spec check_that_member_is_not_last_administrator(integer(), integer()) :: boolean()
defp check_that_member_is_not_last_administrator(group_id, actor_id) do
case Member.list_administrator_members_for_group(group_id) do
[
%Member{
actor: %Actor{
id: member_actor_id
}
}
] ->
case Actors.list_administrator_members_for_group(group_id) do
[%Member{actor: %Actor{id: member_actor_id}}] ->
actor_id == member_actor_id
_ ->

View File

@ -9,7 +9,7 @@ defmodule MobilizonWeb.Resolvers.Member do
Find members for group
"""
def find_members_for_group(%Actor{} = actor, _args, _resolution) do
members = Actors.memberships_for_group(actor)
members = Actors.list_members_for_group(actor)
{:ok, members}
end
end

View File

@ -2,12 +2,13 @@ defmodule MobilizonWeb.Resolvers.Person do
@moduledoc """
Handles the person-related GraphQL calls
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Users.User
alias Mobilizon.Users
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Users
alias Mobilizon.Users.User
@doc """
Find a person
@ -50,9 +51,7 @@ defmodule MobilizonWeb.Resolvers.Person do
def create_person(
_parent,
%{preferred_username: _preferred_username} = args,
%{
context: %{current_user: user}
} = _resolution
%{context: %{current_user: user}} = _resolution
) do
args = Map.put(args, :user_id, user.id)
@ -75,17 +74,13 @@ defmodule MobilizonWeb.Resolvers.Person do
def update_person(
_parent,
%{preferred_username: preferred_username} = args,
%{
context: %{
current_user: user
}
} = _resolution
%{context: %{current_user: user}} = _resolution
) do
args = Map.put(args, :user_id, user.id)
with {:find_actor, %Actor{} = actor} <-
{:find_actor, Actors.get_actor_by_name(preferred_username)},
{:is_owned, true, _} <- User.owns_actor(user, actor.id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
args <- save_attached_pictures(args),
{:ok, actor} <- Actors.update_actor(actor, args) do
{:ok, actor}
@ -93,7 +88,7 @@ defmodule MobilizonWeb.Resolvers.Person do
{:find_actor, nil} ->
{:error, "Actor not found"}
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor is not owned by authenticated user"}
end
end
@ -108,15 +103,11 @@ defmodule MobilizonWeb.Resolvers.Person do
def delete_person(
_parent,
%{preferred_username: preferred_username} = _args,
%{
context: %{
current_user: user
}
} = _resolution
%{context: %{current_user: user}} = _resolution
) do
with {:find_actor, %Actor{} = actor} <-
{:find_actor, Actors.get_actor_by_name(preferred_username)},
{:is_owned, true, _} <- User.owns_actor(user, actor.id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
{:last_identity, false} <- {:last_identity, last_identity?(user)},
{:last_admin, false} <- {:last_admin, last_admin_of_a_group?(actor.id)},
{:ok, actor} <- Actors.delete_actor(actor) do
@ -131,7 +122,7 @@ defmodule MobilizonWeb.Resolvers.Person do
{:last_admin, true} ->
{:error, "Cannot remove the last administrator of a group"}
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor is not owned by authenticated user"}
end
end
@ -184,14 +175,12 @@ defmodule MobilizonWeb.Resolvers.Person do
@doc """
Returns the list of events this person is going to
"""
def person_going_to_events(%Actor{id: actor_id}, _args, %{
context: %{current_user: user}
}) do
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
def person_going_to_events(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
events <- Events.list_event_participations_for_actor(actor) do
{:ok, events}
else
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
end
end
@ -199,9 +188,7 @@ defmodule MobilizonWeb.Resolvers.Person do
@doc """
Returns the list of events this person is going to
"""
def person_going_to_events(_parent, %{}, %{
context: %{current_user: user}
}) do
def person_going_to_events(_parent, %{}, %{context: %{current_user: user}}) do
with %Actor{} = actor <- Users.get_actor_for_user(user),
events <- Events.list_event_participations_for_actor(actor) do
{:ok, events}
@ -220,7 +207,7 @@ defmodule MobilizonWeb.Resolvers.Person do
# We check that the actor is not the last administrator/creator of a group
@spec last_admin_of_a_group?(integer()) :: boolean()
defp last_admin_of_a_group?(actor_id) do
length(Member.list_group_id_where_last_administrator(actor_id)) > 0
length(Actors.list_group_ids_where_last_administrator(actor_id)) > 0
end
@spec proxify_avatar(Actor.t()) :: Actor.t()

View File

@ -2,6 +2,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
@moduledoc """
Handles the picture-related GraphQL calls
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Media
alias Mobilizon.Media.Picture
alias Mobilizon.Users.User
@ -10,9 +11,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
Get picture for an event's pic
"""
def picture(%{picture_id: picture_id} = _parent, _args, _resolution) do
with {:ok, picture} <- do_fetch_picture(picture_id) do
{:ok, picture}
end
with {:ok, picture} <- do_fetch_picture(picture_id), do: {:ok, picture}
end
@doc """
@ -20,15 +19,9 @@ defmodule MobilizonWeb.Resolvers.Picture do
See MobilizonWeb.Resolvers.Event.create_event/3
"""
def picture(%{picture: picture} = _parent, _args, _resolution) do
{:ok, picture}
end
def picture(%{picture: picture} = _parent, _args, _resolution), do: {:ok, picture}
def picture(_parent, %{id: picture_id}, _resolution), do: do_fetch_picture(picture_id)
def picture(_parent, _args, _resolution) do
{:ok, nil}
end
def picture(_parent, _args, _resolution), do: {:ok, nil}
@spec do_fetch_picture(nil) :: {:error, nil}
defp do_fetch_picture(nil), do: {:error, nil}
@ -36,7 +29,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
@spec do_fetch_picture(String.t()) :: {:ok, Picture.t()} | {:error, :not_found}
defp do_fetch_picture(picture_id) do
case Media.get_picture(picture_id) do
%Picture{id: id, file: file} = _pic ->
%Picture{id: id, file: file} ->
{:ok,
%{
name: file.name,
@ -46,18 +39,18 @@ defmodule MobilizonWeb.Resolvers.Picture do
size: file.size
}}
_err ->
_error ->
{:error, "Picture with ID #{picture_id} was not found"}
end
end
@spec upload_picture(map(), map(), map()) :: {:ok, Picture.t()} | {:error, any()}
def upload_picture(_parent, %{file: %Plug.Upload{} = file, actor_id: actor_id} = args, %{
context: %{
current_user: user
}
}) do
with {:is_owned, true, _actor} <- User.owns_actor(user, actor_id),
def upload_picture(
_parent,
%{file: %Plug.Upload{} = file, actor_id: actor_id} = args,
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %{"url" => [%{"href" => url, "mediaType" => content_type}], "size" => size}} <-
MobilizonWeb.Upload.store(file),
args <-
@ -76,11 +69,11 @@ defmodule MobilizonWeb.Resolvers.Picture do
size: picture.file.size
}}
else
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
err ->
{:error, err}
error ->
{:error, error}
end
end

View File

@ -10,9 +10,11 @@ defmodule MobilizonWeb.Resolvers.Report do
alias MobilizonWeb.API.Reports, as: ReportsAPI
import Mobilizon.Users.Guards
def list_reports(_parent, %{page: page, limit: limit, status: status}, %{
context: %{current_user: %User{role: role}}
})
def list_reports(
_parent,
%{page: page, limit: limit, status: status},
%{context: %{current_user: %User{role: role}}}
)
when is_moderator(role) do
{:ok, Mobilizon.Reports.list_reports(page, limit, :updated_at, :desc, status)}
end
@ -21,9 +23,7 @@ defmodule MobilizonWeb.Resolvers.Report do
{:error, "You need to be logged-in and a moderator to list reports"}
end
def get_report(_parent, %{id: id}, %{
context: %{current_user: %User{role: role}}
})
def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do
case Mobilizon.Reports.get_report(id) do
%Report{} = report ->
@ -46,14 +46,14 @@ defmodule MobilizonWeb.Resolvers.Report do
%{reporter_actor_id: reporter_actor_id} = args,
%{context: %{current_user: user}} = _resolution
) do
with {:is_owned, true, _} <- User.owns_actor(user, reporter_actor_id),
with {:is_owned, %Actor{}} <- User.owns_actor(user, reporter_actor_id),
{:ok, _, %Report{} = report} <- ReportsAPI.report(args) do
{:ok, report}
else
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Reporter actor id is not owned by authenticated user"}
_err ->
_error ->
{:error, "Error while saving report"}
end
end
@ -68,22 +68,19 @@ defmodule MobilizonWeb.Resolvers.Report do
def update_report(
_parent,
%{report_id: report_id, moderator_id: moderator_id, status: status},
%{
context: %{current_user: %User{role: role} = user}
}
%{context: %{current_user: %User{role: role} = user}}
)
when is_moderator(role) do
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
%Actor{} = actor <- Actors.get_actor!(moderator_id),
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, moderator_id),
%Report{} = report <- Mobilizon.Reports.get_report(report_id),
{:ok, %Report{} = report} <-
MobilizonWeb.API.Reports.update_report_status(actor, report, status) do
{:ok, report}
else
{:is_owned, false} ->
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
_err ->
_error ->
{:error, "Error while updating report"}
end
end
@ -95,27 +92,27 @@ defmodule MobilizonWeb.Resolvers.Report do
def create_report_note(
_parent,
%{report_id: report_id, moderator_id: moderator_id, content: content},
%{
context: %{current_user: %User{role: role} = user}
}
%{context: %{current_user: %User{role: role} = user}}
)
when is_moderator(role) do
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
%Report{} = report <- Reports.get_report(report_id),
%Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id),
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
{:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.create_report_note(report, moderator, content) do
{:ok, note}
end
end
def delete_report_note(_parent, %{note_id: note_id, moderator_id: moderator_id}, %{
context: %{current_user: %User{role: role} = user}
})
def delete_report_note(
_parent,
%{note_id: note_id, moderator_id: moderator_id},
%{context: %{current_user: %User{role: role} = user}}
)
when is_moderator(role) do
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
%Note{} = note <- Reports.get_note(note_id),
%Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id),
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
{:ok, %Note{} = note} <-
MobilizonWeb.API.Reports.delete_report_note(note, moderator) do
{:ok, %{id: note.id}}

View File

@ -33,7 +33,7 @@ defmodule MobilizonWeb.Resolvers.Tag do
# """
# def get_related_tags(_parent, %{tag_id: tag_id}, _resolution) do
# with %Tag{} = tag <- Mobilizon.Events.get_tag!(tag_id),
# tags <- Mobilizon.Events.tag_neighbors(tag) do
# tags <- Mobilizon.Events.list_tag_neighbors(tag) do
# {:ok, tags}
# end
# end
@ -42,7 +42,7 @@ defmodule MobilizonWeb.Resolvers.Tag do
Retrieve the list of related tags for a parent tag
"""
def get_related_tags(%Tag{} = tag, _args, _resolution) do
with tags <- Mobilizon.Events.tag_neighbors(tag) do
with tags <- Mobilizon.Events.list_tag_neighbors(tag) do
{:ok, tags}
end
end

View File

@ -2,12 +2,14 @@ defmodule MobilizonWeb.Resolvers.User do
@moduledoc """
Handles the user-related GraphQL calls
"""
alias Mobilizon.{Actors, Config, Users}
alias Mobilizon.Actors.Actor
alias Mobilizon.CommonConfig
alias Mobilizon.Users.User
alias Mobilizon.{Actors, Users}
alias Mobilizon.Service.Users.{ResetPassword, Activation}
alias Mobilizon.Users.User
import Mobilizon.Users.Guards
require Logger
@doc """
@ -110,7 +112,8 @@ defmodule MobilizonWeb.Resolvers.User do
"""
@spec create_user(any(), map(), any()) :: tuple()
def create_user(_parent, args, _resolution) do
with {:registrations_open, true} <- {:registrations_open, CommonConfig.registrations_open?()},
with {:registrations_open, true} <-
{:registrations_open, Config.instance_registrations_open?()},
{:ok, %User{} = user} <- Users.register(args) do
Activation.send_confirmation_email(user)
{:ok, user}
@ -118,8 +121,8 @@ defmodule MobilizonWeb.Resolvers.User do
{:registrations_open, false} ->
{:error, "Registrations are not enabled"}
err ->
err
error ->
error
end
end
@ -139,9 +142,9 @@ defmodule MobilizonWeb.Resolvers.User do
user: Map.put(user, :default_actor, actor)
}}
else
err ->
error ->
Logger.info("Unable to validate user with token #{token}")
Logger.debug(inspect(err))
Logger.debug(inspect(error))
{:error, "Unable to validate user"}
end
end
@ -213,7 +216,7 @@ defmodule MobilizonWeb.Resolvers.User do
{:user_actor, _} ->
{:error, :actor_not_from_user}
_err ->
_error ->
{:error, :unable_to_change_default_actor}
end
end

View File

@ -260,7 +260,7 @@ defmodule MobilizonWeb.ReverseProxy do
headers,
"user-agent",
0,
{"user-agent", Mobilizon.Application.user_agent()}
{"user-agent", Mobilizon.user_agent()}
)
else
headers

View File

@ -7,6 +7,7 @@ defmodule MobilizonWeb.Schema do
alias Mobilizon.{Actors, Events, Users, Addresses, Media, Reports}
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Storage.Repo
import_types(MobilizonWeb.Schema.Custom.UUID)
import_types(MobilizonWeb.Schema.Custom.Point)
@ -87,14 +88,17 @@ defmodule MobilizonWeb.Schema do
end
def context(ctx) do
default_query = fn queryable, _params -> queryable end
default_source = Dataloader.Ecto.new(Repo, query: default_query)
loader =
Dataloader.new()
|> Dataloader.add_source(Actors, Actors.data())
|> Dataloader.add_source(Users, Users.data())
|> Dataloader.add_source(Events, Events.data())
|> Dataloader.add_source(Addresses, Addresses.data())
|> Dataloader.add_source(Media, Media.data())
|> Dataloader.add_source(Reports, Reports.data())
|> Dataloader.add_source(Actors, default_source)
|> Dataloader.add_source(Users, default_source)
|> Dataloader.add_source(Events, default_source)
|> Dataloader.add_source(Addresses, default_source)
|> Dataloader.add_source(Media, default_source)
|> Dataloader.add_source(Reports, default_source)
Map.put(ctx, :loader, loader)
end

View File

@ -31,7 +31,13 @@ defmodule MobilizonWeb.Upload do
* `MobilizonWeb.Upload.Filter`
"""
alias Ecto.UUID
alias Mobilizon.Config
alias MobilizonWeb.MIME
require Logger
@type source ::
@ -110,33 +116,33 @@ defmodule MobilizonWeb.Upload do
{size_limit, activity_type} =
case Keyword.get(opts, :type) do
:banner ->
{Mobilizon.CommonConfig.get!([:instance, :banner_upload_limit]), "Image"}
{Config.get!([:instance, :banner_upload_limit]), "Image"}
:avatar ->
{Mobilizon.CommonConfig.get!([:instance, :avatar_upload_limit]), "Image"}
{Config.get!([:instance, :avatar_upload_limit]), "Image"}
_ ->
{Mobilizon.CommonConfig.get!([:instance, :upload_limit]), nil}
{Config.get!([:instance, :upload_limit]), nil}
end
%{
activity_type: Keyword.get(opts, :activity_type, activity_type),
size_limit: Keyword.get(opts, :size_limit, size_limit),
uploader: Keyword.get(opts, :uploader, Mobilizon.CommonConfig.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Mobilizon.CommonConfig.get([__MODULE__, :filters])),
uploader: Keyword.get(opts, :uploader, Config.get([__MODULE__, :uploader])),
filters: Keyword.get(opts, :filters, Config.get([__MODULE__, :filters])),
description: Keyword.get(opts, :description),
base_url:
Keyword.get(
opts,
:base_url,
Mobilizon.CommonConfig.get([__MODULE__, :base_url], MobilizonWeb.Endpoint.url())
Config.get([__MODULE__, :base_url], MobilizonWeb.Endpoint.url())
)
}
end
defp prepare_upload(%Plug.Upload{} = file, opts) do
with {:ok, size} <- check_file_size(file.path, opts.size_limit),
{:ok, content_type, name} <- Mobilizon.MIME.file_mime_type(file.path, file.filename) do
{:ok, content_type, name} <- MIME.file_mime_type(file.path, file.filename) do
{:ok,
%__MODULE__{
id: UUID.generate(),
@ -173,7 +179,7 @@ defmodule MobilizonWeb.Upload do
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
URI.encode(path, &char_unescaped?/1) <>
if Mobilizon.CommonConfig.get([__MODULE__, :link_name], false) do
if Config.get([__MODULE__, :link_name], false) do
"?name=#{URI.encode(name, &char_unescaped?/1)}"
else
""

View File

@ -9,11 +9,14 @@ defmodule MobilizonWeb.Upload.Filter.AnonymizeFilename do
Should be used after `MobilizonWeb.Upload.Filter.Dedupe`.
"""
@behaviour MobilizonWeb.Upload.Filter
alias Mobilizon.Config
def filter(upload) do
extension = List.last(String.split(upload.name, "."))
name = Mobilizon.CommonConfig.get([__MODULE__, :text], random(extension))
name = Config.get([__MODULE__, :text], random(extension))
{:ok, %MobilizonWeb.Upload{upload | name: name}}
end

View File

@ -7,13 +7,16 @@ defmodule MobilizonWeb.Upload.Filter.Mogrify do
@moduledoc """
Handle mogrify transformations
"""
@behaviour MobilizonWeb.Upload.Filter
alias Mobilizon.Config
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
@type conversions :: conversion() | [conversion()]
def filter(%MobilizonWeb.Upload{tempfile: file, content_type: "image" <> _}) do
filters = Mobilizon.CommonConfig.get!([__MODULE__, :args])
filters = Config.get!([__MODULE__, :args])
file
|> Mogrify.open()

View File

@ -7,8 +7,11 @@ defmodule MobilizonWeb.Uploaders.Local do
@moduledoc """
Local uploader for files
"""
@behaviour MobilizonWeb.Uploaders.Uploader
alias Mobilizon.Config
def get_file(_) do
{:ok, {:static_dir, upload_path()}}
end
@ -59,6 +62,6 @@ defmodule MobilizonWeb.Uploaders.Local do
end
def upload_path do
Mobilizon.CommonConfig.get!([__MODULE__, :uploads])
Config.get!([__MODULE__, :uploads])
end
end

View File

@ -1,10 +1,11 @@
defmodule MobilizonWeb.ActivityPub.ActorView do
use MobilizonWeb, :view
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Activity
@private_visibility_empty_collection %{elements: [], total: 0}
@ -47,8 +48,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("following.json", %{actor: actor, page: page}) do
%{total: total, elements: following} =
if Actor.public_visibility?(actor),
do: Actor.get_followings(actor, page),
if Actor.is_public_visibility(actor),
do: Actors.build_followings_for_actor(actor, page),
else: @private_visibility_empty_collection
following
@ -58,8 +59,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("following.json", %{actor: actor}) do
%{total: total, elements: following} =
if Actor.public_visibility?(actor),
do: Actor.get_followings(actor),
if Actor.is_public_visibility(actor),
do: Actors.build_followings_for_actor(actor),
else: @private_visibility_empty_collection
%{
@ -73,8 +74,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("followers.json", %{actor: actor, page: page}) do
%{total: total, elements: followers} =
if Actor.public_visibility?(actor),
do: Actor.get_followers(actor, page),
if Actor.is_public_visibility(actor),
do: Actors.build_followers_for_actor(actor, page),
else: @private_visibility_empty_collection
followers
@ -84,8 +85,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("followers.json", %{actor: actor}) do
%{total: total, elements: followers} =
if Actor.public_visibility?(actor),
do: Actor.get_followers(actor),
if Actor.is_public_visibility(actor),
do: Actors.build_followers_for_actor(actor),
else: @private_visibility_empty_collection
%{
@ -99,7 +100,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("outbox.json", %{actor: actor, page: page}) do
%{total: total, elements: followers} =
if Actor.public_visibility?(actor),
if Actor.is_public_visibility(actor),
do: ActivityPub.fetch_public_activities_for_actor(actor, page),
else: @private_visibility_empty_collection
@ -110,7 +111,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
def render("outbox.json", %{actor: actor}) do
%{total: total, elements: followers} =
if Actor.public_visibility?(actor),
if Actor.is_public_visibility(actor),
do: ActivityPub.fetch_public_activities_for_actor(actor),
else: @private_visibility_empty_collection

View File

@ -1,7 +1,7 @@
defmodule MobilizonWeb.ActivityPub.ObjectView do
use MobilizonWeb, :view
alias Mobilizon.Service.ActivityPub.Utils
alias Mobilizon.Activity
alias Mobilizon.Service.ActivityPub.{Activity, Utils}
def render("activity.json", %{activity: %Activity{local: local, data: data} = activity}) do
%{

View File

@ -1,3 +1,3 @@
defmodule Mobilizon.EmailView do
defmodule MobilizonWeb.EmailView do
use MobilizonWeb, :view
end

View File

@ -0,0 +1,21 @@
defmodule Mobilizon.Service.ActivityPub.Activity do
@moduledoc """
Represents an activity.
"""
@type t :: %__MODULE__{
data: String.t(),
local: boolean,
actor: Actor.t(),
recipients: [String.t()]
# notifications: [???]
}
defstruct [
:data,
:local,
:actor,
:recipients
# :notifications
]
end

View File

@ -10,11 +10,11 @@ defmodule Mobilizon.Service.ActivityPub do
Every ActivityPub method
"""
alias Mobilizon.Config
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Service.ActivityPub.Transmogrifier
alias Mobilizon.Service.WebFinger
alias Mobilizon.Activity
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower}
@ -22,11 +22,10 @@ defmodule Mobilizon.Service.ActivityPub do
alias Mobilizon.Service.Federator
alias Mobilizon.Service.HTTPSignatures.Signature
alias Mobilizon.Service.ActivityPub.Convertible
alias Mobilizon.Service.ActivityPub.{Activity, Convertible}
require Logger
import Mobilizon.Service.ActivityPub.Utils
import Mobilizon.Service.ActivityPub.Visibility
import Mobilizon.Service.ActivityPub.{Utils, Visibility}
@doc """
Get recipients for an activity or object
@ -84,10 +83,10 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
case data["type"] do
"Event" ->
{:ok, Events.get_event_full_by_url!(object_url)}
{:ok, Events.get_public_event_by_url_with_preload!(object_url)}
"Note" ->
{:ok, Events.get_comment_full_from_url!(object_url)}
{:ok, Events.get_comment_from_url_with_preload!(object_url)}
"Actor" ->
{:ok, Actors.get_actor_by_url!(object_url, true)}
@ -97,10 +96,10 @@ defmodule Mobilizon.Service.ActivityPub do
end
else
{:existing_event, %Event{url: event_url}} ->
{:ok, Events.get_event_full_by_url!(event_url)}
{:ok, Events.get_public_event_by_url_with_preload!(event_url)}
{:existing_comment, %Comment{url: comment_url}} ->
{:ok, Events.get_comment_full_from_url!(comment_url)}
{:ok, Events.get_comment_from_url_with_preload!(comment_url)}
{:existing_actor, {:ok, %Actor{url: actor_url}}} ->
{:ok, Actors.get_actor_by_url!(actor_url, true)}
@ -112,6 +111,28 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
@doc """
Getting an actor from url, eventually creating it
"""
@spec get_or_fetch_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_by_url(url, preload \\ false) do
case Actors.get_actor_by_url(url, preload) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
case make_actor_from_url(url, preload) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
_ ->
Logger.warn("Could not fetch by AP id")
{:error, "Could not fetch by AP id"}
end
end
end
@doc """
Create an activity of type "Create"
"""
@ -278,7 +299,7 @@ defmodule Mobilizon.Service.ActivityPub do
"""
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{url: follow_url}} <-
Actor.follow(followed, follower, activity_id, false),
Actors.follow(followed, follower, activity_id, false),
activity_follow_id <-
activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id),
@ -297,7 +318,7 @@ defmodule Mobilizon.Service.ActivityPub do
"""
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower),
with {:ok, %Follower{id: follow_id}} <- Actors.unfollow(followed, follower),
# We recreate the follow activity
data <-
make_follow_data(
@ -437,8 +458,7 @@ defmodule Mobilizon.Service.ActivityPub do
local
) do
with {:only_organizer, false} <-
{:only_organizer,
Participant.check_that_participant_is_not_only_organizer(event_id, actor_id)},
{:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
{:ok, %Participant{} = participant} <-
Mobilizon.Events.get_participant(event_id, actor_id),
{:ok, %Participant{} = participant} <- Mobilizon.Events.delete_participant(participant),
@ -464,7 +484,7 @@ defmodule Mobilizon.Service.ActivityPub do
def make_actor_from_url(url, preload \\ false) do
case fetch_and_prepare_actor_from_url(url) do
{:ok, data} ->
Actors.insert_or_update_actor(data, preload)
Actors.upsert_actor(data, preload)
# Request returned 410
{:error, :actor_deleted} ->
@ -520,15 +540,14 @@ defmodule Mobilizon.Service.ActivityPub do
public = is_public?(activity)
if public && is_delete_activity?(activity) == false &&
Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
if public && !is_delete_activity?(activity) && Config.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Mobilizon.Service.ActivityPub.Relay.publish(activity)
end
followers =
if actor.followers_url in activity.recipients do
Actor.get_full_external_followers(actor)
Actors.list_external_followers_for_actor(actor)
else
[]
end
@ -664,8 +683,8 @@ defmodule Mobilizon.Service.ActivityPub do
"""
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map()
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do
{:ok, events, total_events} = Events.get_public_events_for_actor(actor, page, limit)
{:ok, comments, total_comments} = Events.get_public_comments_for_actor(actor, page, limit)
{:ok, events, total_events} = Events.list_public_events_for_actor(actor, page, limit)
{:ok, comments, total_comments} = Events.list_public_comments_for_actor(actor, page, limit)
event_activities = Enum.map(events, &event_to_activity/1)

View File

@ -33,7 +33,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferredUsername"],
"summary" => object["summary"],
"url" => object["url"],
"url" => object["id"],
"name" => object["name"],
"avatar" => avatar,
"banner" => banner,

View File

@ -4,7 +4,6 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
This module allows to convert events from ActivityStream format to our own internal one, and back
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment, as: CommentModel
alias Mobilizon.Events.Event
@ -20,7 +19,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
@impl Converter
@spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do
{:ok, %Actor{id: actor_id}} = Actors.get_or_fetch_by_url(object["actor"])
{:ok, %Actor{id: actor_id}} = ActivityPub.get_or_fetch_by_url(object["actor"])
Logger.debug("Inserting full comment")
Logger.debug(inspect(object))

View File

@ -8,23 +8,25 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
Handles following and unfollowing relays and instances
"""
alias Mobilizon.Activity
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.API.Follows
require Logger
def get_actor do
with {:ok, %Actor{} = actor} <-
Actors.get_or_create_service_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
Actors.get_or_create_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
actor
end
end
def follow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.follow(local_actor, target_actor) do
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
@ -37,7 +39,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def unfollow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.unfollow(local_actor, target_actor) do
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
@ -50,7 +52,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def accept(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, activity} <- Follows.accept(target_actor, local_actor) do
{:ok, activity}
end
@ -58,7 +60,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
# def reject(target_instance) do
# with %Actor{} = local_actor <- get_actor(),
# {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
# {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_by_url(target_instance),
# {:ok, activity} <- Follows.reject(target_actor, local_actor) do
# {:ok, activity}
# end

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