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 48fd14bf9c
140 changed files with 4498 additions and 5039 deletions

View File

@ -7,7 +7,7 @@ use Mix.Config
# General application configuration # General application configuration
config :mobilizon, config :mobilizon,
ecto_repos: [Mobilizon.Repo] ecto_repos: [Mobilizon.Storage.Repo]
config :mobilizon, :instance, config :mobilizon, :instance,
name: System.get_env("MOBILIZON_INSTANCE_NAME") || "Localhost", name: System.get_env("MOBILIZON_INSTANCE_NAME") || "Localhost",
@ -78,7 +78,7 @@ config :mobilizon, MobilizonWeb.Guardian,
secret_key: "ty0WM7YBE3ojvxoUQxo8AERrNpfbXnIJ82ovkPdqbUFw31T5LcK8wGjaOiReVQjo" secret_key: "ty0WM7YBE3ojvxoUQxo8AERrNpfbXnIJ82ovkPdqbUFw31T5LcK8wGjaOiReVQjo"
config :guardian, Guardian.DB, config :guardian, Guardian.DB,
repo: Mobilizon.Repo, repo: Mobilizon.Storage.Repo,
# default # default
schema_name: "guardian_tokens", schema_name: "guardian_tokens",
# store all token types if not set # 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 # Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime config :phoenix, :plug_init_mode, :runtime
config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.LocalAdapter config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.LocalAdapter
# Configure your database # Configure your database
config :mobilizon, Mobilizon.Repo, config :mobilizon, Mobilizon.Storage.Repo,
types: Mobilizon.PostgresTypes, types: Mobilizon.Storage.PostgresTypes,
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon", username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon", password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_dev", 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" cache_static_manifest: "priv/static/manifest.json"
# Configure your database # Configure your database
config :mobilizon, Mobilizon.Repo, config :mobilizon, Mobilizon.Storage.Repo,
types: Mobilizon.PostgresTypes, types: Mobilizon.Storage.PostgresTypes,
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon", username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon", password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_prod", 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", port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432",
pool_size: 15 pool_size: 15
config :mobilizon, Mobilizon.Mailer, config :mobilizon, MobilizonWeb.Email.Mailer,
adapter: Bamboo.SMTPAdapter, adapter: Bamboo.SMTPAdapter,
server: "localhost", server: "localhost",
hostname: "localhost", hostname: "localhost",

View File

@ -22,16 +22,15 @@ config :logger,
level: :info level: :info
# Configure your database # Configure your database
config :mobilizon, Mobilizon.Repo, config :mobilizon, Mobilizon.Storage.Repo,
types: Mobilizon.PostgresTypes, types: Mobilizon.Storage.PostgresTypes,
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon", username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon", password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_test", database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_test",
hostname: System.get_env("MOBILIZON_DATABASE_HOST") || "localhost", hostname: System.get_env("MOBILIZON_DATABASE_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox, pool: Ecto.Adapters.SQL.Sandbox
types: Mobilizon.PostgresTypes
config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.TestAdapter config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.TestAdapter
config :mobilizon, MobilizonWeb.Upload, filters: [], link_name: false 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") Mix.Task.run("app.start")
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true), 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} <- {:ok, %Bot{} = bot} <-
Actors.create_bot(%{ Actors.create_bot(%{
"type" => type, "type" => type,

View File

@ -1,9 +1,92 @@
defmodule Mobilizon do defmodule Mobilizon do
@moduledoc """ @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 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,52 +1,115 @@
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 defmodule Mobilizon.Actors.Actor do
@moduledoc """ @moduledoc """
Represents an actor (local and remote actors) Represents an actor (local and remote).
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Actors alias Mobilizon.{Actors, Config, Crypto}
alias Mobilizon.Users.User alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member}
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Events.{Event, FeedToken} alias Mobilizon.Events.{Event, FeedToken}
alias Mobilizon.Media.File alias Mobilizon.Media.File
alias Mobilizon.Reports.{Report, Note} alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Users.User
alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint alias MobilizonWeb.Endpoint
import Ecto.Query
import Mobilizon.Ecto
alias Mobilizon.Repo
require Logger 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]
}
@required_attrs [:preferred_username, :keys, :suspended, :url]
@optional_attrs [
:outbox_url,
:inbox_url,
:shared_inbox_url,
:following_url,
:followers_url,
:type,
:name,
:domain,
:summary,
:manually_approves_followers,
:user_id
]
@attrs @required_attrs ++ @optional_attrs
@update_required_attrs @required_attrs -- [:url]
@update_optional_attrs [:name, :summary, :manually_approves_followers, :user_id]
@update_attrs @update_required_attrs ++ @update_optional_attrs
@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
@remote_actor_creation_required_attrs [
:url,
:inbox_url,
:type,
:domain,
:preferred_username,
:keys
]
@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
@relay_creation_attrs [
:type,
:name,
:summary,
:url,
:keys,
:preferred_username,
:domain,
:inbox_url,
:followers_url,
:following_url,
:shared_inbox_url
]
@group_creation_required_attrs [:url, :outbox_url, :inbox_url, :type, :preferred_username]
@group_creation_optional_attrs [:shared_inbox_url, :name, :domain, :summary]
@group_creation_attrs @group_creation_required_attrs ++ @group_creation_optional_attrs
schema "actors" do schema "actors" do
field(:url, :string) field(:url, :string)
@ -55,258 +118,212 @@ defmodule Mobilizon.Actors.Actor do
field(:following_url, :string) field(:following_url, :string)
field(:followers_url, :string) field(:followers_url, :string)
field(:shared_inbox_url, :string) field(:shared_inbox_url, :string)
field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person) field(:type, ActorType, default: :Person)
field(:name, :string) field(:name, :string)
field(:domain, :string, default: nil) field(:domain, :string, default: nil)
field(:summary, :string) field(:summary, :string)
field(:preferred_username, :string) field(:preferred_username, :string)
field(:keys, :string) field(:keys, :string)
field(:manually_approves_followers, :boolean, default: false) field(:manually_approves_followers, :boolean, default: false)
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated) field(:openness, ActorOpenness, default: :moderated)
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private) field(:visibility, ActorVisibility, default: :private)
field(:suspended, :boolean, default: false) field(:suspended, :boolean, default: false)
# field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
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(:followers, Follower, foreign_key: :target_actor_id)
has_many(:followings, Follower, foreign_key: :actor_id) has_many(:followings, Follower, foreign_key: :actor_id)
has_many(:organized_events, Event, foreign_key: :organizer_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) 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(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id) has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id) has_many(:report_notes, Note, foreign_key: :moderator_id)
many_to_many(:memberships, __MODULE__, join_through: Member)
timestamps() timestamps()
end 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 @doc false
def changeset(%Actor{} = actor, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = actor, attrs) do
actor actor
|> Ecto.Changeset.cast(attrs, [ |> cast(attrs, @attrs)
:url,
:outbox_url,
:inbox_url,
:shared_inbox_url,
:following_url,
:followers_url,
:type,
:name,
:domain,
:summary,
:preferred_username,
:keys,
:manually_approves_followers,
:suspended,
:user_id
])
|> build_urls() |> build_urls()
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
|> unique_username_validator() |> unique_username_validator()
|> validate_required([:preferred_username, :keys, :suspended, :url]) |> validate_required(@required_attrs)
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) |> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index) |> unique_constraint(:url, name: :actors_url_index)
end end
@doc false @doc false
def update_changeset(%Actor{} = actor, attrs) do @spec update_changeset(t, map) :: Ecto.Changeset.t()
def update_changeset(%__MODULE__{} = actor, attrs) do
actor actor
|> Ecto.Changeset.cast(attrs, [ |> cast(attrs, @update_attrs)
:name,
:summary,
:keys,
:manually_approves_followers,
:suspended,
:user_id
])
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
|> validate_required([:preferred_username, :keys, :suspended, :url]) |> validate_required(@update_required_attrs)
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) |> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index) |> unique_constraint(:url, name: :actors_url_index)
end end
@doc """ @doc """
Changeset for person registration Changeset for person registration.
""" """
@spec registration_changeset(struct(), map()) :: Ecto.Changeset.t() @spec registration_changeset(t, map) :: Ecto.Changeset.t()
def registration_changeset(%Actor{} = actor, attrs) do def registration_changeset(%__MODULE__{} = actor, attrs) do
actor actor
|> Ecto.Changeset.cast(attrs, [ |> cast(attrs, @registration_attrs)
:preferred_username,
:domain,
:name,
:summary,
:keys,
:suspended,
:url,
:type,
:user_id
])
|> build_urls() |> build_urls()
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator() |> unique_username_validator()
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) |> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index) |> unique_constraint(:url, name: :actors_url_index)
|> validate_required([:preferred_username, :keys, :suspended, :url, :type]) |> validate_required(@registration_required_attrs)
end end
# 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 """ @doc """
Changeset for remote actor creation Changeset for remote actor creation.
""" """
@spec remote_actor_creation(map()) :: Ecto.Changeset.t() @spec remote_actor_creation_changeset(map) :: Ecto.Changeset.t()
def remote_actor_creation(params) do def remote_actor_creation_changeset(attrs) do
changes = changeset =
%Actor{} %__MODULE__{}
|> Ecto.Changeset.cast(params, [ |> cast(attrs, @remote_actor_creation_attrs)
:url, |> validate_required(@remote_actor_creation_required_attrs)
:outbox_url,
:inbox_url,
:shared_inbox_url,
:following_url,
:followers_url,
:type,
:name,
:domain,
:summary,
:preferred_username,
:keys,
:manually_approves_followers
])
|> validate_required([
:url,
:inbox_url,
:type,
:domain,
:preferred_username,
:keys
])
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator() |> unique_username_validator()
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) |> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index) |> unique_constraint(:url, name: :actors_url_index)
|> validate_length(:summary, max: 5000) |> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100) |> validate_length(:preferred_username, max: 100)
Logger.debug("Remote actor creation") Logger.debug("Remote actor creation: #{inspect(changeset)}")
Logger.debug(inspect(changes))
changes changeset
end end
def relay_creation(%{url: url, preferred_username: preferred_username} = _params) do @doc """
key = :public_key.generate_key({:rsa, 2048, 65_537}) Changeset for relay creation.
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) """
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing() @spec relay_creation_changeset(map) :: Ecto.Changeset.t()
def relay_creation_changeset(attrs) do
relay_creation_attrs = build_relay_creation_attrs(attrs)
vars = %{ cast(%__MODULE__{}, relay_creation_attrs, @relay_creation_attrs)
"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, [
:type,
:name,
:summary,
:url,
:keys,
:preferred_username,
:domain,
:inbox_url,
:followers_url,
:following_url,
:shared_inbox_url
])
end end
@doc """ @doc """
Changeset for group creation Changeset for group creation
""" """
@spec group_creation(struct(), map()) :: Ecto.Changeset.t() @spec group_creation_changeset(t, map) :: Ecto.Changeset.t()
def group_creation(%Actor{} = actor, params) do def group_creation_changeset(%__MODULE__{} = actor, params) do
actor actor
|> Ecto.Changeset.cast(params, [ |> cast(params, @group_creation_attrs)
:url,
:outbox_url,
:inbox_url,
:shared_inbox_url,
:type,
:name,
:domain,
:summary,
:preferred_username
])
|> cast_embed(:avatar) |> cast_embed(:avatar)
|> cast_embed(:banner) |> cast_embed(:banner)
|> build_urls(:Group) |> build_urls(:Group)
|> put_change(:domain, nil) |> put_change(:domain, nil)
|> put_change(:keys, Actors.create_keys()) |> put_change(:keys, Crypto.generate_rsa_2048_private_key())
|> put_change(:type, :Group) |> put_change(:type, :Group)
|> unique_username_validator() |> unique_username_validator()
|> validate_required([:url, :outbox_url, :inbox_url, :type, :preferred_username]) |> validate_required(@group_creation_required_attrs)
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index) |> unique_constraint(:preferred_username,
name: :actors_preferred_username_domain_type_index
)
|> unique_constraint(:url, name: :actors_url_index) |> unique_constraint(:url, name: :actors_url_index)
|> validate_length(:summary, max: 5000) |> validate_length(:summary, max: 5000)
|> validate_length(:preferred_username, max: 100) |> validate_length(:preferred_username, max: 100)
end 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( defp unique_username_validator(
%Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset %Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset
) do ) do
with nil <- Map.get(changes, :domain, nil), with nil <- Map.get(changes, :domain, nil),
%Actor{preferred_username: _username} <- Actors.get_local_actor_by_name(username) do %__MODULE__{preferred_username: _} <- Actors.get_local_actor_by_name(username) do
changeset |> add_error(:preferred_username, "Username is already taken") add_error(changeset, :preferred_username, "Username is already taken")
else else
_ -> changeset _ -> changeset
end end
end end
# When we don't even have any preferred_username, don't even try validating preferred_username # When we don't even have any preferred_username, don't even try validating preferred_username
defp unique_username_validator(changeset) do defp unique_username_validator(changeset), do: changeset
changeset
end
@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(changeset, type \\ :Person)
defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do
changeset changeset
|> put_change( |> put_change(:outbox_url, build_url(username, :outbox))
:outbox_url, |> put_change(:followers_url, build_url(username, :followers))
build_url(username, :outbox) |> put_change(:following_url, build_url(username, :following))
) |> put_change(:inbox_url, build_url(username, :inbox))
|> 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(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox")
|> put_change(:url, build_url(username, :page)) |> put_change(:url, build_url(username, :page))
end end
@ -314,19 +331,19 @@ defmodule Mobilizon.Actors.Actor do
defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset
@doc """ @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(preferred_username, endpoint, args \\ [])
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
def build_url(preferred_username, :page, args) do def build_url(preferred_username, :page, args) do
Endpoint Endpoint
|> Routes.page_url(:actor, preferred_username, args) |> Routes.page_url(:actor, preferred_username, args)
|> URI.decode() |> URI.decode()
end end
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
def build_url(preferred_username, endpoint, args) def build_url(preferred_username, endpoint, args)
when endpoint in [:outbox, :following, :followers] do when endpoint in [:outbox, :following, :followers] do
Endpoint Endpoint
@ -334,267 +351,24 @@ defmodule Mobilizon.Actors.Actor do
|> URI.decode() |> URI.decode()
end end
@doc """ @spec build_relay_creation_attrs(map) :: map
Get a public key for a given ActivityPub actor ID (url) defp build_relay_creation_attrs(%{url: url, preferred_username: preferred_username}) do
""" %{
@spec get_public_key_for_url(String.t()) :: {:ok, String.t()} | {:error, atom()} "name" => Config.get([:instance, :name], "Mobilizon"),
def get_public_key_for_url(url) do "summary" =>
with {:ok, %Actor{keys: keys}} <- Actors.get_or_fetch_by_url(url), Config.get(
{:ok, public_key} <- prepare_public_key(keys) do [:instance, :description],
{:ok, public_key} "An internal service actor for this Mobilizon instance"
else ),
{:error, :pem_decode_error} -> "url" => url,
Logger.error("Error while decoding PEM") "keys" => Crypto.generate_rsa_2048_private_key(),
{:error, :pem_decode_error} "preferred_username" => preferred_username,
"domain" => nil,
_ -> "inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
Logger.error("Unable to fetch actor, so no keys for you") "followers_url" => "#{url}/followers",
{:error, :actor_fetch_error} "following_url" => "#{url}/following",
end "shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
end "type" => :Application
}
@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)
end end
end end

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,52 +1,66 @@
defmodule Mobilizon.Actors.Follower do defmodule Mobilizon.Actors.Follower do
@moduledoc """ @moduledoc """
Represents the following of an actor to another actor Represents the following of an actor to another actor.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Actors.Follower
alias Mobilizon.Actors.Actor 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 schema "followers" do
field(:approved, :boolean, default: false) field(:approved, :boolean, default: false)
field(:url, :string) field(:url, :string)
belongs_to(:target_actor, Actor) belongs_to(:target_actor, Actor)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
end end
@doc false @doc false
def changeset(%Follower{} = member, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
member def changeset(follower, attrs) do
|> cast(attrs, [:url, :approved, :target_actor_id, :actor_id]) follower
|> generate_url() |> cast(attrs, @attrs)
|> validate_required([:url, :approved, :target_actor_id, :actor_id]) |> ensure_url()
|> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index) |> validate_required(@required_attrs)
|> unique_constraint(:target_actor_id,
name: :followers_actor_target_actor_unique_index
)
end end
# If there's a blank URL that's because we're doing the first insert # If there's a blank URL that's because we're doing the first insert
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 case fetch_change(changeset, :url) do
{:ok, _url} -> changeset {:ok, _url} ->
:error -> do_generate_url(changeset) changeset
:error ->
generate_url(changeset)
end end
end end
# Most time just go with the given URL # 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() uuid = Ecto.UUID.generate()
changeset changeset
|> put_change( |> put_change(:id, uuid)
:url, |> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}")
"#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}"
)
|> put_change(
:id,
uuid
)
end end
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 defmodule Mobilizon.Actors.Member do
@moduledoc """ @moduledoc """
Represents the membership of an actor to a group Represents the membership of an actor to a group.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query, warn: false
import Mobilizon.Ecto
alias Mobilizon.Actors.Member alias Mobilizon.Actors.{Actor, MemberRole}
alias Mobilizon.Actors.Actor
alias Mobilizon.Repo @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 schema "members" do
field(:role, Mobilizon.Actors.MemberRoleEnum, default: :member) field(:role, MemberRole, default: :member)
belongs_to(:parent, Actor) belongs_to(:parent, Actor)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
timestamps() timestamps()
end 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 """ @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 @spec get_default_member_role(Actor.t()) :: atom
case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do def get_default_member_role(%Actor{openness: :open}), do: :member
nil -> {:error, :member_not_found} def get_default_member_role(%Actor{}), do: :not_approved
member -> {:ok, member}
end
end
@doc """ @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, openness: :invite_only}), do: false
def can_be_joined(%Actor{type: :Group}), do: true def can_be_joined(%Actor{type: :Group}), do: true
@doc """ @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 def is_administrator(%__MODULE__{role: :administrator}), do: {:is_admin, true}
Repo.all( def is_administrator(%__MODULE__{role: :creator}), do: {:is_admin, true}
from( def is_administrator(%__MODULE__{}), do: {:is_admin, false}
m in Member,
where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator), @doc false
preload: [:actor] @spec changeset(t, map) :: Ecto.Changeset.t()
) def changeset(%__MODULE__{} = member, attrs) do
|> paginate(page, limit) member
) |> cast(attrs, @attrs)
|> validate_required(@required_attrs)
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
end end
@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
)
)
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 end

View File

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

View File

@ -3,82 +3,36 @@ defmodule Mobilizon.Addresses do
The Addresses context. The Addresses context.
""" """
import Ecto.Query, warn: false import Ecto.Query
alias Mobilizon.Repo
require Logger
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.Storage.Repo
@geom_types [:point] require Logger
@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
@doc """ @doc """
Gets a single address. 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) def get_address(id), do: Repo.get(Address, id)
@doc """ @doc """
Gets a single address by it's url Gets a single address.
Raises `Ecto.NoResultsError` if the address does not exist.
## Examples
iex> get_address_by_url("https://mobilizon.social/addresses/4572")
%Address{}
iex> get_address_by_url("https://mobilizon.social/addresses/099")
nil
""" """
def get_address_by_url(url) do @spec get_address!(integer | String.t()) :: Address.t()
Repo.get_by(Address, url: url) def get_address!(id), do: Repo.get!(Address, id)
end
@doc """ @doc """
Creates a address. Gets a single address by its url.
## Examples
iex> create_address(%{field: value})
{:ok, %Address{}}
iex> create_address(%{field: bad_value})
{:error, %Ecto.Changeset{}}
""" """
@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 def create_address(attrs \\ %{}) do
%Address{} %Address{}
|> Address.changeset(attrs) |> Address.changeset(attrs)
@ -89,17 +43,9 @@ defmodule Mobilizon.Addresses do
end end
@doc """ @doc """
Updates a address. Updates an address.
## Examples
iex> update_address(address, %{field: new_value})
{:ok, %Address{}}
iex> update_address(address, %{field: bad_value})
{:error, %Ecto.Changeset{}}
""" """
@spec update_address(Address.t(), map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
def update_address(%Address{} = address, attrs) do def update_address(%Address{} = address, attrs) do
address address
|> Address.changeset(attrs) |> Address.changeset(attrs)
@ -107,131 +53,96 @@ defmodule Mobilizon.Addresses do
end end
@doc """ @doc """
Deletes a Address. Deletes an address.
## Examples
iex> delete_address(address)
{:ok, %Address{}}
iex> delete_address(address)
{:error, %Ecto.Changeset{}}
""" """
def delete_address(%Address{} = address) do @spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
Repo.delete(address) def delete_address(%Address{} = address), do: Repo.delete(address)
end
@doc """ @doc """
Returns an `%Ecto.Changeset{}` for tracking address changes. Returns the list of addresses.
## Examples
iex> change_address(address)
%Ecto.Changeset{source: %Address{}}
""" """
def change_address(%Address{} = address) do @spec list_addresses :: [Address.t()]
Address.changeset(address, %{}) def list_addresses, do: Repo.all(Address)
end
@doc """ @doc """
Processes raw geo data informations and return a `Geo` geometry which can be one of `Geo.Point`. Searches addresses.
"""
# 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 We only look at the description for now, and eventually order by object distance.
case type do """
:point -> @spec search_addresses(String.t(), keyword) :: [Address.t()]
process_point(data["latitude"], data["longitude"]) def search_addresses(search, options \\ []) do
end query =
else search
{:error, :invalid_type} |> search_addresses_query(Keyword.get(options, :limit, 5))
|> order_by_coords(Keyword.get(options, :coords))
|> filter_by_contry(Keyword.get(options, :country))
case Keyword.get(options, :single, false) do
true ->
Repo.one(query)
false ->
Repo.all(query)
end end
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 """ @doc """
Search addresses in our database Reverse geocode from coordinates.
We only look at the description for now, and eventually order by object distance We only take addresses 50km around and sort them by distance.
""" """
@spec search_addresses(String.t(), list()) :: list(Address.t()) @spec reverse_geocode(number, number, 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
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 def reverse_geocode(lon, lat, options) do
limit = Keyword.get(options, :limit, 5) limit = Keyword.get(options, :limit, 5)
radius = Keyword.get(options, :radius, 50_000) radius = Keyword.get(options, :radius, 50_000)
country = Keyword.get(options, :country, nil) country = Keyword.get(options, :country)
srid = Keyword.get(options, :srid, 4326) srid = Keyword.get(options, :srid, 4326)
import Geo.PostGIS
with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do
query = point
from(a in Address, |> addresses_around_query(radius, limit)
order_by: [fragment("? <-> ?", a.geom, ^point)], |> filter_by_contry(country)
limit: ^limit, |> Repo.all()
where: st_dwithin_in_meters(^point, a.geom, ^radius)
)
query =
if country,
do: from(a in query, where: ilike(a.country, ^"%#{country}%")),
else: query
Repo.all(query)
end end
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 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 defmodule Mobilizon.Admin.ActionLog do
@moduledoc """ @moduledoc """
ActionLog entity schema Represents an action log entity.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Admin.ActionLogAction 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] @required_attrs [:action, :target_type, :target_id, :changes, :actor_id]
@attrs @required_attrs
@timestamps_opts [type: :utc_datetime]
schema "admin_action_logs" do schema "admin_action_logs" do
field(:action, ActionLogAction) field(:action, ActionLogAction)
field(:target_type, :string) field(:target_type, :string)
field(:target_id, :integer) field(:target_id, :integer)
field(:changes, :map) field(:changes, :map)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
timestamps() timestamps()
end end
@doc false @doc false
def changeset(action_log, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = action_log, attrs) do
action_log action_log
|> cast(attrs, @required_attrs) |> cast(attrs, @attrs)
|> validate_required(@required_attrs -- [:changes]) |> validate_required(@required_attrs)
end end
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 defmodule Mobilizon.Events.Comment do
@moduledoc """ @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 use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment alias Mobilizon.Events.{Comment, CommentVisibility, Event}
alias MobilizonWeb.Router.Helpers, as: Routes alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint 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 schema "comments" do
field(:text, :string) field(:text, :string)
field(:url, :string) field(:url, :string)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:visibility, Mobilizon.Events.CommentVisibilityEnum, default: :public) field(:visibility, CommentVisibility, default: :public)
field(:uuid, Ecto.UUID) field(:uuid, Ecto.UUID)
belongs_to(:actor, Actor, foreign_key: :actor_id) belongs_to(:actor, Actor, foreign_key: :actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id) belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
belongs_to(:event, Event, foreign_key: :event_id) belongs_to(:event, Event, foreign_key: :event_id)
@ -37,38 +46,27 @@ defmodule Mobilizon.Events.Comment do
timestamps(type: :utc_datetime) timestamps(type: :utc_datetime)
end 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 """ @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 origin_comment_id || id
end 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 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 defmodule Mobilizon.Events.Event do
@moduledoc """ @moduledoc """
Represents an event Represents an event.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Events.{Event, Participant, Tag, Session, Track}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Media.Picture
alias Mobilizon.Addresses.Address 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 schema "events" do
field(:url, :string) field(:url, :string)
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
@ -46,96 +90,59 @@ defmodule Mobilizon.Events.Event do
field(:description, :string) field(:description, :string)
field(:ends_on, :utc_datetime) field(:ends_on, :utc_datetime)
field(:title, :string) field(:title, :string)
field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed) field(:status, EventStatus, default: :confirmed)
field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public) field(:visibility, EventVisibility, default: :public)
field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free) field(:join_options, JoinOptions, default: :free)
field(:publish_at, :utc_datetime) field(:publish_at, :utc_datetime)
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate()) field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
field(:online_address, :string) field(:online_address, :string)
field(:phone_address, :string) field(:phone_address, :string)
field(:category, :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(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_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(:physical_address, Address)
belongs_to(:picture, Picture) 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) timestamps(type: :utc_datetime)
end end
@doc false @doc false
def changeset(%Event{} = event, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = event, attrs) do
event event
|> Ecto.Changeset.cast(attrs, [ |> cast(attrs, @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_embed(:options) |> cast_embed(:options)
|> validate_required([ |> validate_required(@required_attrs)
:title,
:begins_on,
:organizer_actor_id,
:url,
:uuid
])
end end
@doc false @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 event
|> Ecto.Changeset.cast(attrs, [ |> Ecto.Changeset.cast(attrs, @update_attrs)
:title,
:slug,
:description,
:begins_on,
:ends_on,
:category,
:status,
:visibility,
:publish_at,
:online_address,
:phone_address,
:picture_id,
:physical_address_id
])
|> cast_embed(:options) |> cast_embed(:options)
|> put_tags(attrs) |> put_tags(attrs)
|> validate_required([ |> validate_required(@update_required_attrs)
:title,
:begins_on,
:organizer_actor_id,
:url,
:uuid
])
end end
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags) @doc """
defp put_tags(changeset, _), do: changeset Checks whether an event can be managed.
"""
def can_event_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id) @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 when organizer_actor_id == actor_id do
{:event_can_be_managed, true} {:event_can_be_managed, true}
end end
def can_event_be_managed_by(_event, _actor) do def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false}
{:event_can_be_managed, false}
end @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 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,70 +1,58 @@
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 defmodule Mobilizon.Events.EventOptions do
@moduledoc """ @moduledoc """
Represents an event options Represents an event options.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.{ alias Mobilizon.Events.{
EventOptions,
EventOffer, EventOffer,
EventParticipationCondition, EventParticipationCondition,
CommentModeration CommentModeration
} }
@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()]
}
@attrs [
:maximum_attendee_capacity,
:remaining_attendee_capacity,
:show_remaining_attendee_capacity,
:attendees,
:program,
:comment_moderation,
:show_participation_price
]
@primary_key false @primary_key false
@derive Jason.Encoder @derive Jason.Encoder
embedded_schema do embedded_schema do
field(:maximum_attendee_capacity, :integer) field(:maximum_attendee_capacity, :integer)
field(:remaining_attendee_capacity, :integer) field(:remaining_attendee_capacity, :integer)
field(:show_remaining_attendee_capacity, :boolean) field(:show_remaining_attendee_capacity, :boolean)
embeds_many(:offers, EventOffer)
embeds_many(:participation_condition, EventParticipationCondition)
field(:attendees, {:array, :string}) field(:attendees, {:array, :string})
field(:program, :string) field(:program, :string)
field(:comment_moderation, CommentModeration) field(:comment_moderation, CommentModeration)
field(:show_participation_price, :boolean) field(:show_participation_price, :boolean)
embeds_many(:offers, EventOffer)
embeds_many(:participation_condition, EventParticipationCondition)
end end
def changeset(%EventOptions{} = event_options, attrs) do @doc false
event_options @spec changeset(t, map) :: Ecto.Changeset.t()
|> Ecto.Changeset.cast(attrs, [ def changeset(%__MODULE__{} = event_options, attrs) do
:maximum_attendee_capacity, cast(event_options, attrs, @attrs)
:remaining_attendee_capacity,
:show_remaining_attendee_capacity,
:attendees,
:program,
:comment_moderation,
:show_participation_price
])
end end
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 defmodule Mobilizon.Events.FeedToken do
@moduledoc """ @moduledoc """
Represents a Token for a Feed of events Represents a token for a feed of events.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Events.FeedToken
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Users.User 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 @primary_key false
schema "feed_tokens" do schema "feed_tokens" do
field(:token, Ecto.UUID, primary_key: true) field(:token, Ecto.UUID, primary_key: true)
belongs_to(:actor, Actor) belongs_to(:actor, Actor)
belongs_to(:user, User) belongs_to(:user, User)
@ -18,9 +31,10 @@ defmodule Mobilizon.Events.FeedToken do
end end
@doc false @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 feed_token
|> Ecto.Changeset.cast(attrs, [:token, :actor_id, :user_id]) |> cast(attrs, @attrs)
|> validate_required([:token, :user_id]) |> validate_required(@required_attrs)
end end
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 defmodule Mobilizon.Events.Participant do
@moduledoc """ @moduledoc """
Represents a participant, an actor participating to an event Represents a participant, an actor participating to an event.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Events.{Participant, Event}
alias Mobilizon.Actors.Actor 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} @primary_key {:id, :binary_id, autogenerate: true}
schema "participants" do schema "participants" do
field(:role, Mobilizon.Events.ParticipantRoleEnum, default: :participant) field(:role, ParticipantRole, default: :participant)
field(:url, :string) field(:url, :string)
belongs_to(:event, Event, primary_key: true) belongs_to(:event, Event, primary_key: true)
belongs_to(:actor, Actor, primary_key: true) belongs_to(:actor, Actor, primary_key: true)
timestamps() timestamps()
end 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 """ @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 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() @spec is_not_only_organizer(integer | String.t(), integer | String.t()) :: boolean
def check_that_participant_is_not_only_organizer(event_id, actor_id) do def is_not_only_organizer(event_id, actor_id) do
case Mobilizon.Events.list_organizers_participants_for_event(event_id) do case Events.list_organizers_participants_for_event(event_id) do
[%Participant{actor: %Actor{id: participant_actor_id}}] -> [%__MODULE__{actor: %Actor{id: participant_actor_id}}] ->
participant_actor_id == actor_id participant_actor_id == actor_id
_ -> _ ->
false false
end end
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 end

View File

@ -1,10 +1,41 @@
defmodule Mobilizon.Events.Session do defmodule Mobilizon.Events.Session do
@moduledoc """ @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 use Ecto.Schema
import Ecto.Changeset 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 schema "sessions" do
field(:audios_urls, :string) field(:audios_urls, :string)
@ -17,6 +48,7 @@ defmodule Mobilizon.Events.Session do
field(:videos_urls, :string) field(:videos_urls, :string)
field(:begins_on, :utc_datetime) field(:begins_on, :utc_datetime)
field(:ends_on, :utc_datetime) field(:ends_on, :utc_datetime)
belongs_to(:event, Event) belongs_to(:event, Event)
belongs_to(:track, Track) belongs_to(:track, Track)
@ -24,29 +56,10 @@ defmodule Mobilizon.Events.Session do
end end
@doc false @doc false
def changeset(%Session{} = session, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = session, attrs) do
session session
|> cast(attrs, [ |> cast(attrs, @attrs)
:title, |> validate_required(@required_attrs)
: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
])
end end
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 defmodule Mobilizon.Events.Tag do
@moduledoc """ @moduledoc """
Represents a tag for events Represents a tag for events.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Events.Tag
alias Mobilizon.Events.Tag.TitleSlug
alias Mobilizon.Events.TagRelation 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 schema "tags" do
field(:title, :string) field(:title, :string)
field(:slug, TitleSlug.Type) field(:slug, TitleSlug.Type)
many_to_many(:related_tags, Tag, join_through: TagRelation)
many_to_many(:related_tags, __MODULE__, join_through: TagRelation)
timestamps() timestamps()
end end
@doc false @doc false
def changeset(%Tag{} = tag, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = tag, attrs) do
tag tag
|> cast(attrs, [:title]) |> cast(attrs, @attrs)
|> TitleSlug.maybe_generate_slug() |> TitleSlug.maybe_generate_slug()
|> validate_required([:title, :slug]) |> validate_required(@required_attrs)
|> TitleSlug.unique_constraint() |> TitleSlug.unique_constraint()
end end
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 defmodule Mobilizon.Events.Track do
@moduledoc """ @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 use Ecto.Schema
import Ecto.Changeset 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 schema "tracks" do
field(:color, :string) field(:color, :string)
field(:description, :string) field(:description, :string)
field(:name, :string) field(:name, :string)
belongs_to(:event, Event) belongs_to(:event, Event)
has_many(:sessions, Session) has_many(:sessions, Session)
@ -17,9 +33,10 @@ defmodule Mobilizon.Events.Track do
end end
@doc false @doc false
def changeset(%Track{} = track, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = track, attrs) do
track track
|> cast(attrs, [:name, :description, :color, :event_id]) |> cast(attrs, @attrs)
|> validate_required([:name, :description, :color]) |> validate_required(@required_attrs)
end end
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 defmodule Mobilizon.Media.File do
@moduledoc """ @moduledoc """
Represents a file entity Represents a file entity.
""" """
use Ecto.Schema 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 embedded_schema do
field(:name, :string) field(:name, :string)
@ -15,9 +28,10 @@ defmodule Mobilizon.Media.File do
end end
@doc false @doc false
def changeset(picture, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
picture def changeset(%__MODULE__{} = file, attrs) do
|> cast(attrs, [:name, :url, :content_type, :size]) file
|> validate_required([:name, :url]) |> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end end
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 defmodule Mobilizon.Media.Picture do
@moduledoc """ @moduledoc """
Represents a picture entity Represents a picture entity.
""" """
use Ecto.Schema 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.Actors.Actor
alias Mobilizon.Media.File
@type t :: %__MODULE__{
file: File.t(),
actor: Actor.t()
}
schema "pictures" do schema "pictures" do
embeds_one(:file, File, on_replace: :update) embeds_one(:file, File, on_replace: :update)
@ -15,7 +23,8 @@ defmodule Mobilizon.Media.Picture do
end end
@doc false @doc false
def changeset(picture, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = picture, attrs) do
picture picture
|> cast(attrs, [:actor_id]) |> cast(attrs, [:actor_id])
|> cast_embed(:file) |> 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 defmodule Mobilizon.Reports.Note do
@moduledoc """ @moduledoc """
Report Note entity Represents a note entity.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset
import Ecto.Changeset, only: [cast: 3, validate_required: 2]
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Reports.Report alias Mobilizon.Reports.Report
@required_attrs [:content, :moderator_id, :report_id]
@attrs @required_attrs
@timestamps_opts [type: :utc_datetime] @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]} @derive {Jason.Encoder, only: [:content]}
schema "report_notes" do schema "report_notes" do
field(:content, :string) field(:content, :string)
belongs_to(:moderator, Actor)
belongs_to(:report, Report) belongs_to(:report, Report)
belongs_to(:moderator, Actor)
timestamps() timestamps()
end end
@doc false @doc false
def changeset(note, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = note, attrs) do
note note
|> cast(attrs, @attrs) |> cast(attrs, @attrs)
|> validate_required(@attrs) |> validate_required(@required_attrs)
end end
end end

View File

@ -1,45 +1,50 @@
import EctoEnum
defenum(Mobilizon.Reports.ReportStateEnum, :report_state, [
:open,
:closed,
:resolved
])
defmodule Mobilizon.Reports.Report do defmodule Mobilizon.Reports.Report do
@moduledoc """ @moduledoc """
Report entity Represents a report entity.
""" """
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Mobilizon.Events.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor 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] @timestamps_opts [type: :utc_datetime]
@derive {Jason.Encoder, only: [:status, :uri]} @derive {Jason.Encoder, only: [:status, :uri]}
schema "reports" do schema "reports" do
field(:content, :string) field(:content, :string)
field(:status, Mobilizon.Reports.ReportStateEnum, default: :open) field(:status, ReportStatus, default: :open)
field(:uri, :string) field(:uri, :string)
# The reported actor # The reported actor
belongs_to(:reported, Actor) belongs_to(:reported, Actor)
# The actor who reported # The actor who reported
belongs_to(:reporter, Actor) belongs_to(:reporter, Actor)
# The actor who last acted on this report # The actor who last acted on this report
belongs_to(:manager, Actor) belongs_to(:manager, Actor)
# The eventual Event inside the report # The eventual Event inside the report
belongs_to(:event, Event) belongs_to(:event, Event)
# The eventual Comments inside the report # The eventual Comments inside the report
many_to_many(:comments, Comment, join_through: "reports_comments", on_replace: :delete) many_to_many(:comments, Comment, join_through: "reports_comments", on_replace: :delete)
# The notes associated to the report # The notes associated to the report
has_many(:notes, Note, foreign_key: :report_id) has_many(:notes, Note, foreign_key: :report_id)
@ -47,13 +52,16 @@ defmodule Mobilizon.Reports.Report do
end end
@doc false @doc false
def changeset(report, attrs) do @spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = report, attrs) do
report report
|> cast(attrs, [:content, :status, :uri, :reported_id, :reporter_id, :manager_id, :event_id]) |> cast(attrs, @attrs)
|> validate_required([:uri, :reported_id, :reporter_id]) |> validate_required(@required_attrs)
end 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 report
|> changeset(attrs) |> changeset(attrs)
|> put_assoc(:comments, attrs["comments"]) |> 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 """ @moduledoc """
Mobilizon Repo Mobilizon Repo.
""" """
use Ecto.Repo, use Ecto.Repo,
otp_app: :mobilizon, otp_app: :mobilizon,
adapter: Ecto.Adapters.Postgres adapter: Ecto.Adapters.Postgres
@doc """ @doc """
Dynamically loads the repository url from the Dynamically loads the repository url from the DATABASE_URL environment variable.
DATABASE_URL environment variable.
""" """
def init(_, opts) do def init(_, opts) do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}

View File

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

View File

@ -3,116 +3,86 @@ defmodule Mobilizon.Users do
The Users context. The Users context.
""" """
import Ecto.Query, warn: false import Ecto.Query
import EctoEnum
alias Mobilizon.Repo import Mobilizon.Storage.Ecto
import Mobilizon.Ecto
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.User alias Mobilizon.Users.User
@doc false @type tokens :: %{
def data() do required(:access_token) => String.t(),
Dataloader.Ecto.new(Repo, query: &query/2) required(:refresh_token) => String.t()
end }
@doc false defenum(UserRole, :user_role, [:administrator, :moderator, :user])
def query(queryable, _params) do
queryable
end
@doc """ @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 def register(%{email: _email, password: _password} = args) do
with {:ok, %User{} = user} <- with {:ok, %User{} = user} <-
%User{} %User{}
|> User.registration_changeset(args) |> User.registration_changeset(args)
|> Mobilizon.Repo.insert() do |> Repo.insert() do
Mobilizon.Events.create_feed_token(%{"user_id" => user.id}) Events.create_feed_token(%{"user_id" => user.id})
{:ok, user} {:ok, user}
end end
end end
@doc """ @doc """
Gets an user by it's email Gets a single user.
Raises `Ecto.NoResultsError` if the user does not exist.
## 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}
""" """
@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 def get_user_by_email(email, activated \\ nil) do
query = query = user_by_email_query(email, activated)
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
case Repo.one(query) do case Repo.one(query) do
nil -> {:error, :user_not_found} nil ->
user -> {:ok, user} {:error, :user_not_found}
user ->
{:ok, user}
end end
end end
@doc """ @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 def get_user_by_activation_token(token) do
Repo.one( token
from( |> user_by_activation_token_query()
u in User, |> Repo.one()
where: u.confirmation_token == ^token,
preload: [:default_actor]
)
)
end end
@doc """ @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() @spec get_user_by_reset_password_token(String.t()) :: Actor.t()
def get_user_by_reset_password_token(token) do def get_user_by_reset_password_token(token) do
Repo.one( token
from( |> user_by_reset_password_token_query()
u in User, |> Repo.one()
where: u.reset_password_token == ^token,
preload: [:default_actor]
)
)
end end
@doc """ @doc """
Updates a user. Updates an user.
## Examples
iex> update_user(User{}, %{password: "coucou"})
{:ok, %Mobilizon.Users.User{}}
iex> update_user(User{}, %{password: nil})
{:error, %Ecto.Changeset{}}
""" """
@spec update_user(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update_user(%User{} = user, attrs) do def update_user(%User{} = user, attrs) do
with {:ok, %User{} = user} <- with {:ok, %User{} = user} <-
user user
@ -123,65 +93,26 @@ defmodule Mobilizon.Users do
end end
@doc """ @doc """
Deletes a User. Deletes an user.
## Examples
iex> delete_user(%User{email: "test@test.tld"})
{:ok, %Mobilizon.Users.User{}}
iex> delete_user(%User{})
{:error, %Ecto.Changeset{}}
""" """
def delete_user(%User{} = user) do @spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
Repo.delete(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
@doc """ @doc """
Gets a single user. Get an user with its actors
Raises `Ecto.NoResultsError` if the user does not exist.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%Mobilizon.Users.User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
""" """
def get_user!(id), do: Repo.get!(User, id) @spec get_user_with_actors!(integer | String.t()) :: User.t()
@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()
def get_user_with_actors!(id) do def get_user_with_actors!(id) do
user = Repo.get!(User, id) id
Repo.preload(user, [:actors, :default_actor]) |> get_user!()
|> Repo.preload([:actors, :default_actor])
end end
@doc """ @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 def get_user_with_actors(id) do
case Repo.get(User, id) do case Repo.get(User, id) do
nil -> nil ->
@ -198,23 +129,24 @@ defmodule Mobilizon.Users do
end end
@doc """ @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() @spec get_actor_for_user(User.t()) :: Actor.t() | nil
def get_actor_for_user(%Mobilizon.Users.User{} = user) do def get_actor_for_user(%User{} = user) do
case Repo.one( actor =
from( user
a in Actor, |> actor_for_user_query()
join: u in User, |> Repo.one()
on: u.default_actor_id == a.id,
where: u.id == ^user.id case actor do
)
) do
nil -> nil ->
case user case get_actors_for_user(user) do
|> get_actors_for_user() do [] ->
[] -> nil nil
actors -> hd(actors)
actors ->
hd(actors)
end end
actor -> actor ->
@ -222,94 +154,48 @@ defmodule Mobilizon.Users do
end end
end end
def get_actors_for_user(%User{id: user_id}) do @doc """
Repo.all(from(a in Actor, where: a.user_id == ^user_id)) 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 end
@doc """ @doc """
Authenticate user Updates user's default actor.
Raises `Ecto.NoResultsError` if the user does not exist.
""" """
def authenticate(%{user: user, password: password}) do @spec update_user_default_actor(integer | String.t(), integer | String.t()) :: User.t()
# 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
def update_user_default_actor(user_id, actor_id) do def update_user_default_actor(user_id, actor_id) do
with _ <- with _ <-
from( user_id
u in User, |> update_user_default_actor_query(actor_id)
where: u.id == ^user_id,
update: [
set: [
default_actor_id: ^actor_id
]
]
)
|> Repo.update_all([]) do |> Repo.update_all([]) do
Repo.get!(User, user_id) user_id
|> get_user!()
|> Repo.preload([:default_actor]) |> Repo.preload([:default_actor])
end end
end end
@doc """ @doc """
Returns the list of users. 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 def list_users(page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil) do
Repo.all( User
User |> Page.paginate(page, limit)
|> paginate(page, limit) |> sort(sort, direction)
|> sort(sort, direction) |> Repo.all()
)
end end
@doc """ @doc """
Returns the list of administrators. 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 User
|> where([u], u.role == ^:administrator) |> where([u], u.role == ^:administrator)
|> Repo.all() |> Repo.all()
@ -317,25 +203,127 @@ defmodule Mobilizon.Users do
@doc """ @doc """
Returns the list of moderators. 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 User
|> where([u], u.role in ^[:administrator, :moderator]) |> where([u], u.role in ^[:administrator, :moderator])
|> Repo.all() |> Repo.all()
end end
def count_users() do @doc """
Repo.one( Counts users.
from( """
u in User, @spec count_users :: integer
select: count(u.id) 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,
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
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,9 @@ defmodule MobilizonWeb.API.Utils do
@moduledoc """ @moduledoc """
Utils for API Utils for API
""" """
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Config
alias Mobilizon.Service.Formatter alias Mobilizon.Service.Formatter
@doc """ @doc """
@ -125,7 +127,7 @@ defmodule MobilizonWeb.API.Utils do
def make_report_content_text(nil), do: {:ok, nil} def make_report_content_text(nil), do: {:ok, nil}
def make_report_content_text(comment) do 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 if String.length(comment) <= max_size do
{:ok, Formatter.html_escape(comment, "text/plain")} {: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 defmodule MobilizonWeb.ActivityPubController do
use MobilizonWeb, :controller 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.ActivityPub
alias Mobilizon.Service.Federator alias Mobilizon.Service.Federator
alias MobilizonWeb.ActivityPub.ActorView
alias MobilizonWeb.Cache
require Logger require Logger
action_fallback(:errors) action_fallback(:errors)
@ -17,7 +20,7 @@ defmodule MobilizonWeb.ActivityPubController do
plug(:relay_active? when action in [:relay]) plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do def relay_active?(conn, _) do
if Mobilizon.CommonConfig.get([:instance, :allow_relay]) do if Config.get([:instance, :allow_relay]) do
conn conn
else else
conn conn
@ -29,7 +32,7 @@ defmodule MobilizonWeb.ActivityPubController do
def following(conn, %{"name" => name, "page" => page}) do def following(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page), 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 conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("following.json", %{actor: actor, page: page})) |> json(ActorView.render("following.json", %{actor: actor, page: page}))
@ -37,7 +40,7 @@ defmodule MobilizonWeb.ActivityPubController do
end end
def following(conn, %{"name" => name}) do 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 conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("following.json", %{actor: actor})) |> json(ActorView.render("following.json", %{actor: actor}))
@ -46,7 +49,7 @@ defmodule MobilizonWeb.ActivityPubController do
def followers(conn, %{"name" => name, "page" => page}) do def followers(conn, %{"name" => name, "page" => page}) do
with {page, ""} <- Integer.parse(page), 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 conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("followers.json", %{actor: actor, page: page})) |> json(ActorView.render("followers.json", %{actor: actor, page: page}))
@ -54,7 +57,7 @@ defmodule MobilizonWeb.ActivityPubController do
end end
def followers(conn, %{"name" => name}) do 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 conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("followers.json", %{actor: actor})) |> json(ActorView.render("followers.json", %{actor: actor}))
@ -111,13 +114,7 @@ defmodule MobilizonWeb.ActivityPubController do
end end
def relay(conn, _params) do def relay(conn, _params) do
with {status, actor} <- with {:commit, %Actor{} = actor} <- Cache.get_relay() do
Cachex.fetch(
:activity_pub,
"relay_actor",
&Mobilizon.Service.ActivityPub.Relay.get_actor/0
),
true <- status in [:ok, :commit] do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(ActorView.render("actor.json", %{actor: actor})) |> 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 def actor(conn, %{"name" => name, "format" => "atom"}) do
case Cachex.fetch(:feed, "actor_" <> name) do case Cachex.fetch(:feed, "actor_" <> name) do
{status, data} when status in [:ok, :commit] -> {:commit, data} ->
conn conn
|> put_resp_content_type("application/atom+xml") |> put_resp_content_type("application/atom+xml")
|> send_resp(200, data) |> send_resp(200, data)
_err -> _ ->
{:error, :not_found} {:error, :not_found}
end end
end end
def actor(conn, %{"name" => name, "format" => "ics"}) do def actor(conn, %{"name" => name, "format" => "ics"}) do
case Cachex.fetch(:ics, "actor_" <> name) do case Cachex.fetch(:ics, "actor_" <> name) do
{status, data} when status in [:ok, :commit] -> {:commit, data} ->
conn conn
|> put_resp_content_type("text/calendar") |> put_resp_content_type("text/calendar")
|> send_resp(200, data) |> send_resp(200, data)
_err -> _ ->
{:error, :not_found} {:error, :not_found}
end end
end end
def event(conn, %{"uuid" => uuid, "format" => "ics"}) do def event(conn, %{"uuid" => uuid, "format" => "ics"}) do
case Cachex.fetch(:ics, "event_" <> uuid) do case Cachex.fetch(:ics, "event_" <> uuid) do
{status, data} when status in [:ok, :commit] -> {:commit, data} ->
conn conn
|> put_resp_content_type("text/calendar") |> put_resp_content_type("text/calendar")
|> send_resp(200, data) |> send_resp(200, data)
_err -> _ ->
{:error, :not_found} {:error, :not_found}
end end
end end
def going(conn, %{"token" => token, "format" => "ics"}) do def going(conn, %{"token" => token, "format" => "ics"}) do
case Cachex.fetch(:ics, "token_" <> token) do case Cachex.fetch(:ics, "token_" <> token) do
{status, data} when status in [:ok, :commit] -> {:commit, data} ->
conn conn
|> put_resp_content_type("text/calendar") |> put_resp_content_type("text/calendar")
|> send_resp(200, data) |> send_resp(200, data)
_err -> _ ->
{:error, :not_found} {:error, :not_found}
end end
end end
def going(conn, %{"token" => token, "format" => "atom"}) do def going(conn, %{"token" => token, "format" => "atom"}) do
case Cachex.fetch(:feed, "token_" <> token) do case Cachex.fetch(:feed, "token_" <> token) do
{status, data} when status in [:ok, :commit] -> {:commit, data} ->
conn conn
|> put_resp_content_type("application/atom+xml") |> put_resp_content_type("application/atom+xml")
|> send_resp(200, data) |> send_resp(200, data)
_err -> {:ignore, _} ->
{:error, :not_found} {:error, :not_found}
end end
end end

View File

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

View File

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

View File

@ -3,8 +3,8 @@ defmodule MobilizonWeb.PageController do
Controller to load our webapp Controller to load our webapp
""" """
use MobilizonWeb, :controller use MobilizonWeb, :controller
alias Mobilizon.Actors
alias Mobilizon.Events alias MobilizonWeb.Cache
plug(:put_layout, false) plug(:put_layout, false)
action_fallback(MobilizonWeb.FallbackController) action_fallback(MobilizonWeb.FallbackController)
@ -12,17 +12,17 @@ defmodule MobilizonWeb.PageController do
def index(conn, _params), do: render(conn, :index) def index(conn, _params), do: render(conn, :index)
def actor(conn, %{"name" => name}) do 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) render_or_error(conn, &ok_status?/2, status, :actor, actor)
end end
def event(conn, %{"uuid" => uuid}) do 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) render_or_error(conn, &ok_status_and_is_visible?/2, status, :event, event)
end end
def comment(conn, %{"uuid" => uuid}) do 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) render_or_error(conn, &ok_status_and_is_visible?/2, status, :comment, comment)
end 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 """ @moduledoc """
Mailer Mobilizon Mailer.
""" """
use Bamboo.Mailer, otp_app: :mobilizon use Bamboo.Mailer, otp_app: :mobilizon
end 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 """ @moduledoc """
Handles proxifying media files Handles proxifying media files
""" """
alias Mobilizon.Config
@base64_opts [padding: false] @base64_opts [padding: false]
def url(nil), do: nil def url(nil), do: nil
@ -66,7 +69,7 @@ defmodule MobilizonWeb.MediaProxy do
def build_url(sig_base64, url_base64, filename \\ nil) 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", "proxy",
sig_base64, sig_base64,
url_base64, url_base64,

View File

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/mime.ex # Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/mime.ex
defmodule Mobilizon.MIME do defmodule MobilizonWeb.MIME do
@moduledoc """ @moduledoc """
Returns the mime-type of a binary and optionally a normalized file-name. 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 Serves uploaded media files
""" """
@behaviour Plug
import Plug.Conn import Plug.Conn
alias Mobilizon.Config
require Logger require Logger
@behaviour Plug
# no slashes # no slashes
@path "media" @path "media"
@ -38,7 +42,7 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do
conn conn
end end
config = Mobilizon.CommonConfig.get([MobilizonWeb.Upload]) config = Config.get([MobilizonWeb.Upload])
with uploader <- Keyword.fetch!(config, :uploader), with uploader <- Keyword.fetch!(config, :uploader),
proxy_remote = Keyword.get(config, :proxy_remote, false), proxy_remote = Keyword.get(config, :proxy_remote, false),
@ -75,7 +79,7 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do
conn conn
|> MobilizonWeb.ReverseProxy.call( |> MobilizonWeb.ReverseProxy.call(
url, url,
Mobilizon.CommonConfig.get([Mobilizon.Upload, :proxy_opts], []) Config.get([Mobilizon.Upload, :proxy_opts], [])
) )
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,11 +9,14 @@ defmodule MobilizonWeb.Upload.Filter.AnonymizeFilename do
Should be used after `MobilizonWeb.Upload.Filter.Dedupe`. Should be used after `MobilizonWeb.Upload.Filter.Dedupe`.
""" """
@behaviour MobilizonWeb.Upload.Filter @behaviour MobilizonWeb.Upload.Filter
alias Mobilizon.Config
def filter(upload) do def filter(upload) do
extension = List.last(String.split(upload.name, ".")) 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}} {:ok, %MobilizonWeb.Upload{upload | name: name}}
end end

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
defmodule MobilizonWeb.ActivityPub.ObjectView do defmodule MobilizonWeb.ActivityPub.ObjectView do
use MobilizonWeb, :view 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 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 use MobilizonWeb, :view
end 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 Every ActivityPub method
""" """
alias Mobilizon.Config
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Event, Comment, Participant} alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Service.ActivityPub.Transmogrifier alias Mobilizon.Service.ActivityPub.Transmogrifier
alias Mobilizon.Service.WebFinger alias Mobilizon.Service.WebFinger
alias Mobilizon.Activity
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
@ -22,11 +22,10 @@ defmodule Mobilizon.Service.ActivityPub do
alias Mobilizon.Service.Federator alias Mobilizon.Service.Federator
alias Mobilizon.Service.HTTPSignatures.Signature alias Mobilizon.Service.HTTPSignatures.Signature
alias Mobilizon.Service.ActivityPub.Convertible alias Mobilizon.Service.ActivityPub.{Activity, Convertible}
require Logger require Logger
import Mobilizon.Service.ActivityPub.Utils import Mobilizon.Service.ActivityPub.{Utils, Visibility}
import Mobilizon.Service.ActivityPub.Visibility
@doc """ @doc """
Get recipients for an activity or object 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 {:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
case data["type"] do case data["type"] do
"Event" -> "Event" ->
{:ok, Events.get_event_full_by_url!(object_url)} {:ok, Events.get_public_event_by_url_with_preload!(object_url)}
"Note" -> "Note" ->
{:ok, Events.get_comment_full_from_url!(object_url)} {:ok, Events.get_comment_from_url_with_preload!(object_url)}
"Actor" -> "Actor" ->
{:ok, Actors.get_actor_by_url!(object_url, true)} {:ok, Actors.get_actor_by_url!(object_url, true)}
@ -97,10 +96,10 @@ defmodule Mobilizon.Service.ActivityPub do
end end
else else
{:existing_event, %Event{url: event_url}} -> {: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}} -> {: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}}} -> {:existing_actor, {:ok, %Actor{url: actor_url}}} ->
{:ok, Actors.get_actor_by_url!(actor_url, true)} {:ok, Actors.get_actor_by_url!(actor_url, true)}
@ -112,6 +111,28 @@ defmodule Mobilizon.Service.ActivityPub do
end end
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 """ @doc """
Create an activity of type "Create" 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 def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{url: follow_url}} <- with {:ok, %Follower{url: follow_url}} <-
Actor.follow(followed, follower, activity_id, false), Actors.follow(followed, follower, activity_id, false),
activity_follow_id <- activity_follow_id <-
activity_id || follow_url, activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id), 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() @spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower), with {:ok, %Follower{id: follow_id}} <- Actors.unfollow(followed, follower),
# We recreate the follow activity # We recreate the follow activity
data <- data <-
make_follow_data( make_follow_data(
@ -437,8 +458,7 @@ defmodule Mobilizon.Service.ActivityPub do
local local
) do ) do
with {:only_organizer, false} <- with {:only_organizer, false} <-
{:only_organizer, {:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
Participant.check_that_participant_is_not_only_organizer(event_id, actor_id)},
{:ok, %Participant{} = participant} <- {:ok, %Participant{} = participant} <-
Mobilizon.Events.get_participant(event_id, actor_id), Mobilizon.Events.get_participant(event_id, actor_id),
{:ok, %Participant{} = participant} <- Mobilizon.Events.delete_participant(participant), {: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 def make_actor_from_url(url, preload \\ false) do
case fetch_and_prepare_actor_from_url(url) do case fetch_and_prepare_actor_from_url(url) do
{:ok, data} -> {:ok, data} ->
Actors.insert_or_update_actor(data, preload) Actors.upsert_actor(data, preload)
# Request returned 410 # Request returned 410
{:error, :actor_deleted} -> {:error, :actor_deleted} ->
@ -520,15 +540,14 @@ defmodule Mobilizon.Service.ActivityPub do
public = is_public?(activity) public = is_public?(activity)
if public && is_delete_activity?(activity) == false && if public && !is_delete_activity?(activity) && Config.get([:instance, :allow_relay]) do
Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
Mobilizon.Service.ActivityPub.Relay.publish(activity) Mobilizon.Service.ActivityPub.Relay.publish(activity)
end end
followers = followers =
if actor.followers_url in activity.recipients do if actor.followers_url in activity.recipients do
Actor.get_full_external_followers(actor) Actors.list_external_followers_for_actor(actor)
else else
[] []
end end
@ -664,8 +683,8 @@ defmodule Mobilizon.Service.ActivityPub do
""" """
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map() @spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map()
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do 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, events, total_events} = Events.list_public_events_for_actor(actor, page, limit)
{:ok, comments, total_comments} = Events.get_public_comments_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) 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"]), "type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferredUsername"], "preferred_username" => object["preferredUsername"],
"summary" => object["summary"], "summary" => object["summary"],
"url" => object["url"], "url" => object["id"],
"name" => object["name"], "name" => object["name"],
"avatar" => avatar, "avatar" => avatar,
"banner" => banner, "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 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.Actors.Actor
alias Mobilizon.Events.Comment, as: CommentModel alias Mobilizon.Events.Comment, as: CommentModel
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
@ -20,7 +19,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
@impl Converter @impl Converter
@spec as_to_model_data(map()) :: map() @spec as_to_model_data(map()) :: map()
def as_to_model_data(object) do 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("Inserting full comment")
Logger.debug(inspect(object)) Logger.debug(inspect(object))

View File

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

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