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:
commit
9a17998074
@ -7,7 +7,7 @@ use Mix.Config
|
||||
|
||||
# General application configuration
|
||||
config :mobilizon,
|
||||
ecto_repos: [Mobilizon.Repo]
|
||||
ecto_repos: [Mobilizon.Storage.Repo]
|
||||
|
||||
config :mobilizon, :instance,
|
||||
name: System.get_env("MOBILIZON_INSTANCE_NAME") || "Localhost",
|
||||
@ -78,7 +78,7 @@ config :mobilizon, MobilizonWeb.Guardian,
|
||||
secret_key: "ty0WM7YBE3ojvxoUQxo8AERrNpfbXnIJ82ovkPdqbUFw31T5LcK8wGjaOiReVQjo"
|
||||
|
||||
config :guardian, Guardian.DB,
|
||||
repo: Mobilizon.Repo,
|
||||
repo: Mobilizon.Storage.Repo,
|
||||
# default
|
||||
schema_name: "guardian_tokens",
|
||||
# store all token types if not set
|
||||
|
@ -61,11 +61,11 @@ config :phoenix, :stacktrace_depth, 20
|
||||
# Initialize plugs at runtime for faster development compilation
|
||||
config :phoenix, :plug_init_mode, :runtime
|
||||
|
||||
config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.LocalAdapter
|
||||
config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.LocalAdapter
|
||||
|
||||
# Configure your database
|
||||
config :mobilizon, Mobilizon.Repo,
|
||||
types: Mobilizon.PostgresTypes,
|
||||
config :mobilizon, Mobilizon.Storage.Repo,
|
||||
types: Mobilizon.Storage.PostgresTypes,
|
||||
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
|
||||
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
|
||||
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_dev",
|
||||
|
@ -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}
|
||||
]
|
@ -12,8 +12,8 @@ config :mobilizon, MobilizonWeb.Endpoint,
|
||||
cache_static_manifest: "priv/static/manifest.json"
|
||||
|
||||
# Configure your database
|
||||
config :mobilizon, Mobilizon.Repo,
|
||||
types: Mobilizon.PostgresTypes,
|
||||
config :mobilizon, Mobilizon.Storage.Repo,
|
||||
types: Mobilizon.Storage.PostgresTypes,
|
||||
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
|
||||
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
|
||||
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_prod",
|
||||
@ -21,7 +21,7 @@ config :mobilizon, Mobilizon.Repo,
|
||||
port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432",
|
||||
pool_size: 15
|
||||
|
||||
config :mobilizon, Mobilizon.Mailer,
|
||||
config :mobilizon, MobilizonWeb.Email.Mailer,
|
||||
adapter: Bamboo.SMTPAdapter,
|
||||
server: "localhost",
|
||||
hostname: "localhost",
|
||||
|
@ -22,16 +22,15 @@ config :logger,
|
||||
level: :info
|
||||
|
||||
# Configure your database
|
||||
config :mobilizon, Mobilizon.Repo,
|
||||
types: Mobilizon.PostgresTypes,
|
||||
config :mobilizon, Mobilizon.Storage.Repo,
|
||||
types: Mobilizon.Storage.PostgresTypes,
|
||||
username: System.get_env("MOBILIZON_DATABASE_USERNAME") || "mobilizon",
|
||||
password: System.get_env("MOBILIZON_DATABASE_PASSWORD") || "mobilizon",
|
||||
database: System.get_env("MOBILIZON_DATABASE_DBNAME") || "mobilizon_test",
|
||||
hostname: System.get_env("MOBILIZON_DATABASE_HOST") || "localhost",
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
types: Mobilizon.PostgresTypes
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.TestAdapter
|
||||
config :mobilizon, MobilizonWeb.Email.Mailer, adapter: Bamboo.TestAdapter
|
||||
|
||||
config :mobilizon, MobilizonWeb.Upload, filters: [], link_name: false
|
||||
|
||||
|
@ -15,7 +15,7 @@ defmodule Mix.Tasks.Mobilizon.CreateBot do
|
||||
Mix.Task.run("app.start")
|
||||
|
||||
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
|
||||
actor <- Actors.register_bot_account(%{name: name, summary: summary}),
|
||||
actor <- Actors.register_bot(%{name: name, summary: summary}),
|
||||
{:ok, %Bot{} = bot} <-
|
||||
Actors.create_bot(%{
|
||||
"type" => type,
|
||||
|
@ -1,9 +1,92 @@
|
||||
defmodule Mobilizon do
|
||||
@moduledoc """
|
||||
Mobilizon is a decentralized and federated Meetup-like using [ActivityPub](http://activitypub.rocks/).
|
||||
Mobilizon is a decentralized and federated Meetup-like using
|
||||
[ActivityPub](http://activitypub.rocks/).
|
||||
|
||||
It consists of an API server build with [Elixir](http://elixir-lang.github.io/) and the [Phoenix Framework](https://hexdocs.pm/phoenix).
|
||||
It consists of an API server build with [Elixir](http://elixir-lang.github.io/)
|
||||
and the [Phoenix Framework](https://hexdocs.pm/phoenix).
|
||||
|
||||
Mobilizon relies on `Guardian` for auth and `Geo`/Postgis for geographical informations.
|
||||
Mobilizon relies on `Guardian` for auth and `Geo`/Postgis for geographical
|
||||
information.
|
||||
"""
|
||||
|
||||
use Application
|
||||
|
||||
import Cachex.Spec
|
||||
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Export.{Feed, ICalendar}
|
||||
|
||||
@name Mix.Project.config()[:name]
|
||||
@version Mix.Project.config()[:version]
|
||||
|
||||
@spec named_version :: String.t()
|
||||
def named_version, do: "#{@name} #{@version}"
|
||||
|
||||
@spec user_agent :: String.t(:w)
|
||||
def user_agent do
|
||||
info = "#{MobilizonWeb.Endpoint.url()} <#{Config.get([:instance, :email], "")}>"
|
||||
|
||||
"#{named_version()}; #{info}"
|
||||
end
|
||||
|
||||
@spec start(:normal | {:takeover, node} | {:failover, node}, term) ::
|
||||
{:ok, pid} | {:ok, pid, term} | {:error, term}
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# supervisors
|
||||
Mobilizon.Storage.Repo,
|
||||
MobilizonWeb.Endpoint,
|
||||
# workers
|
||||
Guardian.DB.Token.SweeperServer,
|
||||
Mobilizon.Service.Federator,
|
||||
cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1),
|
||||
cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1),
|
||||
cachex_spec(:statistics, 10, 60, 60),
|
||||
cachex_spec(:activity_pub, 2500, 3, 15)
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: Mobilizon.Supervisor]
|
||||
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
@spec config_change(keyword, keyword, [atom]) :: :ok
|
||||
def config_change(changed, _new, removed) do
|
||||
MobilizonWeb.Endpoint.config_change(changed, removed)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec cachex_spec(atom, integer, integer, integer, function | nil) :: Supervisor.child_spec()
|
||||
defp cachex_spec(name, limit, default, interval, fallback \\ nil) do
|
||||
%{
|
||||
id: :"cache_#{name}",
|
||||
start:
|
||||
{Cachex, :start_link,
|
||||
[
|
||||
name,
|
||||
Keyword.merge(
|
||||
cachex_options(limit, default, interval),
|
||||
fallback_options(fallback)
|
||||
)
|
||||
]}
|
||||
}
|
||||
end
|
||||
|
||||
@spec cachex_options(integer, integer, integer) :: keyword
|
||||
defp cachex_options(limit, default, interval) do
|
||||
[
|
||||
limit: limit,
|
||||
expiration:
|
||||
expiration(
|
||||
default: :timer.minutes(default),
|
||||
interval: :timer.seconds(interval)
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
@spec fallback_options(function | nil) :: keyword
|
||||
defp fallback_options(nil), do: []
|
||||
defp fallback_options(fallback), do: [fallback: fallback(default: fallback)]
|
||||
end
|
||||
|
@ -1,7 +0,0 @@
|
||||
defmodule Mobilizon.Activity do
|
||||
@moduledoc """
|
||||
Represents an activity
|
||||
"""
|
||||
|
||||
defstruct [:data, :local, :actor, :recipients, :notifications]
|
||||
end
|
@ -1,91 +1,56 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Actors.ActorTypeEnum, :actor_type, [
|
||||
:Person,
|
||||
:Application,
|
||||
:Group,
|
||||
:Organization,
|
||||
:Service
|
||||
])
|
||||
|
||||
defenum(Mobilizon.Actors.ActorOpennessEnum, :actor_openness, [
|
||||
:invite_only,
|
||||
:moderated,
|
||||
:open
|
||||
])
|
||||
|
||||
defenum(Mobilizon.Actors.ActorVisibilityEnum, :actor_visibility_type, [
|
||||
:public,
|
||||
:unlisted,
|
||||
# Probably unused
|
||||
:restricted,
|
||||
:private
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Actors.Actor do
|
||||
@moduledoc """
|
||||
Represents an actor (local and remote actors)
|
||||
Represents an actor (local and remote).
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.{Actors, Config, Crypto}
|
||||
alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member}
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Media.File
|
||||
|
||||
alias Mobilizon.Reports.{Report, Note}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias MobilizonWeb.Router.Helpers, as: Routes
|
||||
alias MobilizonWeb.Endpoint
|
||||
|
||||
import Ecto.Query
|
||||
import Mobilizon.Ecto
|
||||
alias Mobilizon.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
# @type t :: %Actor{description: String.t, id: integer(), inserted_at: DateTime.t, updated_at: DateTime.t, display_name: String.t, domain: String.t, keys: String.t, suspended: boolean(), url: String.t, username: String.t, organized_events: list(), groups: list(), group_request: list(), user: User.t, field: ActorTypeEnum.t}
|
||||
@type t :: %__MODULE__{
|
||||
url: String.t(),
|
||||
outbox_url: String.t(),
|
||||
inbox_url: String.t(),
|
||||
following_url: String.t(),
|
||||
followers_url: String.t(),
|
||||
shared_inbox_url: String.t(),
|
||||
type: ActorType.t(),
|
||||
name: String.t(),
|
||||
domain: String.t(),
|
||||
summary: String.t(),
|
||||
preferred_username: String.t(),
|
||||
keys: String.t(),
|
||||
manually_approves_followers: boolean,
|
||||
openness: ActorOpenness.t(),
|
||||
visibility: ActorVisibility.t(),
|
||||
suspended: boolean,
|
||||
avatar: File.t(),
|
||||
banner: File.t(),
|
||||
user: User.t(),
|
||||
followers: [Follower.t()],
|
||||
followings: [Follower.t()],
|
||||
organized_events: [Event.t()],
|
||||
feed_tokens: [FeedToken.t()],
|
||||
created_reports: [Report.t()],
|
||||
subject_reports: [Report.t()],
|
||||
report_notes: [Note.t()],
|
||||
memberships: [t]
|
||||
}
|
||||
|
||||
schema "actors" do
|
||||
field(:url, :string)
|
||||
field(:outbox_url, :string)
|
||||
field(:inbox_url, :string)
|
||||
field(:following_url, :string)
|
||||
field(:followers_url, :string)
|
||||
field(:shared_inbox_url, :string)
|
||||
field(:type, Mobilizon.Actors.ActorTypeEnum, default: :Person)
|
||||
field(:name, :string)
|
||||
field(:domain, :string, default: nil)
|
||||
field(:summary, :string)
|
||||
field(:preferred_username, :string)
|
||||
field(:keys, :string)
|
||||
field(:manually_approves_followers, :boolean, default: false)
|
||||
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
|
||||
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private)
|
||||
field(:suspended, :boolean, default: false)
|
||||
# field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
|
||||
has_many(:followers, Follower, foreign_key: :target_actor_id)
|
||||
has_many(:followings, Follower, foreign_key: :actor_id)
|
||||
has_many(:organized_events, Event, foreign_key: :organizer_actor_id)
|
||||
many_to_many(:memberships, Actor, join_through: Member)
|
||||
belongs_to(:user, User)
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
|
||||
embeds_one(:avatar, File, on_replace: :update)
|
||||
embeds_one(:banner, File, on_replace: :update)
|
||||
has_many(:created_reports, Report, foreign_key: :reporter_id)
|
||||
has_many(:subject_reports, Report, foreign_key: :reported_id)
|
||||
has_many(:report_notes, Note, foreign_key: :moderator_id)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:url,
|
||||
@required_attrs [:preferred_username, :keys, :suspended, :url]
|
||||
@optional_attrs [
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:shared_inbox_url,
|
||||
@ -95,136 +60,40 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:preferred_username,
|
||||
:keys,
|
||||
:manually_approves_followers,
|
||||
:suspended,
|
||||
:user_id
|
||||
])
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> unique_username_validator()
|
||||
|> validate_required([:preferred_username, :keys, :suspended, :url])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
end
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@doc false
|
||||
def update_changeset(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:name,
|
||||
:summary,
|
||||
:keys,
|
||||
:manually_approves_followers,
|
||||
:suspended,
|
||||
:user_id
|
||||
])
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> validate_required([:preferred_username, :keys, :suspended, :url])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
end
|
||||
@update_required_attrs @required_attrs -- [:url]
|
||||
@update_optional_attrs [:name, :summary, :manually_approves_followers, :user_id]
|
||||
@update_attrs @update_required_attrs ++ @update_optional_attrs
|
||||
|
||||
@doc """
|
||||
Changeset for person registration
|
||||
"""
|
||||
@spec registration_changeset(struct(), map()) :: Ecto.Changeset.t()
|
||||
def registration_changeset(%Actor{} = actor, attrs) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:preferred_username,
|
||||
:domain,
|
||||
:name,
|
||||
:summary,
|
||||
:keys,
|
||||
:suspended,
|
||||
:url,
|
||||
:type,
|
||||
:user_id
|
||||
])
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_required([:preferred_username, :keys, :suspended, :url, :type])
|
||||
end
|
||||
@registration_required_attrs [:preferred_username, :keys, :suspended, :url, :type]
|
||||
@registration_optional_attrs [:domain, :name, :summary, :user_id]
|
||||
@registration_attrs @registration_required_attrs ++ @registration_optional_attrs
|
||||
|
||||
# TODO : Use me !
|
||||
# @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
|
||||
@doc """
|
||||
Changeset for remote actor creation
|
||||
"""
|
||||
@spec remote_actor_creation(map()) :: Ecto.Changeset.t()
|
||||
def remote_actor_creation(params) do
|
||||
changes =
|
||||
%Actor{}
|
||||
|> Ecto.Changeset.cast(params, [
|
||||
:url,
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:shared_inbox_url,
|
||||
:following_url,
|
||||
:followers_url,
|
||||
:type,
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:preferred_username,
|
||||
:keys,
|
||||
:manually_approves_followers
|
||||
])
|
||||
|> validate_required([
|
||||
@remote_actor_creation_required_attrs [
|
||||
:url,
|
||||
:inbox_url,
|
||||
:type,
|
||||
:domain,
|
||||
:preferred_username,
|
||||
:keys
|
||||
])
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_length(:summary, max: 5000)
|
||||
|> validate_length(:preferred_username, max: 100)
|
||||
]
|
||||
@remote_actor_creation_optional_attrs [
|
||||
:outbox_url,
|
||||
:shared_inbox_url,
|
||||
:following_url,
|
||||
:followers_url,
|
||||
:name,
|
||||
:summary,
|
||||
:manually_approves_followers
|
||||
]
|
||||
@remote_actor_creation_attrs @remote_actor_creation_required_attrs ++
|
||||
@remote_actor_creation_optional_attrs
|
||||
|
||||
Logger.debug("Remote actor creation")
|
||||
Logger.debug(inspect(changes))
|
||||
changes
|
||||
end
|
||||
|
||||
def relay_creation(%{url: url, preferred_username: preferred_username} = _params) do
|
||||
key = :public_key.generate_key({:rsa, 2048, 65_537})
|
||||
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
|
||||
pem = [entry] |> :public_key.pem_encode() |> String.trim_trailing()
|
||||
|
||||
vars = %{
|
||||
"name" => Mobilizon.CommonConfig.get([:instance, :name], "Mobilizon"),
|
||||
"summary" =>
|
||||
Mobilizon.CommonConfig.get(
|
||||
[:instance, :description],
|
||||
"An internal service actor for this Mobilizon instance"
|
||||
),
|
||||
"url" => url,
|
||||
"keys" => pem,
|
||||
"preferred_username" => preferred_username,
|
||||
"domain" => nil,
|
||||
"inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
|
||||
"followers_url" => "#{url}/followers",
|
||||
"following_url" => "#{url}/following",
|
||||
"shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
|
||||
"type" => :Application
|
||||
}
|
||||
|
||||
cast(%Actor{}, vars, [
|
||||
@relay_creation_attrs [
|
||||
:type,
|
||||
:name,
|
||||
:summary,
|
||||
@ -236,77 +105,225 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:followers_url,
|
||||
:following_url,
|
||||
:shared_inbox_url
|
||||
])
|
||||
]
|
||||
|
||||
@group_creation_required_attrs [:url, :outbox_url, :inbox_url, :type, :preferred_username]
|
||||
@group_creation_optional_attrs [:shared_inbox_url, :name, :domain, :summary]
|
||||
@group_creation_attrs @group_creation_required_attrs ++ @group_creation_optional_attrs
|
||||
|
||||
schema "actors" do
|
||||
field(:url, :string)
|
||||
field(:outbox_url, :string)
|
||||
field(:inbox_url, :string)
|
||||
field(:following_url, :string)
|
||||
field(:followers_url, :string)
|
||||
field(:shared_inbox_url, :string)
|
||||
field(:type, ActorType, default: :Person)
|
||||
field(:name, :string)
|
||||
field(:domain, :string, default: nil)
|
||||
field(:summary, :string)
|
||||
field(:preferred_username, :string)
|
||||
field(:keys, :string)
|
||||
field(:manually_approves_followers, :boolean, default: false)
|
||||
field(:openness, ActorOpenness, default: :moderated)
|
||||
field(:visibility, ActorVisibility, default: :private)
|
||||
field(:suspended, :boolean, default: false)
|
||||
|
||||
embeds_one(:avatar, File, on_replace: :update)
|
||||
embeds_one(:banner, File, on_replace: :update)
|
||||
belongs_to(:user, User)
|
||||
has_many(:followers, Follower, foreign_key: :target_actor_id)
|
||||
has_many(:followings, Follower, foreign_key: :actor_id)
|
||||
has_many(:organized_events, Event, foreign_key: :organizer_actor_id)
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
|
||||
has_many(:created_reports, Report, foreign_key: :reporter_id)
|
||||
has_many(:subject_reports, Report, foreign_key: :reported_id)
|
||||
has_many(:report_notes, Note, foreign_key: :moderator_id)
|
||||
many_to_many(:memberships, __MODULE__, join_through: Member)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks whether actor visibility is public.
|
||||
"""
|
||||
@spec is_public_visibility(t) :: boolean
|
||||
def is_public_visibility(%__MODULE__{visibility: visibility}) do
|
||||
visibility in [:public, :unlisted]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the display name if available, or the preferred username
|
||||
(with the eventual @domain suffix if it's a distant actor).
|
||||
"""
|
||||
@spec display_name(t) :: String.t()
|
||||
def display_name(%__MODULE__{name: name} = actor) when name in [nil, ""] do
|
||||
preferred_username_and_domain(actor)
|
||||
end
|
||||
|
||||
def display_name(%__MODULE__{name: name}), do: name
|
||||
|
||||
@doc """
|
||||
Returns display name and username.
|
||||
"""
|
||||
@spec display_name_and_username(t) :: String.t()
|
||||
def display_name_and_username(%__MODULE__{name: name} = actor) when name in [nil, ""] do
|
||||
preferred_username_and_domain(actor)
|
||||
end
|
||||
|
||||
def display_name_and_username(%__MODULE__{name: name} = actor) do
|
||||
"#{name} (#{preferred_username_and_domain(actor)})"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the preferred username with the eventual @domain suffix if it's
|
||||
a distant actor.
|
||||
"""
|
||||
@spec preferred_username_and_domain(t) :: String.t()
|
||||
def preferred_username_and_domain(%__MODULE__{
|
||||
preferred_username: preferred_username,
|
||||
domain: nil
|
||||
}) do
|
||||
preferred_username
|
||||
end
|
||||
|
||||
def preferred_username_and_domain(%__MODULE__{
|
||||
preferred_username: preferred_username,
|
||||
domain: domain
|
||||
}) do
|
||||
"#{preferred_username}@#{domain}"
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = actor, attrs) do
|
||||
actor
|
||||
|> cast(attrs, @attrs)
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> unique_username_validator()
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec update_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def update_changeset(%__MODULE__{} = actor, attrs) do
|
||||
actor
|
||||
|> cast(attrs, @update_attrs)
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> validate_required(@update_required_attrs)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for person registration.
|
||||
"""
|
||||
@spec registration_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def registration_changeset(%__MODULE__{} = actor, attrs) do
|
||||
actor
|
||||
|> cast(attrs, @registration_attrs)
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_required(@registration_required_attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for remote actor creation.
|
||||
"""
|
||||
@spec remote_actor_creation_changeset(map) :: Ecto.Changeset.t()
|
||||
def remote_actor_creation_changeset(attrs) do
|
||||
changeset =
|
||||
%__MODULE__{}
|
||||
|> cast(attrs, @remote_actor_creation_attrs)
|
||||
|> validate_required(@remote_actor_creation_required_attrs)
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_length(:summary, max: 5000)
|
||||
|> validate_length(:preferred_username, max: 100)
|
||||
|
||||
Logger.debug("Remote actor creation: #{inspect(changeset)}")
|
||||
|
||||
changeset
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for relay creation.
|
||||
"""
|
||||
@spec relay_creation_changeset(map) :: Ecto.Changeset.t()
|
||||
def relay_creation_changeset(attrs) do
|
||||
relay_creation_attrs = build_relay_creation_attrs(attrs)
|
||||
|
||||
cast(%__MODULE__{}, relay_creation_attrs, @relay_creation_attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Changeset for group creation
|
||||
"""
|
||||
@spec group_creation(struct(), map()) :: Ecto.Changeset.t()
|
||||
def group_creation(%Actor{} = actor, params) do
|
||||
@spec group_creation_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def group_creation_changeset(%__MODULE__{} = actor, params) do
|
||||
actor
|
||||
|> Ecto.Changeset.cast(params, [
|
||||
:url,
|
||||
:outbox_url,
|
||||
:inbox_url,
|
||||
:shared_inbox_url,
|
||||
:type,
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:preferred_username
|
||||
])
|
||||
|> cast(params, @group_creation_attrs)
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> build_urls(:Group)
|
||||
|> put_change(:domain, nil)
|
||||
|> put_change(:keys, Actors.create_keys())
|
||||
|> put_change(:keys, Crypto.generate_rsa_2048_private_key())
|
||||
|> put_change(:type, :Group)
|
||||
|> unique_username_validator()
|
||||
|> validate_required([:url, :outbox_url, :inbox_url, :type, :preferred_username])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
|> validate_required(@group_creation_required_attrs)
|
||||
|> unique_constraint(:preferred_username,
|
||||
name: :actors_preferred_username_domain_type_index
|
||||
)
|
||||
|> unique_constraint(:url, name: :actors_url_index)
|
||||
|> validate_length(:summary, max: 5000)
|
||||
|> validate_length(:preferred_username, max: 100)
|
||||
end
|
||||
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
@spec unique_username_validator(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp unique_username_validator(
|
||||
%Ecto.Changeset{changes: %{preferred_username: username} = changes} = changeset
|
||||
) do
|
||||
with nil <- Map.get(changes, :domain, nil),
|
||||
%Actor{preferred_username: _username} <- Actors.get_local_actor_by_name(username) do
|
||||
changeset |> add_error(:preferred_username, "Username is already taken")
|
||||
%__MODULE__{preferred_username: _} <- Actors.get_local_actor_by_name(username) do
|
||||
add_error(changeset, :preferred_username, "Username is already taken")
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
# When we don't even have any preferred_username, don't even try validating preferred_username
|
||||
defp unique_username_validator(changeset) do
|
||||
changeset
|
||||
end
|
||||
defp unique_username_validator(changeset), do: changeset
|
||||
|
||||
@spec build_urls(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
|
||||
@spec build_urls(Ecto.Changeset.t(), ActorType.t()) :: Ecto.Changeset.t()
|
||||
defp build_urls(changeset, type \\ :Person)
|
||||
|
||||
defp build_urls(%Ecto.Changeset{changes: %{preferred_username: username}} = changeset, _type) do
|
||||
changeset
|
||||
|> put_change(
|
||||
:outbox_url,
|
||||
build_url(username, :outbox)
|
||||
)
|
||||
|> put_change(
|
||||
:followers_url,
|
||||
build_url(username, :followers)
|
||||
)
|
||||
|> put_change(
|
||||
:following_url,
|
||||
build_url(username, :following)
|
||||
)
|
||||
|> put_change(
|
||||
:inbox_url,
|
||||
build_url(username, :inbox)
|
||||
)
|
||||
|> put_change(:outbox_url, build_url(username, :outbox))
|
||||
|> put_change(:followers_url, build_url(username, :followers))
|
||||
|> put_change(:following_url, build_url(username, :following))
|
||||
|> put_change(:inbox_url, build_url(username, :inbox))
|
||||
|> put_change(:shared_inbox_url, "#{MobilizonWeb.Endpoint.url()}/inbox")
|
||||
|> put_change(:url, build_url(username, :page))
|
||||
end
|
||||
@ -314,19 +331,19 @@ defmodule Mobilizon.Actors.Actor do
|
||||
defp build_urls(%Ecto.Changeset{} = changeset, _type), do: changeset
|
||||
|
||||
@doc """
|
||||
Build an AP URL for an actor
|
||||
Builds an AP URL for an actor.
|
||||
"""
|
||||
@spec build_url(String.t(), atom()) :: String.t()
|
||||
@spec build_url(String.t(), atom, keyword) :: String.t()
|
||||
def build_url(preferred_username, endpoint, args \\ [])
|
||||
|
||||
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
|
||||
|
||||
def build_url(preferred_username, :page, args) do
|
||||
Endpoint
|
||||
|> Routes.page_url(:actor, preferred_username, args)
|
||||
|> URI.decode()
|
||||
end
|
||||
|
||||
def build_url(username, :inbox, _args), do: "#{build_url(username, :page)}/inbox"
|
||||
|
||||
def build_url(preferred_username, endpoint, args)
|
||||
when endpoint in [:outbox, :following, :followers] do
|
||||
Endpoint
|
||||
@ -334,267 +351,24 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|> URI.decode()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a public key for a given ActivityPub actor ID (url)
|
||||
"""
|
||||
@spec get_public_key_for_url(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
def get_public_key_for_url(url) do
|
||||
with {:ok, %Actor{keys: keys}} <- Actors.get_or_fetch_by_url(url),
|
||||
{:ok, public_key} <- prepare_public_key(keys) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
{:error, :pem_decode_error} ->
|
||||
Logger.error("Error while decoding PEM")
|
||||
{:error, :pem_decode_error}
|
||||
|
||||
_ ->
|
||||
Logger.error("Unable to fetch actor, so no keys for you")
|
||||
{:error, :actor_fetch_error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert internal PEM encoded keys to public key format
|
||||
"""
|
||||
@spec prepare_public_key(String.t()) :: {:ok, tuple()} | {:error, :pem_decode_error}
|
||||
def prepare_public_key(public_key_code) do
|
||||
case :public_key.pem_decode(public_key_code) do
|
||||
[public_key_entry] ->
|
||||
{:ok, :public_key.pem_entry_decode(public_key_entry)}
|
||||
|
||||
_err ->
|
||||
{:error, :pem_decode_error}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get followers from an actor
|
||||
|
||||
If actor A and C both follow actor B, actor B's followers are A and C
|
||||
"""
|
||||
@spec get_followers(struct(), number(), number()) :: map()
|
||||
def get_followers(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
|
||||
query =
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.actor_id,
|
||||
where: f.target_actor_id == ^actor_id
|
||||
)
|
||||
|
||||
total = Task.async(fn -> Repo.aggregate(query, :count, :id) end)
|
||||
elements = Task.async(fn -> Repo.all(paginate(query, page, limit)) end)
|
||||
|
||||
%{total: Task.await(total), elements: Task.await(elements)}
|
||||
end
|
||||
|
||||
defp get_full_followers_query(%Actor{id: actor_id} = _actor) do
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.actor_id,
|
||||
where: f.target_actor_id == ^actor_id
|
||||
)
|
||||
end
|
||||
|
||||
@spec get_full_followers(struct()) :: list()
|
||||
def get_full_followers(%Actor{} = actor) do
|
||||
actor
|
||||
|> get_full_followers_query()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec get_full_external_followers(struct()) :: list()
|
||||
def get_full_external_followers(%Actor{} = actor) do
|
||||
actor
|
||||
|> get_full_followers_query()
|
||||
|> where([a], not is_nil(a.domain))
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get followings from an actor
|
||||
|
||||
If actor A follows actor B and C, actor A's followings are B and B
|
||||
"""
|
||||
@spec get_followings(struct(), number(), number()) :: list()
|
||||
def get_followings(%Actor{id: actor_id} = _actor, page \\ nil, limit \\ nil) do
|
||||
query =
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.target_actor_id,
|
||||
where: f.actor_id == ^actor_id
|
||||
)
|
||||
|
||||
total = Task.async(fn -> Repo.aggregate(query, :count, :id) end)
|
||||
elements = Task.async(fn -> Repo.all(paginate(query, page, limit)) end)
|
||||
|
||||
%{total: Task.await(total), elements: Task.await(elements)}
|
||||
end
|
||||
|
||||
@spec get_full_followings(struct()) :: list()
|
||||
def get_full_followings(%Actor{id: actor_id} = _actor) do
|
||||
Repo.all(
|
||||
from(
|
||||
a in Actor,
|
||||
join: f in Follower,
|
||||
on: a.id == f.target_actor_id,
|
||||
where: f.actor_id == ^actor_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the groups an actor is member of
|
||||
"""
|
||||
@spec get_groups_member_of(struct()) :: list()
|
||||
def get_groups_member_of(%Actor{id: actor_id}) do
|
||||
Repo.all(
|
||||
from(
|
||||
a in Actor,
|
||||
join: m in Member,
|
||||
on: a.id == m.parent_id,
|
||||
where: m.actor_id == ^actor_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the members for a group actor
|
||||
"""
|
||||
@spec get_members_for_group(struct()) :: list()
|
||||
def get_members_for_group(%Actor{id: actor_id}) do
|
||||
Repo.all(
|
||||
from(
|
||||
a in Actor,
|
||||
join: m in Member,
|
||||
on: a.id == m.actor_id,
|
||||
where: m.parent_id == ^actor_id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor follow another
|
||||
"""
|
||||
@spec follow(struct(), struct(), boolean()) :: Follower.t() | {:error, String.t()}
|
||||
def follow(%Actor{} = followed, %Actor{} = follower, url \\ nil, approved \\ true) do
|
||||
with {:suspended, false} <- {:suspended, followed.suspended},
|
||||
# Check if followed has blocked follower
|
||||
{:already_following, false} <- {:already_following, following?(follower, followed)} do
|
||||
do_follow(follower, followed, approved, url)
|
||||
else
|
||||
{:already_following, %Follower{}} ->
|
||||
{:error, :already_following,
|
||||
"Could not follow actor: you are already following #{followed.preferred_username}"}
|
||||
|
||||
{:suspended, _} ->
|
||||
{:error, :suspended,
|
||||
"Could not follow actor: #{followed.preferred_username} has been suspended"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unfollow an actor (remove a `Mobilizon.Actors.Follower`)
|
||||
"""
|
||||
@spec unfollow(struct(), struct()) :: {:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
|
||||
def unfollow(%Actor{} = followed, %Actor{} = follower) do
|
||||
case {:already_following, following?(follower, followed)} do
|
||||
{:already_following, %Follower{} = follow} ->
|
||||
Actors.delete_follower(follow)
|
||||
|
||||
{:already_following, false} ->
|
||||
{:error, "Could not unfollow actor: you are not following #{followed.preferred_username}"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_follow(struct(), struct(), boolean(), String.t()) ::
|
||||
{:ok, Follower.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp do_follow(%Actor{} = follower, %Actor{} = followed, approved, url) do
|
||||
Logger.info(
|
||||
"Making #{follower.preferred_username} follow #{followed.preferred_username} (approved: #{
|
||||
approved
|
||||
})"
|
||||
)
|
||||
|
||||
Actors.create_follower(%{
|
||||
"actor_id" => follower.id,
|
||||
"target_actor_id" => followed.id,
|
||||
"approved" => approved,
|
||||
"url" => url
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether an actor is following another
|
||||
"""
|
||||
@spec following?(struct(), struct()) :: Follower.t() | false
|
||||
def following?(
|
||||
%Actor{} = follower_actor,
|
||||
%Actor{} = followed_actor
|
||||
) do
|
||||
case Actors.get_follower(followed_actor, follower_actor) do
|
||||
nil -> false
|
||||
%Follower{} = follow -> follow
|
||||
end
|
||||
end
|
||||
|
||||
@spec public_visibility?(struct()) :: boolean()
|
||||
def public_visibility?(%Actor{visibility: visibility}), do: visibility in [:public, :unlisted]
|
||||
|
||||
@doc """
|
||||
Return the preferred_username with the eventual @domain suffix if it's a distant actor
|
||||
"""
|
||||
@spec actor_acct_from_actor(struct()) :: String.t()
|
||||
def actor_acct_from_actor(%Actor{preferred_username: preferred_username, domain: domain}) do
|
||||
if is_nil(domain) do
|
||||
preferred_username
|
||||
else
|
||||
"#{preferred_username}@#{domain}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the display name if available, or the preferred_username (with the eventual @domain suffix if it's a distant actor).
|
||||
"""
|
||||
@spec display_name(struct()) :: String.t()
|
||||
def display_name(%Actor{name: name} = actor) do
|
||||
case name do
|
||||
nil -> actor_acct_from_actor(actor)
|
||||
"" -> actor_acct_from_actor(actor)
|
||||
name -> name
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return display name and username
|
||||
|
||||
## Examples
|
||||
iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: nil})
|
||||
"Thomas (tcit)"
|
||||
|
||||
iex> display_name_and_username(%Actor{name: "Thomas C", preferred_username: "tcit", domain: "framapiaf.org"})
|
||||
"Thomas (tcit@framapiaf.org)"
|
||||
|
||||
iex> display_name_and_username(%Actor{name: nil, preferred_username: "tcit", domain: "framapiaf.org"})
|
||||
"tcit@framapiaf.org"
|
||||
|
||||
"""
|
||||
@spec display_name_and_username(struct()) :: String.t()
|
||||
def display_name_and_username(%Actor{name: nil} = actor), do: actor_acct_from_actor(actor)
|
||||
def display_name_and_username(%Actor{name: ""} = actor), do: actor_acct_from_actor(actor)
|
||||
|
||||
def display_name_and_username(%Actor{name: name} = actor),
|
||||
do: name <> " (" <> actor_acct_from_actor(actor) <> ")"
|
||||
|
||||
@doc """
|
||||
Clear multiple caches for an actor
|
||||
"""
|
||||
@spec clear_cache(struct()) :: {:ok, true}
|
||||
def clear_cache(%Actor{preferred_username: preferred_username, domain: nil}) do
|
||||
Cachex.del(:activity_pub, "actor_" <> preferred_username)
|
||||
Cachex.del(:feed, "actor_" <> preferred_username)
|
||||
Cachex.del(:ics, "actor_" <> preferred_username)
|
||||
@spec build_relay_creation_attrs(map) :: map
|
||||
defp build_relay_creation_attrs(%{url: url, preferred_username: preferred_username}) do
|
||||
%{
|
||||
"name" => Config.get([:instance, :name], "Mobilizon"),
|
||||
"summary" =>
|
||||
Config.get(
|
||||
[:instance, :description],
|
||||
"An internal service actor for this Mobilizon instance"
|
||||
),
|
||||
"url" => url,
|
||||
"keys" => Crypto.generate_rsa_2048_private_key(),
|
||||
"preferred_username" => preferred_username,
|
||||
"domain" => nil,
|
||||
"inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
|
||||
"followers_url" => "#{url}/followers",
|
||||
"following_url" => "#{url}/following",
|
||||
"shared_inbox_url" => "#{MobilizonWeb.Endpoint.url()}/inbox",
|
||||
"type" => :Application
|
||||
}
|
||||
end
|
||||
end
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,30 @@
|
||||
defmodule Mobilizon.Actors.Bot do
|
||||
@moduledoc """
|
||||
Represents a local bot
|
||||
Represents a local bot.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
source: String.t(),
|
||||
type: String.t(),
|
||||
actor: Actor.t(),
|
||||
user: User.t()
|
||||
}
|
||||
|
||||
@required_attrs [:source]
|
||||
@optional_attrs [:type, :actor_id, :user_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "bots" do
|
||||
field(:source, :string)
|
||||
field(:type, :string, default: :ics)
|
||||
|
||||
belongs_to(:actor, Actor)
|
||||
belongs_to(:user, User)
|
||||
|
||||
@ -17,9 +32,10 @@ defmodule Mobilizon.Actors.Bot do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(bot, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = bot, attrs) do
|
||||
bot
|
||||
|> cast(attrs, [:source, :type, :actor_id, :user_id])
|
||||
|> validate_required([:source])
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
|
@ -1,52 +1,66 @@
|
||||
defmodule Mobilizon.Actors.Follower do
|
||||
@moduledoc """
|
||||
Represents the following of an actor to another actor
|
||||
Represents the following of an actor to another actor.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Actors.Follower
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@type t :: %__MODULE__{
|
||||
approved: boolean,
|
||||
url: String.t(),
|
||||
target_actor: Actor.t(),
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
@required_attrs [:url, :approved, :target_actor_id, :actor_id]
|
||||
@attrs @required_attrs
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "followers" do
|
||||
field(:approved, :boolean, default: false)
|
||||
field(:url, :string)
|
||||
|
||||
belongs_to(:target_actor, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Follower{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, [:url, :approved, :target_actor_id, :actor_id])
|
||||
|> generate_url()
|
||||
|> validate_required([:url, :approved, :target_actor_id, :actor_id])
|
||||
|> unique_constraint(:target_actor_id, name: :followers_actor_target_actor_unique_index)
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(follower, attrs) do
|
||||
follower
|
||||
|> cast(attrs, @attrs)
|
||||
|> ensure_url()
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:target_actor_id,
|
||||
name: :followers_actor_target_actor_unique_index
|
||||
)
|
||||
end
|
||||
|
||||
# If there's a blank URL that's because we're doing the first insert
|
||||
defp generate_url(%Ecto.Changeset{data: %Follower{url: nil}} = changeset) do
|
||||
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do
|
||||
case fetch_change(changeset, :url) do
|
||||
{:ok, _url} -> changeset
|
||||
:error -> do_generate_url(changeset)
|
||||
{:ok, _url} ->
|
||||
changeset
|
||||
|
||||
:error ->
|
||||
generate_url(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
# Most time just go with the given URL
|
||||
defp generate_url(%Ecto.Changeset{} = changeset), do: changeset
|
||||
defp ensure_url(%Ecto.Changeset{} = changeset), do: changeset
|
||||
|
||||
defp do_generate_url(%Ecto.Changeset{} = changeset) do
|
||||
@spec generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp generate_url(%Ecto.Changeset{} = changeset) do
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
changeset
|
||||
|> put_change(
|
||||
:url,
|
||||
"#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}"
|
||||
)
|
||||
|> put_change(
|
||||
:id,
|
||||
uuid
|
||||
)
|
||||
|> put_change(:id, uuid)
|
||||
|> put_change(:url, "#{MobilizonWeb.Endpoint.url()}/follow/#{uuid}")
|
||||
end
|
||||
end
|
||||
|
@ -1,101 +1,59 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Actors.MemberRoleEnum, :member_role_type, [
|
||||
:not_approved,
|
||||
:member,
|
||||
:moderator,
|
||||
:administrator,
|
||||
:creator
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Actors.Member do
|
||||
@moduledoc """
|
||||
Represents the membership of an actor to a group
|
||||
Represents the membership of an actor to a group.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query, warn: false
|
||||
import Mobilizon.Ecto
|
||||
|
||||
alias Mobilizon.Actors.Member
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Repo
|
||||
alias Mobilizon.Actors.{Actor, MemberRole}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
role: MemberRole.t(),
|
||||
parent: Actor.t(),
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
@required_attrs [:parent_id, :actor_id]
|
||||
@optional_attrs [:role]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "members" do
|
||||
field(:role, Mobilizon.Actors.MemberRoleEnum, default: :member)
|
||||
field(:role, MemberRole, default: :member)
|
||||
|
||||
belongs_to(:parent, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Member{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, [:role, :parent_id, :actor_id])
|
||||
|> validate_required([:parent_id, :actor_id])
|
||||
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single member of an actor (for example a group)
|
||||
Gets the default member role depending on the actor openness.
|
||||
"""
|
||||
def get_member(actor_id, parent_id) do
|
||||
case Repo.get_by(Member, actor_id: actor_id, parent_id: parent_id) do
|
||||
nil -> {:error, :member_not_found}
|
||||
member -> {:ok, member}
|
||||
end
|
||||
end
|
||||
@spec get_default_member_role(Actor.t()) :: atom
|
||||
def get_default_member_role(%Actor{openness: :open}), do: :member
|
||||
def get_default_member_role(%Actor{}), do: :not_approved
|
||||
|
||||
@doc """
|
||||
Gets a single member of an actor (for example a group)
|
||||
Checks whether the actor can be joined to the group.
|
||||
"""
|
||||
def can_be_joined(%Actor{type: :Group, openness: :invite_only}), do: false
|
||||
def can_be_joined(%Actor{type: :Group}), do: true
|
||||
|
||||
@doc """
|
||||
Returns the list of administrator members for a group.
|
||||
Checks whether the member is an administrator (admin or creator) of the group.
|
||||
"""
|
||||
def list_administrator_members_for_group(id, page \\ nil, limit \\ nil) do
|
||||
Repo.all(
|
||||
from(
|
||||
m in Member,
|
||||
where: m.parent_id == ^id and (m.role == ^:creator or m.role == ^:administrator),
|
||||
preload: [:actor]
|
||||
)
|
||||
|> paginate(page, limit)
|
||||
)
|
||||
end
|
||||
def is_administrator(%__MODULE__{role: :administrator}), do: {:is_admin, true}
|
||||
def is_administrator(%__MODULE__{role: :creator}), do: {:is_admin, true}
|
||||
def is_administrator(%__MODULE__{}), do: {:is_admin, false}
|
||||
|
||||
@doc """
|
||||
Get all group ids where the actor_id is the last administrator
|
||||
"""
|
||||
def list_group_id_where_last_administrator(actor_id) do
|
||||
in_query =
|
||||
from(
|
||||
m in Member,
|
||||
where: m.actor_id == ^actor_id and (m.role == ^:creator or m.role == ^:administrator),
|
||||
select: m.parent_id
|
||||
)
|
||||
|
||||
Repo.all(
|
||||
from(
|
||||
m in Member,
|
||||
where: m.role == ^:creator or m.role == ^:administrator,
|
||||
join: m2 in subquery(in_query),
|
||||
on: m.parent_id == m2.parent_id,
|
||||
group_by: m.parent_id,
|
||||
select: m.parent_id,
|
||||
having: count(m.actor_id) == 1
|
||||
)
|
||||
)
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = member, attrs) do
|
||||
member
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:parent_id, name: :members_actor_parent_unique_index)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the member is an administrator (admin or creator) of the group
|
||||
"""
|
||||
def is_administrator(%Member{role: :administrator}), do: {:is_admin, true}
|
||||
def is_administrator(%Member{role: :creator}), do: {:is_admin, true}
|
||||
def is_administrator(%Member{}), do: {:is_admin, false}
|
||||
end
|
||||
|
@ -1,12 +1,30 @@
|
||||
defmodule Mobilizon.Addresses.Address do
|
||||
@moduledoc "An address for an event or a group"
|
||||
@moduledoc """
|
||||
Represents an address for an event or a group.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
alias Mobilizon.Events.Event
|
||||
# alias Mobilizon.Actors.Actor
|
||||
@attrs [
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
country: String.t(),
|
||||
locality: String.t(),
|
||||
region: String.t(),
|
||||
description: String.t(),
|
||||
floor: String.t(),
|
||||
geom: Geo.PostGIS.Geometry.t(),
|
||||
postal_code: String.t(),
|
||||
street: String.t(),
|
||||
url: String.t(),
|
||||
origin_id: String.t(),
|
||||
events: [Event.t()]
|
||||
}
|
||||
|
||||
@required_attrs [:url]
|
||||
@optional_attrs [
|
||||
:description,
|
||||
:floor,
|
||||
:geom,
|
||||
@ -15,12 +33,9 @@ defmodule Mobilizon.Addresses.Address do
|
||||
:region,
|
||||
:postal_code,
|
||||
:street,
|
||||
:url,
|
||||
:origin_id
|
||||
]
|
||||
@required [
|
||||
:url
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "addresses" do
|
||||
field(:country, :string)
|
||||
@ -33,22 +48,25 @@ defmodule Mobilizon.Addresses.Address do
|
||||
field(:street, :string)
|
||||
field(:url, :string)
|
||||
field(:origin_id, :string)
|
||||
has_many(:event, Event, foreign_key: :physical_address_id)
|
||||
|
||||
has_many(:events, Event, foreign_key: :physical_address_id)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Address{} = address, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = address, attrs) do
|
||||
address
|
||||
|> cast(attrs, @attrs)
|
||||
|> set_url()
|
||||
|> validate_required(@required)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
@spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
|
||||
url =
|
||||
Map.get(changes, :url, MobilizonWeb.Endpoint.url() <> "/address/#{Ecto.UUID.generate()}")
|
||||
uuid = Ecto.UUID.generate()
|
||||
url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{uuid}")
|
||||
|
||||
put_change(changeset, :url, url)
|
||||
end
|
||||
|
@ -3,82 +3,36 @@ defmodule Mobilizon.Addresses do
|
||||
The Addresses context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Mobilizon.Repo
|
||||
require Logger
|
||||
import Ecto.Query
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
@geom_types [:point]
|
||||
|
||||
@doc false
|
||||
def data() do
|
||||
Dataloader.Ecto.new(Repo, query: &query/2)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def query(queryable, _params) do
|
||||
queryable
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of addresses.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_addresses()
|
||||
[%Address{}, ...]
|
||||
|
||||
"""
|
||||
def list_addresses do
|
||||
Repo.all(Address)
|
||||
end
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Gets a single address.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Address does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_address!(123)
|
||||
%Address{}
|
||||
|
||||
iex> get_address!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_address!(id), do: Repo.get!(Address, id)
|
||||
|
||||
@spec get_address(integer | String.t()) :: Address.t() | nil
|
||||
def get_address(id), do: Repo.get(Address, id)
|
||||
|
||||
@doc """
|
||||
Gets a single address by it's url
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_address_by_url("https://mobilizon.social/addresses/4572")
|
||||
%Address{}
|
||||
|
||||
iex> get_address_by_url("https://mobilizon.social/addresses/099")
|
||||
nil
|
||||
Gets a single address.
|
||||
Raises `Ecto.NoResultsError` if the address does not exist.
|
||||
"""
|
||||
def get_address_by_url(url) do
|
||||
Repo.get_by(Address, url: url)
|
||||
end
|
||||
@spec get_address!(integer | String.t()) :: Address.t()
|
||||
def get_address!(id), do: Repo.get!(Address, id)
|
||||
|
||||
@doc """
|
||||
Creates a address.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_address(%{field: value})
|
||||
{:ok, %Address{}}
|
||||
|
||||
iex> create_address(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
Gets a single address by its url.
|
||||
"""
|
||||
@spec get_address_by_url(String.t()) :: Address.t() | nil
|
||||
def get_address_by_url(url), do: Repo.get_by(Address, url: url)
|
||||
|
||||
@doc """
|
||||
Creates an address.
|
||||
"""
|
||||
@spec create_address(map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_address(attrs \\ %{}) do
|
||||
%Address{}
|
||||
|> Address.changeset(attrs)
|
||||
@ -89,17 +43,9 @@ defmodule Mobilizon.Addresses do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a address.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_address(address, %{field: new_value})
|
||||
{:ok, %Address{}}
|
||||
|
||||
iex> update_address(address, %{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
Updates an address.
|
||||
"""
|
||||
@spec update_address(Address.t(), map) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_address(%Address{} = address, attrs) do
|
||||
address
|
||||
|> Address.changeset(attrs)
|
||||
@ -107,131 +53,96 @@ defmodule Mobilizon.Addresses do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a Address.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_address(address)
|
||||
{:ok, %Address{}}
|
||||
|
||||
iex> delete_address(address)
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
Deletes an address.
|
||||
"""
|
||||
def delete_address(%Address{} = address) do
|
||||
Repo.delete(address)
|
||||
end
|
||||
@spec delete_address(Address.t()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_address(%Address{} = address), do: Repo.delete(address)
|
||||
|
||||
@doc """
|
||||
Returns an `%Ecto.Changeset{}` for tracking address changes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> change_address(address)
|
||||
%Ecto.Changeset{source: %Address{}}
|
||||
|
||||
Returns the list of addresses.
|
||||
"""
|
||||
def change_address(%Address{} = address) do
|
||||
Address.changeset(address, %{})
|
||||
end
|
||||
@spec list_addresses :: [Address.t()]
|
||||
def list_addresses, do: Repo.all(Address)
|
||||
|
||||
@doc """
|
||||
Processes raw geo data informations and return a `Geo` geometry which can be one of `Geo.Point`.
|
||||
Searches addresses.
|
||||
|
||||
We only look at the description for now, and eventually order by object distance.
|
||||
"""
|
||||
# TODO: Unused, remove me
|
||||
def process_geom(%{"type" => type_input, "data" => data}) do
|
||||
type =
|
||||
if !is_atom(type_input) && type_input != nil do
|
||||
try do
|
||||
String.to_existing_atom(type_input)
|
||||
rescue
|
||||
e in ArgumentError ->
|
||||
Logger.error("#{type_input} is not an existing atom : #{inspect(e)}")
|
||||
:invalid_type
|
||||
end
|
||||
else
|
||||
type_input
|
||||
end
|
||||
|
||||
if Enum.member?(@geom_types, type) do
|
||||
case type do
|
||||
:point ->
|
||||
process_point(data["latitude"], data["longitude"])
|
||||
end
|
||||
else
|
||||
{:error, :invalid_type}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def process_geom(nil) do
|
||||
{:error, nil}
|
||||
end
|
||||
|
||||
@spec process_point(number(), number()) :: tuple()
|
||||
defp process_point(latitude, longitude) when is_number(latitude) and is_number(longitude) do
|
||||
{:ok, %Geo.Point{coordinates: {latitude, longitude}, srid: 4326}}
|
||||
end
|
||||
|
||||
defp process_point(_, _) do
|
||||
{:error, "Latitude and longitude must be numbers"}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Search addresses in our database
|
||||
|
||||
We only look at the description for now, and eventually order by object distance
|
||||
"""
|
||||
@spec search_addresses(String.t(), list()) :: list(Address.t())
|
||||
@spec search_addresses(String.t(), keyword) :: [Address.t()]
|
||||
def search_addresses(search, options \\ []) do
|
||||
limit = Keyword.get(options, :limit, 5)
|
||||
|
||||
query = from(a in Address, where: ilike(a.description, ^"%#{search}%"), limit: ^limit)
|
||||
|
||||
query =
|
||||
if coords = Keyword.get(options, :coords, false),
|
||||
do:
|
||||
from(a in query,
|
||||
order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")]
|
||||
),
|
||||
else: query
|
||||
search
|
||||
|> search_addresses_query(Keyword.get(options, :limit, 5))
|
||||
|> order_by_coords(Keyword.get(options, :coords))
|
||||
|> filter_by_contry(Keyword.get(options, :country))
|
||||
|
||||
query =
|
||||
if country = Keyword.get(options, :country, nil),
|
||||
do: from(a in query, where: ilike(a.country, ^"%#{country}%")),
|
||||
else: query
|
||||
|
||||
if Keyword.get(options, :single, false) == true, do: Repo.one(query), else: Repo.all(query)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reverse geocode from coordinates in our database
|
||||
|
||||
We only take addresses 50km around and sort them by distance
|
||||
"""
|
||||
@spec reverse_geocode(number(), number(), list()) :: list(Address.t())
|
||||
def reverse_geocode(lon, lat, options) do
|
||||
limit = Keyword.get(options, :limit, 5)
|
||||
radius = Keyword.get(options, :radius, 50_000)
|
||||
country = Keyword.get(options, :country, nil)
|
||||
srid = Keyword.get(options, :srid, 4326)
|
||||
|
||||
import Geo.PostGIS
|
||||
|
||||
with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do
|
||||
query =
|
||||
from(a in Address,
|
||||
order_by: [fragment("? <-> ?", a.geom, ^point)],
|
||||
limit: ^limit,
|
||||
where: st_dwithin_in_meters(^point, a.geom, ^radius)
|
||||
)
|
||||
|
||||
query =
|
||||
if country,
|
||||
do: from(a in query, where: ilike(a.country, ^"%#{country}%")),
|
||||
else: query
|
||||
case Keyword.get(options, :single, false) do
|
||||
true ->
|
||||
Repo.one(query)
|
||||
|
||||
false ->
|
||||
Repo.all(query)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reverse geocode from coordinates.
|
||||
|
||||
We only take addresses 50km around and sort them by distance.
|
||||
"""
|
||||
@spec reverse_geocode(number, number, keyword) :: [Address.t()]
|
||||
def reverse_geocode(lon, lat, options) do
|
||||
limit = Keyword.get(options, :limit, 5)
|
||||
radius = Keyword.get(options, :radius, 50_000)
|
||||
country = Keyword.get(options, :country)
|
||||
srid = Keyword.get(options, :srid, 4326)
|
||||
|
||||
with {:ok, point} <- Geo.WKT.decode("SRID=#{srid};POINT(#{lon} #{lat})") do
|
||||
point
|
||||
|> addresses_around_query(radius, limit)
|
||||
|> filter_by_contry(country)
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
||||
|
||||
@spec search_addresses_query(String.t(), integer) :: Ecto.Query.t()
|
||||
defp search_addresses_query(search, limit) do
|
||||
from(
|
||||
a in Address,
|
||||
where: ilike(a.description, ^"%#{search}%"),
|
||||
limit: ^limit
|
||||
)
|
||||
end
|
||||
|
||||
@spec order_by_coords(Ecto.Query.t(), map | nil) :: Ecto.Query.t()
|
||||
defp order_by_coords(query, nil), do: query
|
||||
|
||||
defp order_by_coords(query, coords) do
|
||||
from(
|
||||
a in query,
|
||||
order_by: [fragment("? <-> ?", a.geom, ^"POINT(#{coords.lon} #{coords.lat})'")]
|
||||
)
|
||||
end
|
||||
|
||||
@spec filter_by_contry(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t()
|
||||
defp filter_by_contry(query, nil), do: query
|
||||
|
||||
defp filter_by_contry(query, country) do
|
||||
from(
|
||||
a in query,
|
||||
where: ilike(a.country, ^"%#{country}%")
|
||||
)
|
||||
end
|
||||
|
||||
@spec addresses_around_query(Geo.geometry(), integer, integer) :: Ecto.Query.t()
|
||||
defp addresses_around_query(point, radius, limit) do
|
||||
import Geo.PostGIS
|
||||
|
||||
from(a in Address,
|
||||
where: st_dwithin_in_meters(^point, a.geom, ^radius),
|
||||
order_by: [fragment("? <-> ?", a.geom, ^point)],
|
||||
limit: ^limit
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -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
|
@ -8,30 +8,45 @@ defenum(Mobilizon.Admin.ActionLogAction, [
|
||||
|
||||
defmodule Mobilizon.Admin.ActionLog do
|
||||
@moduledoc """
|
||||
ActionLog entity schema
|
||||
Represents an action log entity.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Admin.ActionLogAction
|
||||
|
||||
@timestamps_opts [type: :utc_datetime]
|
||||
@type t :: %__MODULE__{
|
||||
action: String.t(),
|
||||
target_type: String.t(),
|
||||
target_id: integer,
|
||||
changes: map,
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
@required_attrs [:action, :target_type, :target_id, :changes, :actor_id]
|
||||
@attrs @required_attrs
|
||||
|
||||
@timestamps_opts [type: :utc_datetime]
|
||||
|
||||
schema "admin_action_logs" do
|
||||
field(:action, ActionLogAction)
|
||||
field(:target_type, :string)
|
||||
field(:target_id, :integer)
|
||||
field(:changes, :map)
|
||||
|
||||
belongs_to(:actor, Actor)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(action_log, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = action_log, attrs) do
|
||||
action_log
|
||||
|> cast(attrs, @required_attrs)
|
||||
|> validate_required(@required_attrs -- [:changes])
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
|
35
lib/mobilizon/admin/admin.ex
Normal file
35
lib/mobilizon/admin/admin.ex
Normal 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
|
@ -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
|
@ -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
77
lib/mobilizon/config.ex
Normal 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
28
lib/mobilizon/crypto.ex
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -1,33 +1,42 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Events.CommentVisibilityEnum, :comment_visibility_type, [
|
||||
:public,
|
||||
:unlisted,
|
||||
:private,
|
||||
:moderated,
|
||||
:invite
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Events.Comment do
|
||||
@moduledoc """
|
||||
An actor comment (for instance on an event or on a group)
|
||||
Represents an actor comment (for instance on an event or on a group).
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Comment
|
||||
alias Mobilizon.Events.{Comment, CommentVisibility, Event}
|
||||
|
||||
alias MobilizonWeb.Router.Helpers, as: Routes
|
||||
alias MobilizonWeb.Endpoint
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
text: String.t(),
|
||||
url: String.t(),
|
||||
local: boolean,
|
||||
visibility: CommentVisibility.t(),
|
||||
uuid: Ecto.UUID.t(),
|
||||
actor: Actor.t(),
|
||||
attributed_to: Actor.t(),
|
||||
event: Event.t(),
|
||||
in_reply_to_comment: t,
|
||||
origin_comment: t
|
||||
}
|
||||
|
||||
@required_attrs [:text, :actor_id, :url]
|
||||
@optional_attrs [:event_id, :in_reply_to_comment_id, :origin_comment_id, :attributed_to_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "comments" do
|
||||
field(:text, :string)
|
||||
field(:url, :string)
|
||||
field(:local, :boolean, default: true)
|
||||
field(:visibility, Mobilizon.Events.CommentVisibilityEnum, default: :public)
|
||||
field(:visibility, CommentVisibility, default: :public)
|
||||
field(:uuid, Ecto.UUID)
|
||||
|
||||
belongs_to(:actor, Actor, foreign_key: :actor_id)
|
||||
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
|
||||
belongs_to(:event, Event, foreign_key: :event_id)
|
||||
@ -37,38 +46,27 @@ defmodule Mobilizon.Events.Comment do
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(comment, attrs) do
|
||||
uuid =
|
||||
if Map.has_key?(attrs, "uuid"),
|
||||
do: attrs["uuid"],
|
||||
else: Ecto.UUID.generate()
|
||||
|
||||
# TODO : really change me right away
|
||||
url =
|
||||
if Map.has_key?(attrs, "url"),
|
||||
do: attrs["url"],
|
||||
else: Routes.page_url(Endpoint, :comment, uuid)
|
||||
|
||||
comment
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:url,
|
||||
:text,
|
||||
:actor_id,
|
||||
:event_id,
|
||||
:in_reply_to_comment_id,
|
||||
:origin_comment_id,
|
||||
:attributed_to_id
|
||||
])
|
||||
|> put_change(:uuid, uuid)
|
||||
|> put_change(:url, url)
|
||||
|> validate_required([:text, :actor_id, :url])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the id of the first comment in the conversation
|
||||
Returns the id of the first comment in the conversation.
|
||||
"""
|
||||
def get_thread_id(%Comment{id: id, origin_comment_id: origin_comment_id}) do
|
||||
@spec get_thread_id(t) :: integer
|
||||
def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do
|
||||
origin_comment_id || id
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = comment, attrs) do
|
||||
uuid = attrs["uuid"] || Ecto.UUID.generate()
|
||||
url = attrs["url"] || generate_url(uuid)
|
||||
|
||||
comment
|
||||
|> cast(attrs, @attrs)
|
||||
|> put_change(:uuid, uuid)
|
||||
|> put_change(:url, url)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
@spec generate_url(String.t()) :: String.t()
|
||||
defp generate_url(uuid), do: Routes.page_url(Endpoint, :comment, uuid)
|
||||
end
|
||||
|
@ -1,43 +1,87 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Events.EventVisibilityEnum, :event_visibility_type, [
|
||||
:public,
|
||||
:unlisted,
|
||||
:restricted,
|
||||
:private
|
||||
])
|
||||
|
||||
defenum(Mobilizon.Events.JoinOptionsEnum, :event_join_options_type, [
|
||||
:free,
|
||||
:restricted,
|
||||
:invite
|
||||
])
|
||||
|
||||
defenum(Mobilizon.Events.EventStatusEnum, :event_status_type, [
|
||||
:tentative,
|
||||
:confirmed,
|
||||
:cancelled
|
||||
])
|
||||
|
||||
defenum(Mobilizon.Event.EventCategoryEnum, :event_category_type, [
|
||||
:business,
|
||||
:conference,
|
||||
:birthday,
|
||||
:demonstration,
|
||||
:meeting
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Events.Event do
|
||||
@moduledoc """
|
||||
Represents an event
|
||||
Represents an event.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.{Event, Participant, Tag, Session, Track}
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
EventOptions,
|
||||
EventStatus,
|
||||
EventVisibility,
|
||||
JoinOptions,
|
||||
Participant,
|
||||
Tag,
|
||||
Session,
|
||||
Track
|
||||
}
|
||||
|
||||
alias Mobilizon.Media.Picture
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
url: String.t(),
|
||||
local: boolean,
|
||||
begins_on: DateTime.t(),
|
||||
slug: String.t(),
|
||||
description: String.t(),
|
||||
ends_on: DateTime.t(),
|
||||
title: String.t(),
|
||||
status: EventStatus.t(),
|
||||
visibility: EventVisibility.t(),
|
||||
join_options: JoinOptions.t(),
|
||||
publish_at: DateTime.t(),
|
||||
uuid: Ecto.UUID.t(),
|
||||
online_address: String.t(),
|
||||
phone_address: String.t(),
|
||||
category: String.t(),
|
||||
options: EventOptions.t(),
|
||||
organizer_actor: Actor.t(),
|
||||
attributed_to: Actor.t(),
|
||||
physical_address: Address.t(),
|
||||
picture: Picture.t(),
|
||||
tracks: [Track.t()],
|
||||
sessions: [Session.t()],
|
||||
tags: [Tag.t()],
|
||||
participants: [Actor.t()]
|
||||
}
|
||||
|
||||
@required_attrs [:title, :begins_on, :organizer_actor_id, :url, :uuid]
|
||||
@optional_attrs [
|
||||
:slug,
|
||||
:description,
|
||||
:ends_on,
|
||||
:category,
|
||||
:status,
|
||||
:visibility,
|
||||
:publish_at,
|
||||
:online_address,
|
||||
:phone_address,
|
||||
:picture_id,
|
||||
:physical_address_id
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@update_required_attrs @required_attrs
|
||||
@update_optional_attrs [
|
||||
:slug,
|
||||
:description,
|
||||
:ends_on,
|
||||
:category,
|
||||
:status,
|
||||
:visibility,
|
||||
:publish_at,
|
||||
:online_address,
|
||||
:phone_address,
|
||||
:picture_id,
|
||||
:physical_address_id
|
||||
]
|
||||
@update_attrs @update_required_attrs ++ @update_optional_attrs
|
||||
|
||||
schema "events" do
|
||||
field(:url, :string)
|
||||
field(:local, :boolean, default: true)
|
||||
@ -46,96 +90,59 @@ defmodule Mobilizon.Events.Event do
|
||||
field(:description, :string)
|
||||
field(:ends_on, :utc_datetime)
|
||||
field(:title, :string)
|
||||
field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed)
|
||||
field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public)
|
||||
field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free)
|
||||
field(:status, EventStatus, default: :confirmed)
|
||||
field(:visibility, EventVisibility, default: :public)
|
||||
field(:join_options, JoinOptions, default: :free)
|
||||
field(:publish_at, :utc_datetime)
|
||||
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
|
||||
field(:online_address, :string)
|
||||
field(:phone_address, :string)
|
||||
field(:category, :string)
|
||||
embeds_one(:options, Mobilizon.Events.EventOptions, on_replace: :update)
|
||||
|
||||
embeds_one(:options, EventOptions, on_replace: :update)
|
||||
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
|
||||
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
|
||||
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
|
||||
many_to_many(:participants, Actor, join_through: Participant)
|
||||
has_many(:tracks, Track)
|
||||
has_many(:sessions, Session)
|
||||
belongs_to(:physical_address, Address)
|
||||
belongs_to(:picture, Picture)
|
||||
has_many(:tracks, Track)
|
||||
has_many(:sessions, Session)
|
||||
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
|
||||
many_to_many(:participants, Actor, join_through: Participant)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Event{} = event, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = event, attrs) do
|
||||
event
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:title,
|
||||
:slug,
|
||||
:description,
|
||||
:url,
|
||||
:begins_on,
|
||||
:ends_on,
|
||||
:organizer_actor_id,
|
||||
:category,
|
||||
:status,
|
||||
:visibility,
|
||||
:publish_at,
|
||||
:online_address,
|
||||
:phone_address,
|
||||
:uuid,
|
||||
:picture_id,
|
||||
:physical_address_id
|
||||
])
|
||||
|> cast(attrs, @attrs)
|
||||
|> cast_embed(:options)
|
||||
|> validate_required([
|
||||
:title,
|
||||
:begins_on,
|
||||
:organizer_actor_id,
|
||||
:url,
|
||||
:uuid
|
||||
])
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def update_changeset(%Event{} = event, attrs) do
|
||||
@spec update_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def update_changeset(%__MODULE__{} = event, attrs) do
|
||||
event
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
:title,
|
||||
:slug,
|
||||
:description,
|
||||
:begins_on,
|
||||
:ends_on,
|
||||
:category,
|
||||
:status,
|
||||
:visibility,
|
||||
:publish_at,
|
||||
:online_address,
|
||||
:phone_address,
|
||||
:picture_id,
|
||||
:physical_address_id
|
||||
])
|
||||
|> Ecto.Changeset.cast(attrs, @update_attrs)
|
||||
|> cast_embed(:options)
|
||||
|> put_tags(attrs)
|
||||
|> validate_required([
|
||||
:title,
|
||||
:begins_on,
|
||||
:organizer_actor_id,
|
||||
:url,
|
||||
:uuid
|
||||
])
|
||||
|> validate_required(@update_required_attrs)
|
||||
end
|
||||
|
||||
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
|
||||
defp put_tags(changeset, _), do: changeset
|
||||
|
||||
def can_event_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id)
|
||||
@doc """
|
||||
Checks whether an event can be managed.
|
||||
"""
|
||||
@spec can_be_managed_by(t, integer | String.t()) :: boolean
|
||||
def can_be_managed_by(%__MODULE__{organizer_actor_id: organizer_actor_id}, actor_id)
|
||||
when organizer_actor_id == actor_id do
|
||||
{:event_can_be_managed, true}
|
||||
end
|
||||
|
||||
def can_event_be_managed_by(_event, _actor) do
|
||||
{:event_can_be_managed, false}
|
||||
end
|
||||
def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false}
|
||||
|
||||
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
|
||||
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
|
||||
defp put_tags(changeset, _), do: changeset
|
||||
end
|
||||
|
19
lib/mobilizon/events/event_offer.ex
Normal file
19
lib/mobilizon/events/event_offer.ex
Normal 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
|
@ -1,63 +1,31 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Events.CommentModeration, :comment_moderation, [:allow_all, :moderated, :closed])
|
||||
|
||||
defmodule Mobilizon.Events.EventOffer do
|
||||
@moduledoc """
|
||||
Represents an event offer
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
embedded_schema do
|
||||
field(:price, :float)
|
||||
field(:price_currency, :string)
|
||||
field(:url, :string)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Events.EventParticipationCondition do
|
||||
@moduledoc """
|
||||
Represents an event participation condition
|
||||
"""
|
||||
use Ecto.Schema
|
||||
|
||||
embedded_schema do
|
||||
field(:title, :string)
|
||||
field(:content, :string)
|
||||
field(:url, :string)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Events.EventOptions do
|
||||
@moduledoc """
|
||||
Represents an event options
|
||||
Represents an event options.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
EventOptions,
|
||||
EventOffer,
|
||||
EventParticipationCondition,
|
||||
CommentModeration
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
@derive Jason.Encoder
|
||||
embedded_schema do
|
||||
field(:maximum_attendee_capacity, :integer)
|
||||
field(:remaining_attendee_capacity, :integer)
|
||||
field(:show_remaining_attendee_capacity, :boolean)
|
||||
embeds_many(:offers, EventOffer)
|
||||
embeds_many(:participation_condition, EventParticipationCondition)
|
||||
field(:attendees, {:array, :string})
|
||||
field(:program, :string)
|
||||
field(:comment_moderation, CommentModeration)
|
||||
field(:show_participation_price, :boolean)
|
||||
end
|
||||
@type t :: %__MODULE__{
|
||||
maximum_attendee_capacity: integer,
|
||||
remaining_attendee_capacity: integer,
|
||||
show_remaining_attendee_capacity: boolean,
|
||||
attendees: [String.t()],
|
||||
program: String.t(),
|
||||
comment_moderation: CommentModeration.t(),
|
||||
show_participation_price: boolean,
|
||||
offers: [EventOffer.t()],
|
||||
participation_condition: [EventParticipationCondition.t()]
|
||||
}
|
||||
|
||||
def changeset(%EventOptions{} = event_options, attrs) do
|
||||
event_options
|
||||
|> Ecto.Changeset.cast(attrs, [
|
||||
@attrs [
|
||||
:maximum_attendee_capacity,
|
||||
:remaining_attendee_capacity,
|
||||
:show_remaining_attendee_capacity,
|
||||
@ -65,6 +33,26 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
:program,
|
||||
:comment_moderation,
|
||||
:show_participation_price
|
||||
])
|
||||
]
|
||||
|
||||
@primary_key false
|
||||
@derive Jason.Encoder
|
||||
embedded_schema do
|
||||
field(:maximum_attendee_capacity, :integer)
|
||||
field(:remaining_attendee_capacity, :integer)
|
||||
field(:show_remaining_attendee_capacity, :boolean)
|
||||
field(:attendees, {:array, :string})
|
||||
field(:program, :string)
|
||||
field(:comment_moderation, CommentModeration)
|
||||
field(:show_participation_price, :boolean)
|
||||
|
||||
embeds_many(:offers, EventOffer)
|
||||
embeds_many(:participation_condition, EventParticipationCondition)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = event_options, attrs) do
|
||||
cast(event_options, attrs, @attrs)
|
||||
end
|
||||
end
|
||||
|
19
lib/mobilizon/events/event_participation_condition.ex
Normal file
19
lib/mobilizon/events/event_participation_condition.ex
Normal 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
@ -1,16 +1,29 @@
|
||||
defmodule Mobilizon.Events.FeedToken do
|
||||
@moduledoc """
|
||||
Represents a Token for a Feed of events
|
||||
Represents a token for a feed of events.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.FeedToken
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
token: Ecto.UUID.t(),
|
||||
actor: Actor.t(),
|
||||
user: User.t()
|
||||
}
|
||||
|
||||
@required_attrs [:token, :user_id]
|
||||
@optional_attrs [:actor_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@primary_key false
|
||||
schema "feed_tokens" do
|
||||
field(:token, Ecto.UUID, primary_key: true)
|
||||
|
||||
belongs_to(:actor, Actor)
|
||||
belongs_to(:user, User)
|
||||
|
||||
@ -18,9 +31,10 @@ defmodule Mobilizon.Events.FeedToken do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%FeedToken{} = feed_token, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = feed_token, attrs) do
|
||||
feed_token
|
||||
|> Ecto.Changeset.cast(attrs, [:token, :actor_id, :user_id])
|
||||
|> validate_required([:token, :user_id])
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
|
@ -1,78 +1,87 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Events.ParticipantRoleEnum, :participant_role_type, [
|
||||
:not_approved,
|
||||
:participant,
|
||||
:moderator,
|
||||
:administrator,
|
||||
:creator
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Events.Participant do
|
||||
@moduledoc """
|
||||
Represents a participant, an actor participating to an event
|
||||
Represents a participant, an actor participating to an event.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.{Participant, Event}
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, ParticipantRole}
|
||||
|
||||
alias MobilizonWeb.Endpoint
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
role: ParticipantRole.t(),
|
||||
url: String.t(),
|
||||
event: Event.t(),
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
@required_attrs [:url, :role, :event_id, :actor_id]
|
||||
@attrs @required_attrs
|
||||
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
schema "participants" do
|
||||
field(:role, Mobilizon.Events.ParticipantRoleEnum, default: :participant)
|
||||
field(:role, ParticipantRole, default: :participant)
|
||||
field(:url, :string)
|
||||
|
||||
belongs_to(:event, Event, primary_key: true)
|
||||
belongs_to(:actor, Actor, primary_key: true)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Participant{} = participant, attrs) do
|
||||
participant
|
||||
|> Ecto.Changeset.cast(attrs, [:url, :role, :event_id, :actor_id])
|
||||
|> generate_url()
|
||||
|> validate_required([:url, :role, :event_id, :actor_id])
|
||||
end
|
||||
|
||||
# If there's a blank URL that's because we're doing the first insert
|
||||
defp generate_url(%Ecto.Changeset{data: %Participant{url: nil}} = changeset) do
|
||||
case fetch_change(changeset, :url) do
|
||||
{:ok, _url} -> changeset
|
||||
:error -> do_generate_url(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
# Most time just go with the given URL
|
||||
defp generate_url(%Ecto.Changeset{} = changeset), do: changeset
|
||||
|
||||
defp do_generate_url(%Ecto.Changeset{} = changeset) do
|
||||
uuid = Ecto.UUID.generate()
|
||||
|
||||
changeset
|
||||
|> put_change(
|
||||
:url,
|
||||
"#{MobilizonWeb.Endpoint.url()}/join/event/#{uuid}"
|
||||
)
|
||||
|> put_change(
|
||||
:id,
|
||||
uuid
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
We check that the actor asking to leave the event is not it's only organizer
|
||||
We check that the actor asking to leave the event is not it's only organizer.
|
||||
We start by fetching the list of organizers and if there's only one of them
|
||||
and that it's the actor requesting leaving the event we return true
|
||||
and that it's the actor requesting leaving the event we return true.
|
||||
"""
|
||||
@spec check_that_participant_is_not_only_organizer(integer(), integer()) :: boolean()
|
||||
def check_that_participant_is_not_only_organizer(event_id, actor_id) do
|
||||
case Mobilizon.Events.list_organizers_participants_for_event(event_id) do
|
||||
[%Participant{actor: %Actor{id: participant_actor_id}}] ->
|
||||
@spec is_not_only_organizer(integer | String.t(), integer | String.t()) :: boolean
|
||||
def is_not_only_organizer(event_id, actor_id) do
|
||||
case Events.list_organizers_participants_for_event(event_id) do
|
||||
[%__MODULE__{actor: %Actor{id: participant_actor_id}}] ->
|
||||
participant_actor_id == actor_id
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = participant, attrs) do
|
||||
participant
|
||||
|> cast(attrs, @attrs)
|
||||
|> ensure_url()
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
# If there's a blank URL that's because we're doing the first insert
|
||||
@spec ensure_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp ensure_url(%Ecto.Changeset{data: %__MODULE__{url: nil}} = changeset) do
|
||||
case fetch_change(changeset, :url) do
|
||||
{:ok, _url} ->
|
||||
changeset
|
||||
|
||||
:error ->
|
||||
update_url(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_url(%Ecto.Changeset{} = changeset), do: changeset
|
||||
|
||||
defp update_url(%Ecto.Changeset{} = changeset) do
|
||||
uuid = Ecto.UUID.generate()
|
||||
url = generate_url(uuid)
|
||||
|
||||
changeset
|
||||
|> put_change(:id, uuid)
|
||||
|> put_change(:url, url)
|
||||
end
|
||||
|
||||
@spec generate_url(String.t()) :: String.t()
|
||||
defp generate_url(uuid), do: "#{Endpoint.url()}/join/event/#{uuid}"
|
||||
end
|
||||
|
@ -1,10 +1,41 @@
|
||||
defmodule Mobilizon.Events.Session do
|
||||
@moduledoc """
|
||||
Represents a session for an event (such as a talk at a conference)
|
||||
Represents a session for an event (such as a talk at a conference).
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.{Session, Event, Track}
|
||||
|
||||
alias Mobilizon.Events.{Event, Track}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
audios_urls: String.t(),
|
||||
language: String.t(),
|
||||
long_abstract: String.t(),
|
||||
short_abstract: String.t(),
|
||||
slides_url: String.t(),
|
||||
subtitle: String.t(),
|
||||
title: String.t(),
|
||||
videos_urls: String.t(),
|
||||
begins_on: DateTime.t(),
|
||||
ends_on: DateTime.t(),
|
||||
event: Event.t(),
|
||||
track: Track.t()
|
||||
}
|
||||
|
||||
@required_attrs [
|
||||
:title,
|
||||
:subtitle,
|
||||
:short_abstract,
|
||||
:long_abstract,
|
||||
:language,
|
||||
:slides_url,
|
||||
:videos_urls,
|
||||
:audios_urls
|
||||
]
|
||||
@optional_attrs [:event_id, :track_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "sessions" do
|
||||
field(:audios_urls, :string)
|
||||
@ -17,6 +48,7 @@ defmodule Mobilizon.Events.Session do
|
||||
field(:videos_urls, :string)
|
||||
field(:begins_on, :utc_datetime)
|
||||
field(:ends_on, :utc_datetime)
|
||||
|
||||
belongs_to(:event, Event)
|
||||
belongs_to(:track, Track)
|
||||
|
||||
@ -24,29 +56,10 @@ defmodule Mobilizon.Events.Session do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Session{} = session, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = session, attrs) do
|
||||
session
|
||||
|> cast(attrs, [
|
||||
:title,
|
||||
:subtitle,
|
||||
:short_abstract,
|
||||
:long_abstract,
|
||||
:language,
|
||||
:slides_url,
|
||||
:videos_urls,
|
||||
:audios_urls,
|
||||
:event_id,
|
||||
:track_id
|
||||
])
|
||||
|> validate_required([
|
||||
:title,
|
||||
:subtitle,
|
||||
:short_abstract,
|
||||
:long_abstract,
|
||||
:language,
|
||||
:slides_url,
|
||||
:videos_urls,
|
||||
:audios_urls
|
||||
])
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
|
@ -1,60 +1,40 @@
|
||||
defmodule Mobilizon.Events.Tag.TitleSlug do
|
||||
@moduledoc """
|
||||
Generates slugs for tags
|
||||
"""
|
||||
alias Mobilizon.Events.Tag
|
||||
import Ecto.Query
|
||||
alias Mobilizon.Repo
|
||||
use EctoAutoslugField.Slug, from: :title, to: :slug
|
||||
|
||||
def build_slug(sources, changeset) do
|
||||
slug = super(sources, changeset)
|
||||
build_unique_slug(slug, changeset)
|
||||
end
|
||||
|
||||
defp build_unique_slug(slug, changeset) do
|
||||
query =
|
||||
from(
|
||||
t in Tag,
|
||||
where: t.slug == ^slug
|
||||
)
|
||||
|
||||
case Repo.one(query) do
|
||||
nil ->
|
||||
slug
|
||||
|
||||
_tag ->
|
||||
slug
|
||||
|> Mobilizon.Ecto.increment_slug()
|
||||
|> build_unique_slug(changeset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Events.Tag do
|
||||
@moduledoc """
|
||||
Represents a tag for events
|
||||
Represents a tag for events.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.Tag
|
||||
alias Mobilizon.Events.Tag.TitleSlug
|
||||
|
||||
alias Mobilizon.Events.TagRelation
|
||||
alias Mobilizon.Events.Tag.TitleSlug
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
title: String.t(),
|
||||
slug: TitleSlug.Type.t(),
|
||||
related_tags: [t]
|
||||
}
|
||||
|
||||
@required_attrs [:title, :slug]
|
||||
@attrs @required_attrs
|
||||
|
||||
schema "tags" do
|
||||
field(:title, :string)
|
||||
field(:slug, TitleSlug.Type)
|
||||
many_to_many(:related_tags, Tag, join_through: TagRelation)
|
||||
|
||||
many_to_many(:related_tags, __MODULE__, join_through: TagRelation)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Tag{} = tag, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = tag, attrs) do
|
||||
tag
|
||||
|> cast(attrs, [:title])
|
||||
|> cast(attrs, @attrs)
|
||||
|> TitleSlug.maybe_generate_slug()
|
||||
|> validate_required([:title, :slug])
|
||||
|> validate_required(@required_attrs)
|
||||
|> TitleSlug.unique_constraint()
|
||||
end
|
||||
end
|
||||
|
53
lib/mobilizon/events/tag/title_slug.ex
Normal file
53
lib/mobilizon/events/tag/title_slug.ex
Normal 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
|
48
lib/mobilizon/events/tag_relation.ex
Normal file
48
lib/mobilizon/events/tag_relation.ex
Normal 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
|
@ -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
|
@ -1,15 +1,31 @@
|
||||
defmodule Mobilizon.Events.Track do
|
||||
@moduledoc """
|
||||
Represents a track for an event (such as a theme) having multiple sessions
|
||||
Represents a track for an event (such as a theme) having multiple sessions.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.{Track, Event, Session}
|
||||
|
||||
alias Mobilizon.Events.{Event, Session}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
color: String.t(),
|
||||
description: String.t(),
|
||||
name: String.t(),
|
||||
event: Event.t(),
|
||||
sessions: [Session.t()]
|
||||
}
|
||||
|
||||
@required_attrs [:name, :description, :color]
|
||||
@optional_attrs [:event_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "tracks" do
|
||||
field(:color, :string)
|
||||
field(:description, :string)
|
||||
field(:name, :string)
|
||||
|
||||
belongs_to(:event, Event)
|
||||
has_many(:sessions, Session)
|
||||
|
||||
@ -17,9 +33,10 @@ defmodule Mobilizon.Events.Track do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%Track{} = track, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = track, attrs) do
|
||||
track
|
||||
|> cast(attrs, [:name, :description, :color, :event_id])
|
||||
|> validate_required([:name, :description, :color])
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
|
@ -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
|
@ -1,9 +1,22 @@
|
||||
defmodule Mobilizon.Media.File do
|
||||
@moduledoc """
|
||||
Represents a file entity
|
||||
Represents a file entity.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
import Ecto.Changeset, only: [cast: 3, validate_required: 2]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
name: String.t(),
|
||||
url: String.t(),
|
||||
content_type: String.t(),
|
||||
size: integer
|
||||
}
|
||||
|
||||
@required_attrs [:name, :url]
|
||||
@optional_attrs [:content_type, :size]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
embedded_schema do
|
||||
field(:name, :string)
|
||||
@ -15,9 +28,10 @@ defmodule Mobilizon.Media.File do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(picture, attrs) do
|
||||
picture
|
||||
|> cast(attrs, [:name, :url, :content_type, :size])
|
||||
|> validate_required([:name, :url])
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = file, attrs) do
|
||||
file
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
|
85
lib/mobilizon/media/media.ex
Normal file
85
lib/mobilizon/media/media.ex
Normal 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
|
@ -1,11 +1,19 @@
|
||||
defmodule Mobilizon.Media.Picture do
|
||||
@moduledoc """
|
||||
Represents a picture entity
|
||||
Represents a picture entity.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Media.File
|
||||
|
||||
import Ecto.Changeset, only: [cast: 3, cast_embed: 2]
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.File
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
file: File.t(),
|
||||
actor: Actor.t()
|
||||
}
|
||||
|
||||
schema "pictures" do
|
||||
embeds_one(:file, File, on_replace: :update)
|
||||
@ -15,7 +23,8 @@ defmodule Mobilizon.Media.Picture do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(picture, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = picture, attrs) do
|
||||
picture
|
||||
|> cast(attrs, [:actor_id])
|
||||
|> cast_embed(:file)
|
||||
|
@ -1,5 +0,0 @@
|
||||
Postgrex.Types.define(
|
||||
Mobilizon.PostgresTypes,
|
||||
[Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(),
|
||||
json: Jason
|
||||
)
|
@ -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
|
@ -1,28 +1,41 @@
|
||||
defmodule Mobilizon.Reports.Note do
|
||||
@moduledoc """
|
||||
Report Note entity
|
||||
Represents a note entity.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
import Ecto.Changeset, only: [cast: 3, validate_required: 2]
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Reports.Report
|
||||
|
||||
@required_attrs [:content, :moderator_id, :report_id]
|
||||
@attrs @required_attrs
|
||||
|
||||
@timestamps_opts [type: :utc_datetime]
|
||||
@attrs [:content, :moderator_id, :report_id]
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
content: String.t(),
|
||||
report: Report.t(),
|
||||
moderator: Actor.t()
|
||||
}
|
||||
|
||||
@derive {Jason.Encoder, only: [:content]}
|
||||
schema "report_notes" do
|
||||
field(:content, :string)
|
||||
belongs_to(:moderator, Actor)
|
||||
|
||||
belongs_to(:report, Report)
|
||||
belongs_to(:moderator, Actor)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(note, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = note, attrs) do
|
||||
note
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
end
|
||||
|
@ -1,45 +1,50 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Reports.ReportStateEnum, :report_state, [
|
||||
:open,
|
||||
:closed,
|
||||
:resolved
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Reports.Report do
|
||||
@moduledoc """
|
||||
Report entity
|
||||
Represents a report entity.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Reports.Note
|
||||
alias Mobilizon.Events.{Comment, Event}
|
||||
alias Mobilizon.Reports.{Note, ReportStatus}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
content: String.t(),
|
||||
status: ReportStatus.t(),
|
||||
uri: String.t(),
|
||||
reported: Actor.t(),
|
||||
reporter: Actor.t(),
|
||||
manager: Actor.t(),
|
||||
event: Event.t(),
|
||||
comments: [Comment.t()],
|
||||
notes: [Note.t()]
|
||||
}
|
||||
|
||||
@required_attrs [:uri, :reported_id, :reporter_id]
|
||||
@optional_attrs [:content, :status, :manager_id, :event_id]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@timestamps_opts [type: :utc_datetime]
|
||||
|
||||
@derive {Jason.Encoder, only: [:status, :uri]}
|
||||
schema "reports" do
|
||||
field(:content, :string)
|
||||
field(:status, Mobilizon.Reports.ReportStateEnum, default: :open)
|
||||
field(:status, ReportStatus, default: :open)
|
||||
field(:uri, :string)
|
||||
|
||||
# The reported actor
|
||||
belongs_to(:reported, Actor)
|
||||
|
||||
# The actor who reported
|
||||
belongs_to(:reporter, Actor)
|
||||
|
||||
# The actor who last acted on this report
|
||||
belongs_to(:manager, Actor)
|
||||
|
||||
# The eventual Event inside the report
|
||||
belongs_to(:event, Event)
|
||||
|
||||
# The eventual Comments inside the report
|
||||
many_to_many(:comments, Comment, join_through: "reports_comments", on_replace: :delete)
|
||||
|
||||
# The notes associated to the report
|
||||
has_many(:notes, Note, foreign_key: :report_id)
|
||||
|
||||
@ -47,13 +52,16 @@ defmodule Mobilizon.Reports.Report do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(report, attrs) do
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = report, attrs) do
|
||||
report
|
||||
|> cast(attrs, [:content, :status, :uri, :reported_id, :reporter_id, :manager_id, :event_id])
|
||||
|> validate_required([:uri, :reported_id, :reporter_id])
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
def creation_changeset(report, attrs) do
|
||||
@doc false
|
||||
@spec creation_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def creation_changeset(%__MODULE__{} = report, attrs) do
|
||||
report
|
||||
|> changeset(attrs)
|
||||
|> put_assoc(:comments, attrs["comments"])
|
||||
|
171
lib/mobilizon/reports/reports.ex
Normal file
171
lib/mobilizon/reports/reports.ex
Normal 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
|
15
lib/mobilizon/storage/ecto.ex
Normal file
15
lib/mobilizon/storage/ecto.ex
Normal 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
|
48
lib/mobilizon/storage/page.ex
Normal file
48
lib/mobilizon/storage/page.ex
Normal 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
|
5
lib/mobilizon/storage/postgrex_types.ex
Normal file
5
lib/mobilizon/storage/postgrex_types.ex
Normal file
@ -0,0 +1,5 @@
|
||||
Postgrex.Types.define(
|
||||
Mobilizon.Storage.PostgresTypes,
|
||||
[Geo.PostGIS.Extension | Ecto.Adapters.Postgres.extensions()],
|
||||
json: Jason
|
||||
)
|
@ -1,14 +1,14 @@
|
||||
defmodule Mobilizon.Repo do
|
||||
defmodule Mobilizon.Storage.Repo do
|
||||
@moduledoc """
|
||||
Mobilizon Repo
|
||||
Mobilizon Repo.
|
||||
"""
|
||||
|
||||
use Ecto.Repo,
|
||||
otp_app: :mobilizon,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
|
||||
@doc """
|
||||
Dynamically loads the repository url from the
|
||||
DATABASE_URL environment variable.
|
||||
Dynamically loads the repository url from the DATABASE_URL environment variable.
|
||||
"""
|
||||
def init(_, opts) do
|
||||
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
|
@ -1,45 +1,35 @@
|
||||
import EctoEnum
|
||||
|
||||
defenum(Mobilizon.Users.UserRoleEnum, :user_role_type, [
|
||||
:administrator,
|
||||
:moderator,
|
||||
:user
|
||||
])
|
||||
|
||||
defmodule Mobilizon.Users.User do
|
||||
@moduledoc """
|
||||
Represents a local user
|
||||
Represents a local user.
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Service.EmailChecker
|
||||
alias Mobilizon.Crypto
|
||||
alias Mobilizon.Events.FeedToken
|
||||
alias Mobilizon.Service.EmailChecker
|
||||
alias Mobilizon.Users.UserRole
|
||||
|
||||
schema "users" do
|
||||
field(:email, :string)
|
||||
field(:password_hash, :string)
|
||||
field(:password, :string, virtual: true)
|
||||
field(:role, Mobilizon.Users.UserRoleEnum, default: :user)
|
||||
has_many(:actors, Actor)
|
||||
belongs_to(:default_actor, Actor)
|
||||
field(:confirmed_at, :utc_datetime)
|
||||
field(:confirmation_sent_at, :utc_datetime)
|
||||
field(:confirmation_token, :string)
|
||||
field(:reset_password_sent_at, :utc_datetime)
|
||||
field(:reset_password_token, :string)
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :user_id)
|
||||
@type t :: %__MODULE__{
|
||||
email: String.t(),
|
||||
password_hash: String.t(),
|
||||
password: String.t(),
|
||||
role: UserRole.t(),
|
||||
confirmed_at: DateTime.t(),
|
||||
confirmation_sent_at: DateTime.t(),
|
||||
confirmation_token: String.t(),
|
||||
reset_password_sent_at: DateTime.t(),
|
||||
reset_password_token: String.t(),
|
||||
default_actor: Actor.t(),
|
||||
actors: [Actor.t()],
|
||||
feed_tokens: [FeedToken.t()]
|
||||
}
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def changeset(%User{} = user, attrs) do
|
||||
changeset =
|
||||
user
|
||||
|> cast(attrs, [
|
||||
:email,
|
||||
@required_attrs [:email]
|
||||
@optional_attrs [
|
||||
:role,
|
||||
:password,
|
||||
:password_hash,
|
||||
@ -48,16 +38,43 @@ defmodule Mobilizon.Users.User do
|
||||
:confirmation_token,
|
||||
:reset_password_sent_at,
|
||||
:reset_password_token
|
||||
])
|
||||
|> validate_required([:email])
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@registration_required_attrs [:email, :password]
|
||||
|
||||
@password_reset_required_attrs [:password, :reset_password_token, :reset_password_sent_at]
|
||||
|
||||
@confirmation_token_length 30
|
||||
|
||||
schema "users" do
|
||||
field(:email, :string)
|
||||
field(:password_hash, :string)
|
||||
field(:password, :string, virtual: true)
|
||||
field(:role, UserRole, default: :user)
|
||||
field(:confirmed_at, :utc_datetime)
|
||||
field(:confirmation_sent_at, :utc_datetime)
|
||||
field(:confirmation_token, :string)
|
||||
field(:reset_password_sent_at, :utc_datetime)
|
||||
field(:reset_password_token, :string)
|
||||
|
||||
belongs_to(:default_actor, Actor)
|
||||
has_many(:actors, Actor)
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :user_id)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = user, attrs) do
|
||||
changeset =
|
||||
user
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> unique_constraint(:email, message: "This email is already used.")
|
||||
|> validate_email()
|
||||
|> validate_length(
|
||||
:password,
|
||||
min: 6,
|
||||
max: 100,
|
||||
message: "The chosen password is too short."
|
||||
)
|
||||
|> validate_length(:password, min: 6, max: 100, message: "The chosen password is too short.")
|
||||
|
||||
if Map.has_key?(attrs, :default_actor) do
|
||||
put_assoc(changeset, :default_actor, attrs.default_actor)
|
||||
@ -66,11 +83,13 @@ defmodule Mobilizon.Users.User do
|
||||
end
|
||||
end
|
||||
|
||||
def registration_changeset(struct, params) do
|
||||
struct
|
||||
|> changeset(params)
|
||||
@doc false
|
||||
@spec registration_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def registration_changeset(%__MODULE__{} = user, attrs) do
|
||||
user
|
||||
|> changeset(attrs)
|
||||
|> cast_assoc(:default_actor)
|
||||
|> validate_required([:email, :password])
|
||||
|> validate_required(@registration_required_attrs)
|
||||
|> hash_password()
|
||||
|> save_confirmation_token()
|
||||
|> unique_constraint(
|
||||
@ -79,16 +98,18 @@ defmodule Mobilizon.Users.User do
|
||||
)
|
||||
end
|
||||
|
||||
def send_password_reset_changeset(%User{} = user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:reset_password_token, :reset_password_sent_at])
|
||||
@doc false
|
||||
@spec send_password_reset_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def send_password_reset_changeset(%__MODULE__{} = user, attrs) do
|
||||
cast(user, attrs, [:reset_password_token, :reset_password_sent_at])
|
||||
end
|
||||
|
||||
def password_reset_changeset(%User{} = user, attrs) do
|
||||
@doc false
|
||||
@spec password_reset_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def password_reset_changeset(%__MODULE__{} = user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:password, :reset_password_token, :reset_password_sent_at])
|
||||
|> validate_length(
|
||||
:password,
|
||||
|> cast(attrs, @password_reset_required_attrs)
|
||||
|> validate_length(:password,
|
||||
min: 6,
|
||||
max: 100,
|
||||
message: "registration.error.password_too_short"
|
||||
@ -96,28 +117,48 @@ defmodule Mobilizon.Users.User do
|
||||
|> hash_password()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks whether an user is confirmed.
|
||||
"""
|
||||
@spec is_confirmed(t) :: boolean
|
||||
def is_confirmed(%__MODULE__{confirmed_at: nil}), do: false
|
||||
def is_confirmed(%__MODULE__{}), do: true
|
||||
|
||||
@doc """
|
||||
Returns whether an user owns an actor.
|
||||
"""
|
||||
@spec owns_actor(t, integer | String.t()) :: {:is_owned, Actor.t() | nil}
|
||||
def owns_actor(%__MODULE__{actors: actors}, actor_id) do
|
||||
user_actor = Enum.find(actors, fn actor -> "#{actor.id}" == "#{actor_id}" end)
|
||||
|
||||
{:is_owned, user_actor}
|
||||
end
|
||||
|
||||
@spec save_confirmation_token(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp save_confirmation_token(changeset) do
|
||||
case changeset do
|
||||
%Ecto.Changeset{valid?: true, changes: %{email: _email}} ->
|
||||
changeset = put_change(changeset, :confirmation_token, random_string(30))
|
||||
now = DateTime.utc_now()
|
||||
|
||||
put_change(
|
||||
changeset,
|
||||
:confirmation_sent_at,
|
||||
DateTime.utc_now() |> DateTime.truncate(:second)
|
||||
)
|
||||
changeset
|
||||
|> put_change(:confirmation_token, Crypto.random_string(@confirmation_token_length))
|
||||
|> put_change(:confirmation_sent_at, DateTime.truncate(now, :second))
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
@spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp validate_email(changeset) do
|
||||
case changeset do
|
||||
%Ecto.Changeset{valid?: true, changes: %{email: email}} ->
|
||||
case EmailChecker.valid?(email) do
|
||||
false -> add_error(changeset, :email, "Email doesn't fit required format")
|
||||
_ -> changeset
|
||||
false ->
|
||||
add_error(changeset, :email, "Email doesn't fit required format")
|
||||
|
||||
true ->
|
||||
changeset
|
||||
end
|
||||
|
||||
_ ->
|
||||
@ -125,46 +166,14 @@ defmodule Mobilizon.Users.User do
|
||||
end
|
||||
end
|
||||
|
||||
defp random_string(length) do
|
||||
length
|
||||
|> :crypto.strong_rand_bytes()
|
||||
|> Base.url_encode64()
|
||||
end
|
||||
|
||||
# Hash password when it's changed
|
||||
@spec hash_password(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp hash_password(changeset) do
|
||||
case changeset do
|
||||
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
|
||||
put_change(
|
||||
changeset,
|
||||
:password_hash,
|
||||
Argon2.hash_pwd_salt(password)
|
||||
)
|
||||
put_change(changeset, :password_hash, Argon2.hash_pwd_salt(password))
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def is_confirmed(%User{confirmed_at: nil} = _user), do: {:error, :unconfirmed}
|
||||
def is_confirmed(%User{} = user), do: {:ok, user}
|
||||
|
||||
@doc """
|
||||
Returns whether an user owns an actor
|
||||
"""
|
||||
@spec owns_actor(struct(), String.t()) :: {:is_owned, false} | {:is_owned, true, Actor.t()}
|
||||
def owns_actor(%User{} = user, actor_id) when is_binary(actor_id) do
|
||||
case Integer.parse(actor_id) do
|
||||
{actor_id, ""} -> owns_actor(user, actor_id)
|
||||
_ -> {:is_owned, false}
|
||||
end
|
||||
end
|
||||
|
||||
@spec owns_actor(struct(), integer()) :: {:is_owned, false} | {:is_owned, true, Actor.t()}
|
||||
def owns_actor(%User{actors: actors}, actor_id) do
|
||||
case Enum.find(actors, fn a -> a.id == actor_id end) do
|
||||
nil -> {:is_owned, false}
|
||||
actor -> {:is_owned, true, actor}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,116 +3,86 @@ defmodule Mobilizon.Users do
|
||||
The Users context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
import Ecto.Query
|
||||
import EctoEnum
|
||||
|
||||
alias Mobilizon.Repo
|
||||
import Mobilizon.Ecto
|
||||
import Mobilizon.Storage.Ecto
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@doc false
|
||||
def data() do
|
||||
Dataloader.Ecto.new(Repo, query: &query/2)
|
||||
end
|
||||
@type tokens :: %{
|
||||
required(:access_token) => String.t(),
|
||||
required(:refresh_token) => String.t()
|
||||
}
|
||||
|
||||
@doc false
|
||||
def query(queryable, _params) do
|
||||
queryable
|
||||
end
|
||||
defenum(UserRole, :user_role, [:administrator, :moderator, :user])
|
||||
|
||||
@doc """
|
||||
Register user
|
||||
Registers an user.
|
||||
"""
|
||||
@spec register(map()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
@spec register(map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def register(%{email: _email, password: _password} = args) do
|
||||
with {:ok, %User{} = user} <-
|
||||
%User{}
|
||||
|> User.registration_changeset(args)
|
||||
|> Mobilizon.Repo.insert() do
|
||||
Mobilizon.Events.create_feed_token(%{"user_id" => user.id})
|
||||
|> Repo.insert() do
|
||||
Events.create_feed_token(%{"user_id" => user.id})
|
||||
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an user by it's email
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_user_by_email("test@test.tld", true)
|
||||
{:ok, %Mobilizon.Users.User{}}
|
||||
|
||||
iex> get_user_by_email("test@notfound.tld", false)
|
||||
{:error, :user_not_found}
|
||||
Gets a single user.
|
||||
Raises `Ecto.NoResultsError` if the user does not exist.
|
||||
"""
|
||||
@spec get_user!(integer | String.t()) :: User.t()
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
@doc """
|
||||
Gets an user by its email.
|
||||
"""
|
||||
@spec get_user_by_email(String.t(), boolean | nil) ::
|
||||
{:ok, User.t()} | {:error, :user_not_found}
|
||||
def get_user_by_email(email, activated \\ nil) do
|
||||
query =
|
||||
case activated do
|
||||
nil ->
|
||||
from(u in User, where: u.email == ^email, preload: :default_actor)
|
||||
|
||||
true ->
|
||||
from(
|
||||
u in User,
|
||||
where: u.email == ^email and not is_nil(u.confirmed_at),
|
||||
preload: :default_actor
|
||||
)
|
||||
|
||||
false ->
|
||||
from(
|
||||
u in User,
|
||||
where: u.email == ^email and is_nil(u.confirmed_at),
|
||||
preload: :default_actor
|
||||
)
|
||||
end
|
||||
query = user_by_email_query(email, activated)
|
||||
|
||||
case Repo.one(query) do
|
||||
nil -> {:error, :user_not_found}
|
||||
user -> {:ok, user}
|
||||
nil ->
|
||||
{:error, :user_not_found}
|
||||
|
||||
user ->
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get an user by it's activation token
|
||||
Get an user by its activation token.
|
||||
"""
|
||||
@spec get_user_by_activation_token(String.t()) :: Actor.t()
|
||||
@spec get_user_by_activation_token(String.t()) :: Actor.t() | nil
|
||||
def get_user_by_activation_token(token) do
|
||||
Repo.one(
|
||||
from(
|
||||
u in User,
|
||||
where: u.confirmation_token == ^token,
|
||||
preload: [:default_actor]
|
||||
)
|
||||
)
|
||||
token
|
||||
|> user_by_activation_token_query()
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get an user by it's reset password token
|
||||
Get an user by its reset password token.
|
||||
"""
|
||||
@spec get_user_by_reset_password_token(String.t()) :: Actor.t()
|
||||
def get_user_by_reset_password_token(token) do
|
||||
Repo.one(
|
||||
from(
|
||||
u in User,
|
||||
where: u.reset_password_token == ^token,
|
||||
preload: [:default_actor]
|
||||
)
|
||||
)
|
||||
token
|
||||
|> user_by_reset_password_token_query()
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a user.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> update_user(User{}, %{password: "coucou"})
|
||||
{:ok, %Mobilizon.Users.User{}}
|
||||
|
||||
iex> update_user(User{}, %{password: nil})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
Updates an user.
|
||||
"""
|
||||
@spec update_user(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_user(%User{} = user, attrs) do
|
||||
with {:ok, %User{} = user} <-
|
||||
user
|
||||
@ -123,65 +93,26 @@ defmodule Mobilizon.Users do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a User.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> delete_user(%User{email: "test@test.tld"})
|
||||
{:ok, %Mobilizon.Users.User{}}
|
||||
|
||||
iex> delete_user(%User{})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
Deletes an user.
|
||||
"""
|
||||
def delete_user(%User{} = user) do
|
||||
Repo.delete(user)
|
||||
end
|
||||
|
||||
# @doc """
|
||||
# Returns an `%Ecto.Changeset{}` for tracking user changes.
|
||||
|
||||
# ## Examples
|
||||
|
||||
# iex> change_user(%Mobilizon.Users.User{})
|
||||
# %Ecto.Changeset{data: %Mobilizon.Users.User{}}
|
||||
|
||||
# """
|
||||
# def change_user(%User{} = user) do
|
||||
# User.changeset(user, %{})
|
||||
# end
|
||||
@spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_user(%User{} = user), do: Repo.delete(user)
|
||||
|
||||
@doc """
|
||||
Gets a single user.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the User does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_user!(123)
|
||||
%Mobilizon.Users.User{}
|
||||
|
||||
iex> get_user!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
Get an user with its actors
|
||||
Raises `Ecto.NoResultsError` if the user does not exist.
|
||||
"""
|
||||
def get_user!(id), do: Repo.get!(User, id)
|
||||
|
||||
@doc """
|
||||
Get an user with it's actors
|
||||
|
||||
Raises `Ecto.NoResultsError` if the User does not exist.
|
||||
"""
|
||||
@spec get_user_with_actors!(integer()) :: User.t()
|
||||
@spec get_user_with_actors!(integer | String.t()) :: User.t()
|
||||
def get_user_with_actors!(id) do
|
||||
user = Repo.get!(User, id)
|
||||
Repo.preload(user, [:actors, :default_actor])
|
||||
id
|
||||
|> get_user!()
|
||||
|> Repo.preload([:actors, :default_actor])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get user with it's actors by ID
|
||||
Get user with its actors.
|
||||
"""
|
||||
@spec get_user_with_actors(integer()) :: User.t()
|
||||
@spec get_user_with_actors(integer()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
def get_user_with_actors(id) do
|
||||
case Repo.get(User, id) do
|
||||
nil ->
|
||||
@ -198,23 +129,24 @@ defmodule Mobilizon.Users do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the associated actor for an user, either the default set one or the first found
|
||||
Gets the associated actor for an user, either the default set one or the first
|
||||
found.
|
||||
"""
|
||||
@spec get_actor_for_user(Mobilizon.Users.User.t()) :: Mobilizon.Actors.Actor.t()
|
||||
def get_actor_for_user(%Mobilizon.Users.User{} = user) do
|
||||
case Repo.one(
|
||||
from(
|
||||
a in Actor,
|
||||
join: u in User,
|
||||
on: u.default_actor_id == a.id,
|
||||
where: u.id == ^user.id
|
||||
)
|
||||
) do
|
||||
@spec get_actor_for_user(User.t()) :: Actor.t() | nil
|
||||
def get_actor_for_user(%User{} = user) do
|
||||
actor =
|
||||
user
|
||||
|> actor_for_user_query()
|
||||
|> Repo.one()
|
||||
|
||||
case actor do
|
||||
nil ->
|
||||
case user
|
||||
|> get_actors_for_user() do
|
||||
[] -> nil
|
||||
actors -> hd(actors)
|
||||
case get_actors_for_user(user) do
|
||||
[] ->
|
||||
nil
|
||||
|
||||
actors ->
|
||||
hd(actors)
|
||||
end
|
||||
|
||||
actor ->
|
||||
@ -222,94 +154,48 @@ defmodule Mobilizon.Users do
|
||||
end
|
||||
end
|
||||
|
||||
def get_actors_for_user(%User{id: user_id}) do
|
||||
Repo.all(from(a in Actor, where: a.user_id == ^user_id))
|
||||
@doc """
|
||||
Gets actors for an user.
|
||||
"""
|
||||
@spec get_actors_for_user(User.t()) :: [Actor.t()]
|
||||
def get_actors_for_user(%User{} = user) do
|
||||
user
|
||||
|> actors_for_user_query()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Authenticate user
|
||||
Updates user's default actor.
|
||||
Raises `Ecto.NoResultsError` if the user does not exist.
|
||||
"""
|
||||
def authenticate(%{user: user, password: password}) do
|
||||
# Does password match the one stored in the database?
|
||||
with true <- Argon2.verify_pass(password, user.password_hash),
|
||||
# Yes, create and return the token
|
||||
{:ok, tokens} <- generate_tokens(user) do
|
||||
{:ok, tokens}
|
||||
else
|
||||
_ ->
|
||||
# No, return an error
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate access token and refresh token
|
||||
"""
|
||||
def generate_tokens(user) do
|
||||
with {:ok, access_token} <- generate_access_token(user),
|
||||
{:ok, refresh_token} <- generate_refresh_token(user) do
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}}
|
||||
end
|
||||
end
|
||||
|
||||
defp generate_access_token(user) do
|
||||
with {:ok, access_token, _claims} <-
|
||||
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "access") do
|
||||
{:ok, access_token}
|
||||
end
|
||||
end
|
||||
|
||||
def generate_refresh_token(user) do
|
||||
with {:ok, refresh_token, _claims} <-
|
||||
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
|
||||
{:ok, refresh_token}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_user_default_actor(integer | String.t(), integer | String.t()) :: User.t()
|
||||
def update_user_default_actor(user_id, actor_id) do
|
||||
with _ <-
|
||||
from(
|
||||
u in User,
|
||||
where: u.id == ^user_id,
|
||||
update: [
|
||||
set: [
|
||||
default_actor_id: ^actor_id
|
||||
]
|
||||
]
|
||||
)
|
||||
user_id
|
||||
|> update_user_default_actor_query(actor_id)
|
||||
|> Repo.update_all([]) do
|
||||
Repo.get!(User, user_id)
|
||||
user_id
|
||||
|> get_user!()
|
||||
|> Repo.preload([:default_actor])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of users.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_users()
|
||||
[%Mobilizon.Users.User{}]
|
||||
|
||||
"""
|
||||
@spec list_users(integer | nil, integer | nil, atom | nil, atom | nil) :: [User.t()]
|
||||
def list_users(page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil) do
|
||||
Repo.all(
|
||||
User
|
||||
|> paginate(page, limit)
|
||||
|> Page.paginate(page, limit)
|
||||
|> sort(sort, direction)
|
||||
)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of administrators.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_admins()
|
||||
[%Mobilizon.Users.User{role: :administrator}]
|
||||
|
||||
"""
|
||||
def list_admins() do
|
||||
@spec list_admins :: [User.t()]
|
||||
def list_admins do
|
||||
User
|
||||
|> where([u], u.role == ^:administrator)
|
||||
|> Repo.all()
|
||||
@ -317,25 +203,127 @@ defmodule Mobilizon.Users do
|
||||
|
||||
@doc """
|
||||
Returns the list of moderators.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_moderators()
|
||||
[%Mobilizon.Users.User{role: :moderator}, %Mobilizon.Users.User{role: :administrator}]
|
||||
|
||||
"""
|
||||
def list_moderators() do
|
||||
@spec list_moderators :: [User.t()]
|
||||
def list_moderators do
|
||||
User
|
||||
|> where([u], u.role in ^[:administrator, :moderator])
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def count_users() do
|
||||
Repo.one(
|
||||
@doc """
|
||||
Counts users.
|
||||
"""
|
||||
@spec count_users :: integer
|
||||
def count_users, do: Repo.one(from(u in User, select: count(u.id)))
|
||||
|
||||
@doc """
|
||||
Authenticate an user.
|
||||
"""
|
||||
@spec authenticate(User.t()) :: {:ok, tokens} | {:error, :unauthorized}
|
||||
def authenticate(%{user: %User{password_hash: password_hash} = user, password: password}) do
|
||||
# Does password match the one stored in the database?
|
||||
if Argon2.verify_pass(password, password_hash) do
|
||||
{:ok, _tokens} = generate_tokens(user)
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates access token and refresh token for an user.
|
||||
"""
|
||||
@spec generate_tokens(User.t()) :: {:ok, tokens}
|
||||
def generate_tokens(user) do
|
||||
with {:ok, access_token} <- generate_access_token(user),
|
||||
{:ok, refresh_token} <- generate_refresh_token(user) do
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates access token for an user.
|
||||
"""
|
||||
@spec generate_access_token(User.t()) :: {:ok, String.t()}
|
||||
def generate_access_token(user) do
|
||||
with {:ok, access_token, _claims} <-
|
||||
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "access") do
|
||||
{:ok, access_token}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates refresh token for an user.
|
||||
"""
|
||||
@spec generate_refresh_token(User.t()) :: {:ok, String.t()}
|
||||
def generate_refresh_token(user) do
|
||||
with {:ok, refresh_token, _claims} <-
|
||||
MobilizonWeb.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
|
||||
{:ok, refresh_token}
|
||||
end
|
||||
end
|
||||
|
||||
@spec user_by_email_query(String.t(), boolean | nil) :: Ecto.Query.t()
|
||||
defp user_by_email_query(email, nil) do
|
||||
from(u in User, where: u.email == ^email, preload: :default_actor)
|
||||
end
|
||||
|
||||
defp user_by_email_query(email, true) do
|
||||
from(
|
||||
u in User,
|
||||
select: count(u.id)
|
||||
where: u.email == ^email and not is_nil(u.confirmed_at),
|
||||
preload: :default_actor
|
||||
)
|
||||
end
|
||||
|
||||
defp user_by_email_query(email, false) do
|
||||
from(
|
||||
u in User,
|
||||
where: u.email == ^email and is_nil(u.confirmed_at),
|
||||
preload: :default_actor
|
||||
)
|
||||
end
|
||||
|
||||
@spec user_by_activation_token_query(String.t()) :: Ecto.Query.t()
|
||||
defp user_by_activation_token_query(token) do
|
||||
from(
|
||||
u in User,
|
||||
where: u.confirmation_token == ^token,
|
||||
preload: [:default_actor]
|
||||
)
|
||||
end
|
||||
|
||||
@spec user_by_reset_password_token_query(String.t()) :: Ecto.Query.t()
|
||||
defp user_by_reset_password_token_query(token) do
|
||||
from(
|
||||
u in User,
|
||||
where: u.reset_password_token == ^token,
|
||||
preload: [:default_actor]
|
||||
)
|
||||
end
|
||||
|
||||
@spec actor_for_user_query(User.t()) :: Ecto.Query.t()
|
||||
defp actor_for_user_query(%User{id: user_id}) do
|
||||
from(
|
||||
a in Actor,
|
||||
join: u in User,
|
||||
on: u.default_actor_id == a.id,
|
||||
where: u.id == ^user_id
|
||||
)
|
||||
end
|
||||
|
||||
@spec actors_for_user_query(User.t()) :: Ecto.Query.t()
|
||||
defp actors_for_user_query(%User{id: user_id}) do
|
||||
from(a in Actor, where: a.user_id == ^user_id)
|
||||
end
|
||||
|
||||
@spec update_user_default_actor_query(integer | String.t(), integer | String.t()) ::
|
||||
Ecto.Query.t()
|
||||
defp update_user_default_actor_query(user_id, actor_id) do
|
||||
from(
|
||||
u in User,
|
||||
where: u.id == ^user_id,
|
||||
update: [set: [default_actor_id: ^actor_id]]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -5,6 +5,7 @@ defmodule MobilizonWeb.API.Events do
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
||||
alias Mobilizon.Service.ActivityPub.Activity
|
||||
alias MobilizonWeb.API.Utils
|
||||
|
||||
@doc """
|
||||
|
@ -32,7 +32,7 @@ defmodule MobilizonWeb.API.Follows do
|
||||
|
||||
def accept(%Actor{} = follower, %Actor{} = followed) do
|
||||
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <-
|
||||
Actor.following?(follower, followed),
|
||||
Actors.is_following(follower, followed),
|
||||
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}",
|
||||
data <-
|
||||
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),
|
||||
|
@ -3,6 +3,7 @@ defmodule MobilizonWeb.API.Groups do
|
||||
API for Events
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
|
||||
@ -22,21 +23,13 @@ defmodule MobilizonWeb.API.Groups do
|
||||
banner: _banner
|
||||
} = args
|
||||
) do
|
||||
with {:is_owned, true, actor} <- User.owns_actor(user, creator_actor_id),
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id),
|
||||
title <- String.trim(title),
|
||||
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
|
||||
visibility <- Map.get(args, :visibility, :public),
|
||||
{content_html, tags, to, cc} <-
|
||||
Utils.prepare_content(actor, summary, visibility, [], nil),
|
||||
group <-
|
||||
ActivityPubUtils.make_group_data(
|
||||
actor.url,
|
||||
to,
|
||||
title,
|
||||
content_html,
|
||||
tags,
|
||||
cc
|
||||
) do
|
||||
group <- ActivityPubUtils.make_group_data(actor.url, to, title, content_html, tags, cc) do
|
||||
ActivityPub.create(%{
|
||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
actor: actor,
|
||||
@ -47,7 +40,7 @@ defmodule MobilizonWeb.API.Groups do
|
||||
{:existing_group, _} ->
|
||||
{:error, "A group with this name already exists"}
|
||||
|
||||
{:is_owned, _} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
|
@ -3,17 +3,18 @@ defmodule MobilizonWeb.API.Reports do
|
||||
API for Reports
|
||||
"""
|
||||
|
||||
import MobilizonWeb.API.Utils
|
||||
import Mobilizon.Service.Admin.ActionLogService
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Activity
|
||||
alias Mobilizon.Service.ActivityPub.Activity
|
||||
alias Mobilizon.Reports, as: ReportsAction
|
||||
alias Mobilizon.Reports.{Report, Note}
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
import MobilizonWeb.API.Utils
|
||||
import Mobilizon.Service.Admin.ActionLogService
|
||||
|
||||
@doc """
|
||||
Create a report/flag on an actor, and optionally on an event or on comments.
|
||||
@ -61,7 +62,7 @@ defmodule MobilizonWeb.API.Reports do
|
||||
"""
|
||||
def update_report_status(%Actor{} = actor, %Report{} = report, state) do
|
||||
with {:valid_state, true} <-
|
||||
{:valid_state, Mobilizon.Reports.ReportStateEnum.valid_value?(state)},
|
||||
{:valid_state, Mobilizon.Reports.ReportStatus.valid_value?(state)},
|
||||
{:ok, report} <- ReportsAction.update_report(report, %{"status" => state}),
|
||||
{:ok, _} <- log_action(actor, "update", report) do
|
||||
{:ok, report}
|
||||
@ -72,7 +73,7 @@ defmodule MobilizonWeb.API.Reports do
|
||||
|
||||
defp get_report_comments(%Actor{id: actor_id}, comment_ids) do
|
||||
{:get_report_comments,
|
||||
Events.get_all_comments_by_actor_and_ids(actor_id, comment_ids) |> Enum.map(& &1.url)}
|
||||
Events.list_comments_by_actor_and_ids(actor_id, comment_ids) |> Enum.map(& &1.url)}
|
||||
end
|
||||
|
||||
defp get_report_comments(_, _), do: {:get_report_comments, nil}
|
||||
@ -89,7 +90,7 @@ defmodule MobilizonWeb.API.Reports do
|
||||
with %User{role: role} <- Users.get_user!(user_id),
|
||||
{:role, true} <- {:role, role in [:administrator, :moderator]},
|
||||
{:ok, %Note{} = note} <-
|
||||
Mobilizon.Reports.create_report_note(%{
|
||||
Mobilizon.Reports.create_note(%{
|
||||
"report_id" => report_id,
|
||||
"moderator_id" => moderator_id,
|
||||
"content" => content
|
||||
@ -114,7 +115,7 @@ defmodule MobilizonWeb.API.Reports do
|
||||
%User{role: role} <- Users.get_user!(user_id),
|
||||
{:role, true} <- {:role, role in [:administrator, :moderator]},
|
||||
{:ok, %Note{} = note} <-
|
||||
Mobilizon.Reports.delete_report_note(note),
|
||||
Mobilizon.Reports.delete_note(note),
|
||||
{:ok, _} <- log_action(moderator, "delete", note) do
|
||||
{:ok, note}
|
||||
else
|
||||
|
@ -1,20 +1,21 @@
|
||||
defmodule MobilizonWeb.API.Search do
|
||||
@moduledoc """
|
||||
API for Search
|
||||
API for search.
|
||||
"""
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Actors.ActorType
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Comment}
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Storage.Page
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Search actors
|
||||
Searches actors.
|
||||
"""
|
||||
@spec search_actors(String.t(), integer(), integer(), String.t()) ::
|
||||
{:ok, %{total: integer(), elements: list(Actor.t())}} | {:error, any()}
|
||||
@spec search_actors(String.t(), integer | nil, integer | nil, ActorType.t()) ::
|
||||
{:ok, Page.t()} | {:error, String.t()}
|
||||
def search_actors(search, page \\ 1, limit \\ 10, result_type) do
|
||||
search = String.trim(search)
|
||||
|
||||
@ -22,31 +23,33 @@ defmodule MobilizonWeb.API.Search do
|
||||
search == "" ->
|
||||
{:error, "Search can't be empty"}
|
||||
|
||||
# Some URLs could be domain.tld/@username, so keep this condition above handle_search? function
|
||||
url_search?(search) ->
|
||||
# If this is not an actor, skip
|
||||
# Some URLs could be domain.tld/@username, so keep this condition above
|
||||
# the `is_handle` function
|
||||
is_url(search) ->
|
||||
# skip, if it's not an actor
|
||||
case process_from_url(search) do
|
||||
%{:total => total, :elements => [%Actor{}] = elements} ->
|
||||
{:ok, %{total: total, elements: elements}}
|
||||
%Page{total: _total, elements: _elements} = page ->
|
||||
{:ok, page}
|
||||
|
||||
_ ->
|
||||
{:ok, %{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
handle_search?(search) ->
|
||||
is_handle(search) ->
|
||||
{:ok, process_from_username(search)}
|
||||
|
||||
true ->
|
||||
{:ok,
|
||||
Actors.find_and_count_actors_by_username_or_name(search, [result_type], page, limit)}
|
||||
page = Actors.build_actors_by_username_or_name_page(search, [result_type], page, limit)
|
||||
|
||||
{:ok, page}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Search events
|
||||
"""
|
||||
@spec search_events(String.t(), integer(), integer()) ::
|
||||
{:ok, %{total: integer(), elements: list(Event.t())}} | {:error, any()}
|
||||
@spec search_events(String.t(), integer | nil, integer | nil) ::
|
||||
{:ok, Page.t()} | {:error, String.t()}
|
||||
def search_events(search, page \\ 1, limit \\ 10) do
|
||||
search = String.trim(search)
|
||||
|
||||
@ -54,59 +57,52 @@ defmodule MobilizonWeb.API.Search do
|
||||
search == "" ->
|
||||
{:error, "Search can't be empty"}
|
||||
|
||||
url_search?(search) ->
|
||||
# If this is not an event, skip
|
||||
is_url(search) ->
|
||||
# skip, if it's w not an actor
|
||||
case process_from_url(search) do
|
||||
{total = total, [%Event{} = elements]} ->
|
||||
{:ok, %{total: total, elements: elements}}
|
||||
%Page{total: _total, elements: _elements} = page ->
|
||||
{:ok, page}
|
||||
|
||||
_ ->
|
||||
{:ok, %{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
true ->
|
||||
{:ok, Events.find_and_count_events_by_name(search, page, limit)}
|
||||
{:ok, Events.build_events_by_name(search, page, limit)}
|
||||
end
|
||||
end
|
||||
|
||||
# If the search string is an username
|
||||
@spec process_from_username(String.t()) :: %{total: integer(), elements: [Actor.t()]}
|
||||
@spec process_from_username(String.t()) :: Page.t()
|
||||
defp process_from_username(search) do
|
||||
case ActivityPub.find_or_make_actor_from_nickname(search) do
|
||||
{:ok, actor} ->
|
||||
%{total: 1, elements: [actor]}
|
||||
%Page{total: 1, elements: [actor]}
|
||||
|
||||
{:error, _err} ->
|
||||
Logger.debug(fn -> "Unable to find or make actor '#{search}'" end)
|
||||
%{total: 0, elements: []}
|
||||
|
||||
%Page{total: 0, elements: []}
|
||||
end
|
||||
end
|
||||
|
||||
# If the search string is an URL
|
||||
@spec process_from_url(String.t()) :: %{
|
||||
total: integer(),
|
||||
elements: [Actor.t() | Event.t() | Comment.t()]
|
||||
}
|
||||
@spec process_from_url(String.t()) :: Page.t()
|
||||
defp process_from_url(search) do
|
||||
case ActivityPub.fetch_object_from_url(search) do
|
||||
{:ok, object} ->
|
||||
%{total: 1, elements: [object]}
|
||||
%Page{total: 1, elements: [object]}
|
||||
|
||||
{:error, _err} ->
|
||||
Logger.debug(fn -> "Unable to find or make object from URL '#{search}'" end)
|
||||
%{total: 0, elements: []}
|
||||
|
||||
%Page{total: 0, elements: []}
|
||||
end
|
||||
end
|
||||
|
||||
# Is the search an URL search?
|
||||
@spec url_search?(String.t()) :: boolean
|
||||
defp url_search?(search) do
|
||||
String.starts_with?(search, "https://") or String.starts_with?(search, "http://")
|
||||
end
|
||||
@spec is_url(String.t()) :: boolean
|
||||
defp is_url(search), do: String.starts_with?(search, ["http://", "https://"])
|
||||
|
||||
# Is the search an handle search?
|
||||
@spec handle_search?(String.t()) :: boolean
|
||||
defp handle_search?(search) do
|
||||
String.match?(search, ~r/@/)
|
||||
end
|
||||
@spec is_handle(String.t()) :: boolean
|
||||
defp is_handle(search), do: String.match?(search, ~r/@/)
|
||||
end
|
||||
|
@ -2,7 +2,9 @@ defmodule MobilizonWeb.API.Utils do
|
||||
@moduledoc """
|
||||
Utils for API
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Formatter
|
||||
|
||||
@doc """
|
||||
@ -125,7 +127,7 @@ defmodule MobilizonWeb.API.Utils do
|
||||
def make_report_content_text(nil), do: {:ok, nil}
|
||||
|
||||
def make_report_content_text(comment) do
|
||||
max_size = Mobilizon.CommonConfig.get([:instance, :max_report_comment_size], 1000)
|
||||
max_size = Config.get([:instance, :max_report_comment_size], 1000)
|
||||
|
||||
if String.length(comment) <= max_size do
|
||||
{:ok, Formatter.html_escape(comment, "text/plain")}
|
||||
|
24
lib/mobilizon_web/cache.ex
Normal file
24
lib/mobilizon_web/cache.ex
Normal 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
70
lib/mobilizon_web/cache/activity_pub.ex
vendored
Normal 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
|
@ -5,11 +5,14 @@
|
||||
|
||||
defmodule MobilizonWeb.ActivityPubController do
|
||||
use MobilizonWeb, :controller
|
||||
alias Mobilizon.{Actors, Actors.Actor}
|
||||
alias MobilizonWeb.ActivityPub.ActorView
|
||||
|
||||
alias Mobilizon.{Actors, Actors.Actor, Config}
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.Federator
|
||||
|
||||
alias MobilizonWeb.ActivityPub.ActorView
|
||||
alias MobilizonWeb.Cache
|
||||
|
||||
require Logger
|
||||
|
||||
action_fallback(:errors)
|
||||
@ -17,7 +20,7 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
plug(:relay_active? when action in [:relay])
|
||||
|
||||
def relay_active?(conn, _) do
|
||||
if Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
|
||||
if Config.get([:instance, :allow_relay]) do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
@ -29,7 +32,7 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
|
||||
def following(conn, %{"name" => name, "page" => page}) do
|
||||
with {page, ""} <- Integer.parse(page),
|
||||
%Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
|
||||
%Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ActorView.render("following.json", %{actor: actor, page: page}))
|
||||
@ -37,7 +40,7 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
end
|
||||
|
||||
def following(conn, %{"name" => name}) do
|
||||
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
|
||||
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ActorView.render("following.json", %{actor: actor}))
|
||||
@ -46,7 +49,7 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
|
||||
def followers(conn, %{"name" => name, "page" => page}) do
|
||||
with {page, ""} <- Integer.parse(page),
|
||||
%Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
|
||||
%Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ActorView.render("followers.json", %{actor: actor, page: page}))
|
||||
@ -54,7 +57,7 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
end
|
||||
|
||||
def followers(conn, %{"name" => name}) do
|
||||
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_everything(name) do
|
||||
with %Actor{} = actor <- Actors.get_local_actor_by_name_with_preload(name) do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ActorView.render("followers.json", %{actor: actor}))
|
||||
@ -111,13 +114,7 @@ defmodule MobilizonWeb.ActivityPubController do
|
||||
end
|
||||
|
||||
def relay(conn, _params) do
|
||||
with {status, actor} <-
|
||||
Cachex.fetch(
|
||||
:activity_pub,
|
||||
"relay_actor",
|
||||
&Mobilizon.Service.ActivityPub.Relay.get_actor/0
|
||||
),
|
||||
true <- status in [:ok, :commit] do
|
||||
with {:commit, %Actor{} = actor} <- Cache.get_relay() do
|
||||
conn
|
||||
|> put_resp_header("content-type", "application/activity+json")
|
||||
|> json(ActorView.render("actor.json", %{actor: actor}))
|
||||
|
@ -8,60 +8,60 @@ defmodule MobilizonWeb.FeedController do
|
||||
|
||||
def actor(conn, %{"name" => name, "format" => "atom"}) do
|
||||
case Cachex.fetch(:feed, "actor_" <> name) do
|
||||
{status, data} when status in [:ok, :commit] ->
|
||||
{:commit, data} ->
|
||||
conn
|
||||
|> put_resp_content_type("application/atom+xml")
|
||||
|> send_resp(200, data)
|
||||
|
||||
_err ->
|
||||
_ ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(conn, %{"name" => name, "format" => "ics"}) do
|
||||
case Cachex.fetch(:ics, "actor_" <> name) do
|
||||
{status, data} when status in [:ok, :commit] ->
|
||||
{:commit, data} ->
|
||||
conn
|
||||
|> put_resp_content_type("text/calendar")
|
||||
|> send_resp(200, data)
|
||||
|
||||
_err ->
|
||||
_ ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def event(conn, %{"uuid" => uuid, "format" => "ics"}) do
|
||||
case Cachex.fetch(:ics, "event_" <> uuid) do
|
||||
{status, data} when status in [:ok, :commit] ->
|
||||
{:commit, data} ->
|
||||
conn
|
||||
|> put_resp_content_type("text/calendar")
|
||||
|> send_resp(200, data)
|
||||
|
||||
_err ->
|
||||
_ ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def going(conn, %{"token" => token, "format" => "ics"}) do
|
||||
case Cachex.fetch(:ics, "token_" <> token) do
|
||||
{status, data} when status in [:ok, :commit] ->
|
||||
{:commit, data} ->
|
||||
conn
|
||||
|> put_resp_content_type("text/calendar")
|
||||
|> send_resp(200, data)
|
||||
|
||||
_err ->
|
||||
_ ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def going(conn, %{"token" => token, "format" => "atom"}) do
|
||||
case Cachex.fetch(:feed, "token_" <> token) do
|
||||
{status, data} when status in [:ok, :commit] ->
|
||||
{:commit, data} ->
|
||||
conn
|
||||
|> put_resp_content_type("application/atom+xml")
|
||||
|> send_resp(200, data)
|
||||
|
||||
_err ->
|
||||
{:ignore, _} ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
@ -5,13 +5,16 @@
|
||||
|
||||
defmodule MobilizonWeb.MediaProxyController do
|
||||
use MobilizonWeb, :controller
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
alias MobilizonWeb.ReverseProxy
|
||||
alias MobilizonWeb.MediaProxy
|
||||
|
||||
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
|
||||
|
||||
def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
|
||||
with config <- Mobilizon.CommonConfig.get([:media_proxy], []),
|
||||
with config <- Config.get([:media_proxy], []),
|
||||
true <- Keyword.get(config, :enabled, false),
|
||||
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||
:ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do
|
||||
|
@ -6,10 +6,9 @@
|
||||
defmodule MobilizonWeb.NodeInfoController do
|
||||
use MobilizonWeb, :controller
|
||||
|
||||
alias Mobilizon.CommonConfig
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Statistics
|
||||
|
||||
@instance Application.get_env(:mobilizon, :instance)
|
||||
@node_info_supported_versions ["2.0", "2.1"]
|
||||
@node_info_schema_uri "http://nodeinfo.diaspora.software/ns/schema/"
|
||||
|
||||
@ -35,14 +34,14 @@ defmodule MobilizonWeb.NodeInfoController do
|
||||
version: version,
|
||||
software: %{
|
||||
name: "Mobilizon",
|
||||
version: Keyword.get(@instance, :version)
|
||||
version: Config.instance_version()
|
||||
},
|
||||
protocols: ["activitypub"],
|
||||
services: %{
|
||||
inbound: [],
|
||||
outbound: ["atom1.0"]
|
||||
},
|
||||
openRegistrations: CommonConfig.registrations_open?(),
|
||||
openRegistrations: Config.instance_registrations_open?(),
|
||||
usage: %{
|
||||
users: %{
|
||||
total: Statistics.get_cached_value(:local_users)
|
||||
@ -51,14 +50,14 @@ defmodule MobilizonWeb.NodeInfoController do
|
||||
localComments: Statistics.get_cached_value(:local_comments)
|
||||
},
|
||||
metadata: %{
|
||||
nodeName: CommonConfig.instance_name(),
|
||||
nodeDescription: CommonConfig.instance_description()
|
||||
nodeName: Config.instance_name(),
|
||||
nodeDescription: Config.instance_description()
|
||||
}
|
||||
}
|
||||
|
||||
response =
|
||||
if version == "2.1" do
|
||||
put_in(response, [:software, :repository], Keyword.get(@instance, :repository))
|
||||
put_in(response, [:software, :repository], Config.instance_repository())
|
||||
else
|
||||
response
|
||||
end
|
||||
|
@ -3,8 +3,8 @@ defmodule MobilizonWeb.PageController do
|
||||
Controller to load our webapp
|
||||
"""
|
||||
use MobilizonWeb, :controller
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Events
|
||||
|
||||
alias MobilizonWeb.Cache
|
||||
|
||||
plug(:put_layout, false)
|
||||
action_fallback(MobilizonWeb.FallbackController)
|
||||
@ -12,17 +12,17 @@ defmodule MobilizonWeb.PageController do
|
||||
def index(conn, _params), do: render(conn, :index)
|
||||
|
||||
def actor(conn, %{"name" => name}) do
|
||||
{status, actor} = Actors.get_cached_local_actor_by_name(name)
|
||||
{status, actor} = Cache.get_local_actor_by_name(name)
|
||||
render_or_error(conn, &ok_status?/2, status, :actor, actor)
|
||||
end
|
||||
|
||||
def event(conn, %{"uuid" => uuid}) do
|
||||
{status, event} = Events.get_cached_event_full_by_uuid(uuid)
|
||||
{status, event} = Cache.get_public_event_by_uuid_with_preload(uuid)
|
||||
render_or_error(conn, &ok_status_and_is_visible?/2, status, :event, event)
|
||||
end
|
||||
|
||||
def comment(conn, %{"uuid" => uuid}) do
|
||||
{status, comment} = Events.get_cached_comment_full_by_uuid(uuid)
|
||||
{status, comment} = Cache.get_comment_by_uuid_with_preload(uuid)
|
||||
render_or_error(conn, &ok_status_and_is_visible?/2, status, :comment, comment)
|
||||
end
|
||||
|
||||
|
38
lib/mobilizon_web/email/admin.ex
Normal file
38
lib/mobilizon_web/email/admin.ex
Normal 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
|
17
lib/mobilizon_web/email/email.ex
Normal file
17
lib/mobilizon_web/email/email.ex
Normal 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
|
@ -1,6 +1,6 @@
|
||||
defmodule Mobilizon.Mailer do
|
||||
defmodule MobilizonWeb.Email.Mailer do
|
||||
@moduledoc """
|
||||
Mailer
|
||||
Mobilizon Mailer.
|
||||
"""
|
||||
use Bamboo.Mailer, otp_app: :mobilizon
|
||||
end
|
64
lib/mobilizon_web/email/user.ex
Normal file
64
lib/mobilizon_web/email/user.ex
Normal 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
|
@ -7,6 +7,9 @@ defmodule MobilizonWeb.MediaProxy do
|
||||
@moduledoc """
|
||||
Handles proxifying media files
|
||||
"""
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
@base64_opts [padding: false]
|
||||
|
||||
def url(nil), do: nil
|
||||
@ -66,7 +69,7 @@ defmodule MobilizonWeb.MediaProxy do
|
||||
|
||||
def build_url(sig_base64, url_base64, filename \\ nil) do
|
||||
[
|
||||
Mobilizon.CommonConfig.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()),
|
||||
Config.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()),
|
||||
"proxy",
|
||||
sig_base64,
|
||||
url_base64,
|
||||
|
@ -3,7 +3,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/mime.ex
|
||||
|
||||
defmodule Mobilizon.MIME do
|
||||
defmodule MobilizonWeb.MIME do
|
||||
@moduledoc """
|
||||
Returns the mime-type of a binary and optionally a normalized file-name.
|
||||
"""
|
@ -8,10 +8,14 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do
|
||||
Serves uploaded media files
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Plug
|
||||
# no slashes
|
||||
@path "media"
|
||||
|
||||
@ -38,7 +42,7 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do
|
||||
conn
|
||||
end
|
||||
|
||||
config = Mobilizon.CommonConfig.get([MobilizonWeb.Upload])
|
||||
config = Config.get([MobilizonWeb.Upload])
|
||||
|
||||
with uploader <- Keyword.fetch!(config, :uploader),
|
||||
proxy_remote = Keyword.get(config, :proxy_remote, false),
|
||||
@ -75,7 +79,7 @@ defmodule MobilizonWeb.Plugs.UploadedMedia do
|
||||
conn
|
||||
|> MobilizonWeb.ReverseProxy.call(
|
||||
url,
|
||||
Mobilizon.CommonConfig.get([Mobilizon.Upload, :proxy_opts], [])
|
||||
Config.get([Mobilizon.Upload, :proxy_opts], [])
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -2,12 +2,14 @@ defmodule MobilizonWeb.Resolvers.Comment do
|
||||
@moduledoc """
|
||||
Handles the comment-related GraphQL calls
|
||||
"""
|
||||
require Logger
|
||||
|
||||
alias Mobilizon.Events.Comment
|
||||
alias Mobilizon.Activity
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Service.ActivityPub.Activity
|
||||
alias MobilizonWeb.API.Comments
|
||||
|
||||
require Logger
|
||||
|
||||
def create_comment(_parent, %{text: comment, actor_username: username}, %{
|
||||
context: %{current_user: %User{} = _user}
|
||||
}) do
|
||||
|
@ -1,19 +1,19 @@
|
||||
defmodule MobilizonWeb.Resolvers.Config do
|
||||
@moduledoc """
|
||||
Handles the config-related GraphQL calls
|
||||
Handles the config-related GraphQL calls.
|
||||
"""
|
||||
|
||||
import Mobilizon.CommonConfig
|
||||
alias Mobilizon.Config
|
||||
|
||||
@doc """
|
||||
Get config
|
||||
Gets config.
|
||||
"""
|
||||
def get_config(_parent, _params, _context) do
|
||||
{:ok,
|
||||
%{
|
||||
name: instance_name(),
|
||||
registrations_open: registrations_open?(),
|
||||
description: instance_description()
|
||||
name: Config.instance_name(),
|
||||
registrations_open: Config.instance_registrations_open?(),
|
||||
description: Config.instance_description()
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
@ -2,16 +2,17 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
@moduledoc """
|
||||
Handles the event-related GraphQL calls
|
||||
"""
|
||||
alias Mobilizon.Activity
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Participant, EventOptions}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias MobilizonWeb.Resolvers.Person
|
||||
alias Mobilizon.Service.ActivityPub.Activity
|
||||
import Mobilizon.Service.Admin.ActionLogService
|
||||
|
||||
# We limit the max number of events that can be retrieved
|
||||
@ -28,7 +29,7 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
end
|
||||
|
||||
def find_event(_parent, %{uuid: uuid}, _resolution) do
|
||||
case Mobilizon.Events.get_event_full_by_uuid(uuid) do
|
||||
case Mobilizon.Events.get_public_event_by_uuid_with_preload(uuid) do
|
||||
nil ->
|
||||
{:error, "Event with UUID #{uuid} not found"}
|
||||
|
||||
@ -69,17 +70,14 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
) do
|
||||
# We get the organizer's next public event
|
||||
events =
|
||||
[Events.get_actor_upcoming_public_event(organizer_actor, uuid)]
|
||||
[Events.get_upcoming_public_event_for_actor(organizer_actor, uuid)]
|
||||
|> Enum.filter(&is_map/1)
|
||||
|
||||
# We find similar events with the same tags
|
||||
# uniq_by : It's possible event_from_same_actor is inside events_from_tags
|
||||
events =
|
||||
(events ++
|
||||
Events.find_similar_events_by_common_tags(
|
||||
tags,
|
||||
@number_of_related_events
|
||||
))
|
||||
events
|
||||
|> Enum.concat(Events.list_events_by_tags(tags, @number_of_related_events))
|
||||
|> uniq_events()
|
||||
|
||||
# TODO: We should use tag_relations to find more appropriate events
|
||||
@ -87,8 +85,10 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
# We've considered all recommended events, so we fetch the latest events
|
||||
events =
|
||||
if @number_of_related_events - length(events) > 0 do
|
||||
(events ++
|
||||
Events.list_events(1, @number_of_related_events, :begins_on, :asc, true, true))
|
||||
events
|
||||
|> Enum.concat(
|
||||
Events.list_events(1, @number_of_related_events, :begins_on, :asc, true, true)
|
||||
)
|
||||
|> uniq_events()
|
||||
else
|
||||
events
|
||||
@ -112,26 +112,23 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
def actor_join_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
{:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Mobilizon.Events.get_event_full(event_id)},
|
||||
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
|
||||
{:error, :participant_not_found} <- Mobilizon.Events.get_participant(event_id, actor_id),
|
||||
{:ok, _activity, participant} <- MobilizonWeb.API.Participations.join(event, actor),
|
||||
participant <-
|
||||
Map.put(participant, :event, event)
|
||||
participant
|
||||
|> Map.put(:event, event)
|
||||
|> Map.put(:actor, Person.proxify_pictures(actor)) do
|
||||
{:ok, participant}
|
||||
else
|
||||
{:has_event, _} ->
|
||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:error, :event_not_found} ->
|
||||
@ -152,32 +149,18 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
def actor_leave_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
{:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Mobilizon.Events.get_event_full(event_id)},
|
||||
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
|
||||
{:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
event: %{
|
||||
id: event_id
|
||||
},
|
||||
actor: %{
|
||||
id: actor_id
|
||||
}
|
||||
}
|
||||
}
|
||||
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
|
||||
else
|
||||
{:has_event, _} ->
|
||||
{:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
|
||||
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:only_organizer, true} ->
|
||||
@ -198,31 +181,19 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
def create_event(
|
||||
_parent,
|
||||
%{organizer_actor_id: organizer_actor_id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
} = _resolution
|
||||
%{context: %{current_user: user}} = _resolution
|
||||
) do
|
||||
# See https://github.com/absinthe-graphql/absinthe/issues/490
|
||||
with args <- Map.put(args, :options, args[:options] || %{}),
|
||||
{:is_owned, true, organizer_actor} <- User.owns_actor(user, organizer_actor_id),
|
||||
{:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
|
||||
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
|
||||
{:ok, args_with_organizer} <- save_attached_picture(args_with_organizer),
|
||||
{:ok, args_with_organizer} <- save_physical_address(args_with_organizer),
|
||||
{
|
||||
:ok,
|
||||
%Activity{
|
||||
data: %{
|
||||
"object" => %{"type" => "Event"} = _object
|
||||
}
|
||||
},
|
||||
%Event{} = event
|
||||
} <-
|
||||
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
|
||||
MobilizonWeb.API.Events.create_event(args_with_organizer) do
|
||||
{:ok, event}
|
||||
else
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Organizer actor id is not owned by the user"}
|
||||
end
|
||||
end
|
||||
@ -237,35 +208,24 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
def update_event(
|
||||
_parent,
|
||||
%{event_id: event_id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
} = _resolution
|
||||
%{context: %{current_user: user}} = _resolution
|
||||
) do
|
||||
# See https://github.com/absinthe-graphql/absinthe/issues/490
|
||||
with args <- Map.put(args, :options, args[:options] || %{}),
|
||||
{:ok, %Event{} = event} <- Mobilizon.Events.get_event_full(event_id),
|
||||
{:is_owned, true, organizer_actor} <- User.owns_actor(user, event.organizer_actor_id),
|
||||
{:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
|
||||
{:is_owned, %Actor{} = organizer_actor} <-
|
||||
User.owns_actor(user, event.organizer_actor_id),
|
||||
args <- Map.put(args, :organizer_actor, organizer_actor),
|
||||
{:ok, args} <- save_attached_picture(args),
|
||||
{:ok, args} <- save_physical_address(args),
|
||||
{
|
||||
:ok,
|
||||
%Activity{
|
||||
data: %{
|
||||
"object" => %{"type" => "Event"} = _object
|
||||
}
|
||||
},
|
||||
%Event{} = event
|
||||
} <-
|
||||
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
|
||||
MobilizonWeb.API.Events.update_event(args, event) do
|
||||
{:ok, event}
|
||||
else
|
||||
{:error, :event_not_found} ->
|
||||
{:error, "Event not found"}
|
||||
|
||||
{:is_owned, _} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "User doesn't own actor"}
|
||||
end
|
||||
end
|
||||
@ -279,24 +239,14 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
# However, we need to pass it's actor ID
|
||||
@spec save_attached_picture(map()) :: {:ok, map()}
|
||||
defp save_attached_picture(
|
||||
%{
|
||||
picture: %{
|
||||
picture: %{file: %Plug.Upload{} = _picture} = all_pic
|
||||
}
|
||||
} = args
|
||||
%{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args
|
||||
) do
|
||||
{:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor.id))}
|
||||
end
|
||||
|
||||
# Otherwise if we use a previously uploaded picture we need to fetch it from database
|
||||
@spec save_attached_picture(map()) :: {:ok, map()}
|
||||
defp save_attached_picture(
|
||||
%{
|
||||
picture: %{
|
||||
picture_id: picture_id
|
||||
}
|
||||
} = args
|
||||
) do
|
||||
defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do
|
||||
with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
|
||||
{:ok, Map.put(args, :picture, picture)}
|
||||
end
|
||||
@ -306,13 +256,7 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
defp save_attached_picture(args), do: {:ok, args}
|
||||
|
||||
@spec save_physical_address(map()) :: {:ok, map()}
|
||||
defp save_physical_address(
|
||||
%{
|
||||
physical_address: %{
|
||||
url: physical_address_url
|
||||
}
|
||||
} = args
|
||||
)
|
||||
defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args)
|
||||
when not is_nil(physical_address_url) do
|
||||
with %Address{} = address <- Addresses.get_address_by_url(physical_address_url),
|
||||
args <- Map.put(args, :physical_address, address.url) do
|
||||
@ -337,23 +281,20 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
def delete_event(
|
||||
_parent,
|
||||
%{event_id: event_id, actor_id: actor_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{role: role} = user
|
||||
}
|
||||
}
|
||||
%{context: %{current_user: %User{role: role} = user}}
|
||||
) do
|
||||
with {:ok, %Event{local: is_local} = event} <- Mobilizon.Events.get_event_full(event_id),
|
||||
with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id),
|
||||
{actor_id, ""} <- Integer.parse(actor_id),
|
||||
{:is_owned, true, _} <- User.owns_actor(user, actor_id) do
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id) do
|
||||
cond do
|
||||
Event.can_event_be_managed_by(event, actor_id) == {:event_can_be_managed, true} ->
|
||||
{:event_can_be_managed, true} == Event.can_be_managed_by(event, actor_id) ->
|
||||
do_delete_event(event)
|
||||
|
||||
role in [:moderator, :administrator] ->
|
||||
with {:ok, res} <- do_delete_event(event, !is_local),
|
||||
%Actor{} = actor <- Actors.get_actor(actor_id) do
|
||||
log_action(actor, "delete", event)
|
||||
|
||||
{:ok, res}
|
||||
end
|
||||
|
||||
@ -364,7 +305,7 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
{:error, :event_not_found} ->
|
||||
{:error, "Event not found"}
|
||||
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
|
@ -2,10 +2,11 @@ defmodule MobilizonWeb.Resolvers.FeedToken do
|
||||
@moduledoc """
|
||||
Handles the feed tokens-related GraphQL calls
|
||||
"""
|
||||
require Logger
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.FeedToken
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Create an feed token for an user and a defined actor
|
||||
@ -14,11 +15,11 @@ defmodule MobilizonWeb.Resolvers.FeedToken do
|
||||
def create_feed_token(_parent, %{actor_id: actor_id}, %{
|
||||
context: %{current_user: %User{id: id} = user}
|
||||
}) do
|
||||
with {:is_owned, true, _actor} <- User.owns_actor(user, actor_id),
|
||||
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
|
||||
{:ok, feed_token} <- Events.create_feed_token(%{"user_id" => id, "actor_id" => actor_id}) do
|
||||
{:ok, feed_token}
|
||||
else
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
|
@ -4,10 +4,12 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Service.ActivityPub.Activity
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Activity
|
||||
|
||||
alias MobilizonWeb.Resolvers.Person
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
@ -40,19 +42,11 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
def create_group(
|
||||
_parent,
|
||||
args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {
|
||||
:ok,
|
||||
%Activity{
|
||||
data: %{
|
||||
"object" => %{"type" => "Group"} = _object
|
||||
}
|
||||
},
|
||||
%Activity{data: %{"object" => %{"type" => "Group"} = _object}},
|
||||
%Actor{} = group
|
||||
} <-
|
||||
MobilizonWeb.API.Groups.create_group(
|
||||
@ -66,10 +60,7 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
banner: Map.get(args, "banner")
|
||||
}
|
||||
) do
|
||||
{
|
||||
:ok,
|
||||
group
|
||||
}
|
||||
{:ok, group}
|
||||
end
|
||||
end
|
||||
|
||||
@ -83,17 +74,13 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
def delete_group(
|
||||
_parent,
|
||||
%{group_id: group_id, actor_id: actor_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {actor_id, ""} <- Integer.parse(actor_id),
|
||||
{group_id, ""} <- Integer.parse(group_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
{:is_owned, true, _} <- User.owns_actor(user, actor_id),
|
||||
{:ok, %Member{} = member} <- Member.get_member(actor_id, group.id),
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
|
||||
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
|
||||
{:is_admin, true} <- Member.is_administrator(member),
|
||||
group <- Actors.delete_group!(group) do
|
||||
{:ok, %{id: group.id}}
|
||||
@ -101,7 +88,7 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
{:error, :group_not_found} ->
|
||||
{:error, "Group not found"}
|
||||
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:error, :member_not_found} ->
|
||||
@ -122,39 +109,26 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
def join_group(
|
||||
_parent,
|
||||
%{group_id: group_id, actor_id: actor_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {actor_id, ""} <- Integer.parse(actor_id),
|
||||
{group_id, ""} <- Integer.parse(group_id),
|
||||
{:is_owned, true, actor} <- User.owns_actor(user, actor_id),
|
||||
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
{:error, :member_not_found} <- Member.get_member(actor.id, group.id),
|
||||
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
|
||||
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
|
||||
role <- Mobilizon.Actors.get_default_member_role(group),
|
||||
{:ok, _} <-
|
||||
Actors.create_member(%{
|
||||
parent_id: group.id,
|
||||
actor_id: actor.id,
|
||||
role: role
|
||||
}) do
|
||||
role <- Member.get_default_member_role(group),
|
||||
{:ok, _} <- Actors.create_member(%{parent_id: group.id, actor_id: actor.id, role: role}) do
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
parent:
|
||||
group
|
||||
|> Person.proxify_pictures(),
|
||||
actor:
|
||||
actor
|
||||
|> Person.proxify_pictures(),
|
||||
parent: Person.proxify_pictures(group),
|
||||
actor: Person.proxify_pictures(actor),
|
||||
role: role
|
||||
}
|
||||
}
|
||||
else
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:error, :group_not_found} ->
|
||||
@ -178,33 +152,19 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
def leave_group(
|
||||
_parent,
|
||||
%{group_id: group_id, actor_id: actor_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {actor_id, ""} <- Integer.parse(actor_id),
|
||||
{group_id, ""} <- Integer.parse(group_id),
|
||||
{:is_owned, true, actor} <- User.owns_actor(user, actor_id),
|
||||
{:ok, %Member{} = member} <- Member.get_member(actor.id, group_id),
|
||||
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
{:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id),
|
||||
{:only_administrator, false} <-
|
||||
{:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)},
|
||||
{:ok, _} <-
|
||||
Mobilizon.Actors.delete_member(member) do
|
||||
{
|
||||
:ok,
|
||||
%{
|
||||
parent: %{
|
||||
id: group_id
|
||||
},
|
||||
actor: %{
|
||||
id: actor_id
|
||||
}
|
||||
}
|
||||
}
|
||||
{:ok, %{parent: %{id: group_id}, actor: %{id: actor_id}}}
|
||||
else
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
{:error, :member_not_found} ->
|
||||
@ -224,14 +184,8 @@ defmodule MobilizonWeb.Resolvers.Group do
|
||||
# and that it's the actor requesting leaving the group we return true
|
||||
@spec check_that_member_is_not_last_administrator(integer(), integer()) :: boolean()
|
||||
defp check_that_member_is_not_last_administrator(group_id, actor_id) do
|
||||
case Member.list_administrator_members_for_group(group_id) do
|
||||
[
|
||||
%Member{
|
||||
actor: %Actor{
|
||||
id: member_actor_id
|
||||
}
|
||||
}
|
||||
] ->
|
||||
case Actors.list_administrator_members_for_group(group_id) do
|
||||
[%Member{actor: %Actor{id: member_actor_id}}] ->
|
||||
actor_id == member_actor_id
|
||||
|
||||
_ ->
|
||||
|
@ -9,7 +9,7 @@ defmodule MobilizonWeb.Resolvers.Member do
|
||||
Find members for group
|
||||
"""
|
||||
def find_members_for_group(%Actor{} = actor, _args, _resolution) do
|
||||
members = Actors.memberships_for_group(actor)
|
||||
members = Actors.list_members_for_group(actor)
|
||||
{:ok, members}
|
||||
end
|
||||
end
|
||||
|
@ -2,12 +2,13 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
@moduledoc """
|
||||
Handles the person-related GraphQL calls
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@doc """
|
||||
Find a person
|
||||
@ -50,9 +51,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
def create_person(
|
||||
_parent,
|
||||
%{preferred_username: _preferred_username} = args,
|
||||
%{
|
||||
context: %{current_user: user}
|
||||
} = _resolution
|
||||
%{context: %{current_user: user}} = _resolution
|
||||
) do
|
||||
args = Map.put(args, :user_id, user.id)
|
||||
|
||||
@ -75,17 +74,13 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
def update_person(
|
||||
_parent,
|
||||
%{preferred_username: preferred_username} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
} = _resolution
|
||||
%{context: %{current_user: user}} = _resolution
|
||||
) do
|
||||
args = Map.put(args, :user_id, user.id)
|
||||
|
||||
with {:find_actor, %Actor{} = actor} <-
|
||||
{:find_actor, Actors.get_actor_by_name(preferred_username)},
|
||||
{:is_owned, true, _} <- User.owns_actor(user, actor.id),
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
|
||||
args <- save_attached_pictures(args),
|
||||
{:ok, actor} <- Actors.update_actor(actor, args) do
|
||||
{:ok, actor}
|
||||
@ -93,7 +88,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
{:find_actor, nil} ->
|
||||
{:error, "Actor not found"}
|
||||
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
@ -108,15 +103,11 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
def delete_person(
|
||||
_parent,
|
||||
%{preferred_username: preferred_username} = _args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
} = _resolution
|
||||
%{context: %{current_user: user}} = _resolution
|
||||
) do
|
||||
with {:find_actor, %Actor{} = actor} <-
|
||||
{:find_actor, Actors.get_actor_by_name(preferred_username)},
|
||||
{:is_owned, true, _} <- User.owns_actor(user, actor.id),
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
|
||||
{:last_identity, false} <- {:last_identity, last_identity?(user)},
|
||||
{:last_admin, false} <- {:last_admin, last_admin_of_a_group?(actor.id)},
|
||||
{:ok, actor} <- Actors.delete_actor(actor) do
|
||||
@ -131,7 +122,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
{:last_admin, true} ->
|
||||
{:error, "Cannot remove the last administrator of a group"}
|
||||
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
@ -184,14 +175,12 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
@doc """
|
||||
Returns the list of events this person is going to
|
||||
"""
|
||||
def person_going_to_events(%Actor{id: actor_id}, _args, %{
|
||||
context: %{current_user: user}
|
||||
}) do
|
||||
with {:is_owned, true, actor} <- User.owns_actor(user, actor_id),
|
||||
def person_going_to_events(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
events <- Events.list_event_participations_for_actor(actor) do
|
||||
{:ok, events}
|
||||
else
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
@ -199,9 +188,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
@doc """
|
||||
Returns the list of events this person is going to
|
||||
"""
|
||||
def person_going_to_events(_parent, %{}, %{
|
||||
context: %{current_user: user}
|
||||
}) do
|
||||
def person_going_to_events(_parent, %{}, %{context: %{current_user: user}}) do
|
||||
with %Actor{} = actor <- Users.get_actor_for_user(user),
|
||||
events <- Events.list_event_participations_for_actor(actor) do
|
||||
{:ok, events}
|
||||
@ -220,7 +207,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
# We check that the actor is not the last administrator/creator of a group
|
||||
@spec last_admin_of_a_group?(integer()) :: boolean()
|
||||
defp last_admin_of_a_group?(actor_id) do
|
||||
length(Member.list_group_id_where_last_administrator(actor_id)) > 0
|
||||
length(Actors.list_group_ids_where_last_administrator(actor_id)) > 0
|
||||
end
|
||||
|
||||
@spec proxify_avatar(Actor.t()) :: Actor.t()
|
||||
|
@ -2,6 +2,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
|
||||
@moduledoc """
|
||||
Handles the picture-related GraphQL calls
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Users.User
|
||||
@ -10,9 +11,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
|
||||
Get picture for an event's pic
|
||||
"""
|
||||
def picture(%{picture_id: picture_id} = _parent, _args, _resolution) do
|
||||
with {:ok, picture} <- do_fetch_picture(picture_id) do
|
||||
{:ok, picture}
|
||||
end
|
||||
with {:ok, picture} <- do_fetch_picture(picture_id), do: {:ok, picture}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -20,15 +19,9 @@ defmodule MobilizonWeb.Resolvers.Picture do
|
||||
|
||||
See MobilizonWeb.Resolvers.Event.create_event/3
|
||||
"""
|
||||
def picture(%{picture: picture} = _parent, _args, _resolution) do
|
||||
{:ok, picture}
|
||||
end
|
||||
|
||||
def picture(%{picture: picture} = _parent, _args, _resolution), do: {:ok, picture}
|
||||
def picture(_parent, %{id: picture_id}, _resolution), do: do_fetch_picture(picture_id)
|
||||
|
||||
def picture(_parent, _args, _resolution) do
|
||||
{:ok, nil}
|
||||
end
|
||||
def picture(_parent, _args, _resolution), do: {:ok, nil}
|
||||
|
||||
@spec do_fetch_picture(nil) :: {:error, nil}
|
||||
defp do_fetch_picture(nil), do: {:error, nil}
|
||||
@ -36,7 +29,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
|
||||
@spec do_fetch_picture(String.t()) :: {:ok, Picture.t()} | {:error, :not_found}
|
||||
defp do_fetch_picture(picture_id) do
|
||||
case Media.get_picture(picture_id) do
|
||||
%Picture{id: id, file: file} = _pic ->
|
||||
%Picture{id: id, file: file} ->
|
||||
{:ok,
|
||||
%{
|
||||
name: file.name,
|
||||
@ -46,18 +39,18 @@ defmodule MobilizonWeb.Resolvers.Picture do
|
||||
size: file.size
|
||||
}}
|
||||
|
||||
_err ->
|
||||
_error ->
|
||||
{:error, "Picture with ID #{picture_id} was not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec upload_picture(map(), map(), map()) :: {:ok, Picture.t()} | {:error, any()}
|
||||
def upload_picture(_parent, %{file: %Plug.Upload{} = file, actor_id: actor_id} = args, %{
|
||||
context: %{
|
||||
current_user: user
|
||||
}
|
||||
}) do
|
||||
with {:is_owned, true, _actor} <- User.owns_actor(user, actor_id),
|
||||
def upload_picture(
|
||||
_parent,
|
||||
%{file: %Plug.Upload{} = file, actor_id: actor_id} = args,
|
||||
%{context: %{current_user: user}}
|
||||
) do
|
||||
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
|
||||
{:ok, %{"url" => [%{"href" => url, "mediaType" => content_type}], "size" => size}} <-
|
||||
MobilizonWeb.Upload.store(file),
|
||||
args <-
|
||||
@ -76,11 +69,11 @@ defmodule MobilizonWeb.Resolvers.Picture do
|
||||
size: picture.file.size
|
||||
}}
|
||||
else
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
err ->
|
||||
{:error, err}
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -10,9 +10,11 @@ defmodule MobilizonWeb.Resolvers.Report do
|
||||
alias MobilizonWeb.API.Reports, as: ReportsAPI
|
||||
import Mobilizon.Users.Guards
|
||||
|
||||
def list_reports(_parent, %{page: page, limit: limit, status: status}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
def list_reports(
|
||||
_parent,
|
||||
%{page: page, limit: limit, status: status},
|
||||
%{context: %{current_user: %User{role: role}}}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
{:ok, Mobilizon.Reports.list_reports(page, limit, :updated_at, :desc, status)}
|
||||
end
|
||||
@ -21,9 +23,7 @@ defmodule MobilizonWeb.Resolvers.Report do
|
||||
{:error, "You need to be logged-in and a moderator to list reports"}
|
||||
end
|
||||
|
||||
def get_report(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_moderator(role) do
|
||||
case Mobilizon.Reports.get_report(id) do
|
||||
%Report{} = report ->
|
||||
@ -46,14 +46,14 @@ defmodule MobilizonWeb.Resolvers.Report do
|
||||
%{reporter_actor_id: reporter_actor_id} = args,
|
||||
%{context: %{current_user: user}} = _resolution
|
||||
) do
|
||||
with {:is_owned, true, _} <- User.owns_actor(user, reporter_actor_id),
|
||||
with {:is_owned, %Actor{}} <- User.owns_actor(user, reporter_actor_id),
|
||||
{:ok, _, %Report{} = report} <- ReportsAPI.report(args) do
|
||||
{:ok, report}
|
||||
else
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Reporter actor id is not owned by authenticated user"}
|
||||
|
||||
_err ->
|
||||
_error ->
|
||||
{:error, "Error while saving report"}
|
||||
end
|
||||
end
|
||||
@ -68,22 +68,19 @@ defmodule MobilizonWeb.Resolvers.Report do
|
||||
def update_report(
|
||||
_parent,
|
||||
%{report_id: report_id, moderator_id: moderator_id, status: status},
|
||||
%{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
}
|
||||
%{context: %{current_user: %User{role: role} = user}}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
|
||||
%Actor{} = actor <- Actors.get_actor!(moderator_id),
|
||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, moderator_id),
|
||||
%Report{} = report <- Mobilizon.Reports.get_report(report_id),
|
||||
{:ok, %Report{} = report} <-
|
||||
MobilizonWeb.API.Reports.update_report_status(actor, report, status) do
|
||||
{:ok, report}
|
||||
else
|
||||
{:is_owned, false} ->
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
|
||||
_err ->
|
||||
_error ->
|
||||
{:error, "Error while updating report"}
|
||||
end
|
||||
end
|
||||
@ -95,27 +92,27 @@ defmodule MobilizonWeb.Resolvers.Report do
|
||||
def create_report_note(
|
||||
_parent,
|
||||
%{report_id: report_id, moderator_id: moderator_id, content: content},
|
||||
%{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
}
|
||||
%{context: %{current_user: %User{role: role} = user}}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
|
||||
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
|
||||
%Report{} = report <- Reports.get_report(report_id),
|
||||
%Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id),
|
||||
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
|
||||
{:ok, %Note{} = note} <-
|
||||
MobilizonWeb.API.Reports.create_report_note(report, moderator, content) do
|
||||
{:ok, note}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_report_note(_parent, %{note_id: note_id, moderator_id: moderator_id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
})
|
||||
def delete_report_note(
|
||||
_parent,
|
||||
%{note_id: note_id, moderator_id: moderator_id},
|
||||
%{context: %{current_user: %User{role: role} = user}}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
with {:is_owned, true, _} <- User.owns_actor(user, moderator_id),
|
||||
with {:is_owned, %Actor{}} <- User.owns_actor(user, moderator_id),
|
||||
%Note{} = note <- Reports.get_note(note_id),
|
||||
%Actor{} = moderator <- Actors.get_local_actor_with_everything(moderator_id),
|
||||
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
|
||||
{:ok, %Note{} = note} <-
|
||||
MobilizonWeb.API.Reports.delete_report_note(note, moderator) do
|
||||
{:ok, %{id: note.id}}
|
||||
|
@ -33,7 +33,7 @@ defmodule MobilizonWeb.Resolvers.Tag do
|
||||
# """
|
||||
# def get_related_tags(_parent, %{tag_id: tag_id}, _resolution) do
|
||||
# with %Tag{} = tag <- Mobilizon.Events.get_tag!(tag_id),
|
||||
# tags <- Mobilizon.Events.tag_neighbors(tag) do
|
||||
# tags <- Mobilizon.Events.list_tag_neighbors(tag) do
|
||||
# {:ok, tags}
|
||||
# end
|
||||
# end
|
||||
@ -42,7 +42,7 @@ defmodule MobilizonWeb.Resolvers.Tag do
|
||||
Retrieve the list of related tags for a parent tag
|
||||
"""
|
||||
def get_related_tags(%Tag{} = tag, _args, _resolution) do
|
||||
with tags <- Mobilizon.Events.tag_neighbors(tag) do
|
||||
with tags <- Mobilizon.Events.list_tag_neighbors(tag) do
|
||||
{:ok, tags}
|
||||
end
|
||||
end
|
||||
|
@ -2,12 +2,14 @@ defmodule MobilizonWeb.Resolvers.User do
|
||||
@moduledoc """
|
||||
Handles the user-related GraphQL calls
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Config, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.CommonConfig
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.{Actors, Users}
|
||||
alias Mobilizon.Service.Users.{ResetPassword, Activation}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
@ -110,7 +112,8 @@ defmodule MobilizonWeb.Resolvers.User do
|
||||
"""
|
||||
@spec create_user(any(), map(), any()) :: tuple()
|
||||
def create_user(_parent, args, _resolution) do
|
||||
with {:registrations_open, true} <- {:registrations_open, CommonConfig.registrations_open?()},
|
||||
with {:registrations_open, true} <-
|
||||
{:registrations_open, Config.instance_registrations_open?()},
|
||||
{:ok, %User{} = user} <- Users.register(args) do
|
||||
Activation.send_confirmation_email(user)
|
||||
{:ok, user}
|
||||
@ -118,8 +121,8 @@ defmodule MobilizonWeb.Resolvers.User do
|
||||
{:registrations_open, false} ->
|
||||
{:error, "Registrations are not enabled"}
|
||||
|
||||
err ->
|
||||
err
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@ -139,9 +142,9 @@ defmodule MobilizonWeb.Resolvers.User do
|
||||
user: Map.put(user, :default_actor, actor)
|
||||
}}
|
||||
else
|
||||
err ->
|
||||
error ->
|
||||
Logger.info("Unable to validate user with token #{token}")
|
||||
Logger.debug(inspect(err))
|
||||
Logger.debug(inspect(error))
|
||||
{:error, "Unable to validate user"}
|
||||
end
|
||||
end
|
||||
@ -213,7 +216,7 @@ defmodule MobilizonWeb.Resolvers.User do
|
||||
{:user_actor, _} ->
|
||||
{:error, :actor_not_from_user}
|
||||
|
||||
_err ->
|
||||
_error ->
|
||||
{:error, :unable_to_change_default_actor}
|
||||
end
|
||||
end
|
||||
|
@ -260,7 +260,7 @@ defmodule MobilizonWeb.ReverseProxy do
|
||||
headers,
|
||||
"user-agent",
|
||||
0,
|
||||
{"user-agent", Mobilizon.Application.user_agent()}
|
||||
{"user-agent", Mobilizon.user_agent()}
|
||||
)
|
||||
else
|
||||
headers
|
||||
|
@ -7,6 +7,7 @@ defmodule MobilizonWeb.Schema do
|
||||
alias Mobilizon.{Actors, Events, Users, Addresses, Media, Reports}
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Events.{Event, Comment, Participant}
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
import_types(MobilizonWeb.Schema.Custom.UUID)
|
||||
import_types(MobilizonWeb.Schema.Custom.Point)
|
||||
@ -87,14 +88,17 @@ defmodule MobilizonWeb.Schema do
|
||||
end
|
||||
|
||||
def context(ctx) do
|
||||
default_query = fn queryable, _params -> queryable end
|
||||
default_source = Dataloader.Ecto.new(Repo, query: default_query)
|
||||
|
||||
loader =
|
||||
Dataloader.new()
|
||||
|> Dataloader.add_source(Actors, Actors.data())
|
||||
|> Dataloader.add_source(Users, Users.data())
|
||||
|> Dataloader.add_source(Events, Events.data())
|
||||
|> Dataloader.add_source(Addresses, Addresses.data())
|
||||
|> Dataloader.add_source(Media, Media.data())
|
||||
|> Dataloader.add_source(Reports, Reports.data())
|
||||
|> Dataloader.add_source(Actors, default_source)
|
||||
|> Dataloader.add_source(Users, default_source)
|
||||
|> Dataloader.add_source(Events, default_source)
|
||||
|> Dataloader.add_source(Addresses, default_source)
|
||||
|> Dataloader.add_source(Media, default_source)
|
||||
|> Dataloader.add_source(Reports, default_source)
|
||||
|
||||
Map.put(ctx, :loader, loader)
|
||||
end
|
||||
|
@ -31,7 +31,13 @@ defmodule MobilizonWeb.Upload do
|
||||
* `MobilizonWeb.Upload.Filter`
|
||||
|
||||
"""
|
||||
|
||||
alias Ecto.UUID
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
alias MobilizonWeb.MIME
|
||||
|
||||
require Logger
|
||||
|
||||
@type source ::
|
||||
@ -110,33 +116,33 @@ defmodule MobilizonWeb.Upload do
|
||||
{size_limit, activity_type} =
|
||||
case Keyword.get(opts, :type) do
|
||||
:banner ->
|
||||
{Mobilizon.CommonConfig.get!([:instance, :banner_upload_limit]), "Image"}
|
||||
{Config.get!([:instance, :banner_upload_limit]), "Image"}
|
||||
|
||||
:avatar ->
|
||||
{Mobilizon.CommonConfig.get!([:instance, :avatar_upload_limit]), "Image"}
|
||||
{Config.get!([:instance, :avatar_upload_limit]), "Image"}
|
||||
|
||||
_ ->
|
||||
{Mobilizon.CommonConfig.get!([:instance, :upload_limit]), nil}
|
||||
{Config.get!([:instance, :upload_limit]), nil}
|
||||
end
|
||||
|
||||
%{
|
||||
activity_type: Keyword.get(opts, :activity_type, activity_type),
|
||||
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
||||
uploader: Keyword.get(opts, :uploader, Mobilizon.CommonConfig.get([__MODULE__, :uploader])),
|
||||
filters: Keyword.get(opts, :filters, Mobilizon.CommonConfig.get([__MODULE__, :filters])),
|
||||
uploader: Keyword.get(opts, :uploader, Config.get([__MODULE__, :uploader])),
|
||||
filters: Keyword.get(opts, :filters, Config.get([__MODULE__, :filters])),
|
||||
description: Keyword.get(opts, :description),
|
||||
base_url:
|
||||
Keyword.get(
|
||||
opts,
|
||||
:base_url,
|
||||
Mobilizon.CommonConfig.get([__MODULE__, :base_url], MobilizonWeb.Endpoint.url())
|
||||
Config.get([__MODULE__, :base_url], MobilizonWeb.Endpoint.url())
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp prepare_upload(%Plug.Upload{} = file, opts) do
|
||||
with {:ok, size} <- check_file_size(file.path, opts.size_limit),
|
||||
{:ok, content_type, name} <- Mobilizon.MIME.file_mime_type(file.path, file.filename) do
|
||||
{:ok, content_type, name} <- MIME.file_mime_type(file.path, file.filename) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
id: UUID.generate(),
|
||||
@ -173,7 +179,7 @@ defmodule MobilizonWeb.Upload do
|
||||
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
|
||||
path =
|
||||
URI.encode(path, &char_unescaped?/1) <>
|
||||
if Mobilizon.CommonConfig.get([__MODULE__, :link_name], false) do
|
||||
if Config.get([__MODULE__, :link_name], false) do
|
||||
"?name=#{URI.encode(name, &char_unescaped?/1)}"
|
||||
else
|
||||
""
|
||||
|
@ -9,11 +9,14 @@ defmodule MobilizonWeb.Upload.Filter.AnonymizeFilename do
|
||||
|
||||
Should be used after `MobilizonWeb.Upload.Filter.Dedupe`.
|
||||
"""
|
||||
|
||||
@behaviour MobilizonWeb.Upload.Filter
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
def filter(upload) do
|
||||
extension = List.last(String.split(upload.name, "."))
|
||||
name = Mobilizon.CommonConfig.get([__MODULE__, :text], random(extension))
|
||||
name = Config.get([__MODULE__, :text], random(extension))
|
||||
{:ok, %MobilizonWeb.Upload{upload | name: name}}
|
||||
end
|
||||
|
||||
|
@ -7,13 +7,16 @@ defmodule MobilizonWeb.Upload.Filter.Mogrify do
|
||||
@moduledoc """
|
||||
Handle mogrify transformations
|
||||
"""
|
||||
|
||||
@behaviour MobilizonWeb.Upload.Filter
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
|
||||
@type conversions :: conversion() | [conversion()]
|
||||
|
||||
def filter(%MobilizonWeb.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
filters = Mobilizon.CommonConfig.get!([__MODULE__, :args])
|
||||
filters = Config.get!([__MODULE__, :args])
|
||||
|
||||
file
|
||||
|> Mogrify.open()
|
||||
|
@ -7,8 +7,11 @@ defmodule MobilizonWeb.Uploaders.Local do
|
||||
@moduledoc """
|
||||
Local uploader for files
|
||||
"""
|
||||
|
||||
@behaviour MobilizonWeb.Uploaders.Uploader
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
def get_file(_) do
|
||||
{:ok, {:static_dir, upload_path()}}
|
||||
end
|
||||
@ -59,6 +62,6 @@ defmodule MobilizonWeb.Uploaders.Local do
|
||||
end
|
||||
|
||||
def upload_path do
|
||||
Mobilizon.CommonConfig.get!([__MODULE__, :uploads])
|
||||
Config.get!([__MODULE__, :uploads])
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,11 @@
|
||||
defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
use MobilizonWeb, :view
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.ActivityPub.Activity
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.Utils
|
||||
alias Mobilizon.Activity
|
||||
|
||||
@private_visibility_empty_collection %{elements: [], total: 0}
|
||||
|
||||
@ -47,8 +48,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
|
||||
def render("following.json", %{actor: actor, page: page}) do
|
||||
%{total: total, elements: following} =
|
||||
if Actor.public_visibility?(actor),
|
||||
do: Actor.get_followings(actor, page),
|
||||
if Actor.is_public_visibility(actor),
|
||||
do: Actors.build_followings_for_actor(actor, page),
|
||||
else: @private_visibility_empty_collection
|
||||
|
||||
following
|
||||
@ -58,8 +59,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
|
||||
def render("following.json", %{actor: actor}) do
|
||||
%{total: total, elements: following} =
|
||||
if Actor.public_visibility?(actor),
|
||||
do: Actor.get_followings(actor),
|
||||
if Actor.is_public_visibility(actor),
|
||||
do: Actors.build_followings_for_actor(actor),
|
||||
else: @private_visibility_empty_collection
|
||||
|
||||
%{
|
||||
@ -73,8 +74,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
|
||||
def render("followers.json", %{actor: actor, page: page}) do
|
||||
%{total: total, elements: followers} =
|
||||
if Actor.public_visibility?(actor),
|
||||
do: Actor.get_followers(actor, page),
|
||||
if Actor.is_public_visibility(actor),
|
||||
do: Actors.build_followers_for_actor(actor, page),
|
||||
else: @private_visibility_empty_collection
|
||||
|
||||
followers
|
||||
@ -84,8 +85,8 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
|
||||
def render("followers.json", %{actor: actor}) do
|
||||
%{total: total, elements: followers} =
|
||||
if Actor.public_visibility?(actor),
|
||||
do: Actor.get_followers(actor),
|
||||
if Actor.is_public_visibility(actor),
|
||||
do: Actors.build_followers_for_actor(actor),
|
||||
else: @private_visibility_empty_collection
|
||||
|
||||
%{
|
||||
@ -99,7 +100,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
|
||||
def render("outbox.json", %{actor: actor, page: page}) do
|
||||
%{total: total, elements: followers} =
|
||||
if Actor.public_visibility?(actor),
|
||||
if Actor.is_public_visibility(actor),
|
||||
do: ActivityPub.fetch_public_activities_for_actor(actor, page),
|
||||
else: @private_visibility_empty_collection
|
||||
|
||||
@ -110,7 +111,7 @@ defmodule MobilizonWeb.ActivityPub.ActorView do
|
||||
|
||||
def render("outbox.json", %{actor: actor}) do
|
||||
%{total: total, elements: followers} =
|
||||
if Actor.public_visibility?(actor),
|
||||
if Actor.is_public_visibility(actor),
|
||||
do: ActivityPub.fetch_public_activities_for_actor(actor),
|
||||
else: @private_visibility_empty_collection
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
defmodule MobilizonWeb.ActivityPub.ObjectView do
|
||||
use MobilizonWeb, :view
|
||||
alias Mobilizon.Service.ActivityPub.Utils
|
||||
alias Mobilizon.Activity
|
||||
|
||||
alias Mobilizon.Service.ActivityPub.{Activity, Utils}
|
||||
|
||||
def render("activity.json", %{activity: %Activity{local: local, data: data} = activity}) do
|
||||
%{
|
||||
|
@ -1,3 +1,3 @@
|
||||
defmodule Mobilizon.EmailView do
|
||||
defmodule MobilizonWeb.EmailView do
|
||||
use MobilizonWeb, :view
|
||||
end
|
||||
|
21
lib/service/activity_pub/activity.ex
Normal file
21
lib/service/activity_pub/activity.ex
Normal 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
|
@ -10,11 +10,11 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
Every ActivityPub method
|
||||
"""
|
||||
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Comment, Participant}
|
||||
alias Mobilizon.Service.ActivityPub.Transmogrifier
|
||||
alias Mobilizon.Service.WebFinger
|
||||
alias Mobilizon.Activity
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
@ -22,11 +22,10 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
alias Mobilizon.Service.Federator
|
||||
alias Mobilizon.Service.HTTPSignatures.Signature
|
||||
|
||||
alias Mobilizon.Service.ActivityPub.Convertible
|
||||
alias Mobilizon.Service.ActivityPub.{Activity, Convertible}
|
||||
|
||||
require Logger
|
||||
import Mobilizon.Service.ActivityPub.Utils
|
||||
import Mobilizon.Service.ActivityPub.Visibility
|
||||
import Mobilizon.Service.ActivityPub.{Utils, Visibility}
|
||||
|
||||
@doc """
|
||||
Get recipients for an activity or object
|
||||
@ -84,10 +83,10 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
|
||||
case data["type"] do
|
||||
"Event" ->
|
||||
{:ok, Events.get_event_full_by_url!(object_url)}
|
||||
{:ok, Events.get_public_event_by_url_with_preload!(object_url)}
|
||||
|
||||
"Note" ->
|
||||
{:ok, Events.get_comment_full_from_url!(object_url)}
|
||||
{:ok, Events.get_comment_from_url_with_preload!(object_url)}
|
||||
|
||||
"Actor" ->
|
||||
{:ok, Actors.get_actor_by_url!(object_url, true)}
|
||||
@ -97,10 +96,10 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
end
|
||||
else
|
||||
{:existing_event, %Event{url: event_url}} ->
|
||||
{:ok, Events.get_event_full_by_url!(event_url)}
|
||||
{:ok, Events.get_public_event_by_url_with_preload!(event_url)}
|
||||
|
||||
{:existing_comment, %Comment{url: comment_url}} ->
|
||||
{:ok, Events.get_comment_full_from_url!(comment_url)}
|
||||
{:ok, Events.get_comment_from_url_with_preload!(comment_url)}
|
||||
|
||||
{:existing_actor, {:ok, %Actor{url: actor_url}}} ->
|
||||
{:ok, Actors.get_actor_by_url!(actor_url, true)}
|
||||
@ -112,6 +111,28 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Getting an actor from url, eventually creating it
|
||||
"""
|
||||
@spec get_or_fetch_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
|
||||
def get_or_fetch_by_url(url, preload \\ false) do
|
||||
case Actors.get_actor_by_url(url, preload) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
_ ->
|
||||
case make_actor_from_url(url, preload) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
_ ->
|
||||
Logger.warn("Could not fetch by AP id")
|
||||
|
||||
{:error, "Could not fetch by AP id"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an activity of type "Create"
|
||||
"""
|
||||
@ -278,7 +299,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"""
|
||||
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, %Follower{url: follow_url}} <-
|
||||
Actor.follow(followed, follower, activity_id, false),
|
||||
Actors.follow(followed, follower, activity_id, false),
|
||||
activity_follow_id <-
|
||||
activity_id || follow_url,
|
||||
data <- make_follow_data(followed, follower, activity_follow_id),
|
||||
@ -297,7 +318,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"""
|
||||
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
|
||||
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, %Follower{id: follow_id}} <- Actor.unfollow(followed, follower),
|
||||
with {:ok, %Follower{id: follow_id}} <- Actors.unfollow(followed, follower),
|
||||
# We recreate the follow activity
|
||||
data <-
|
||||
make_follow_data(
|
||||
@ -437,8 +458,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
local
|
||||
) do
|
||||
with {:only_organizer, false} <-
|
||||
{:only_organizer,
|
||||
Participant.check_that_participant_is_not_only_organizer(event_id, actor_id)},
|
||||
{:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.get_participant(event_id, actor_id),
|
||||
{:ok, %Participant{} = participant} <- Mobilizon.Events.delete_participant(participant),
|
||||
@ -464,7 +484,7 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
def make_actor_from_url(url, preload \\ false) do
|
||||
case fetch_and_prepare_actor_from_url(url) do
|
||||
{:ok, data} ->
|
||||
Actors.insert_or_update_actor(data, preload)
|
||||
Actors.upsert_actor(data, preload)
|
||||
|
||||
# Request returned 410
|
||||
{:error, :actor_deleted} ->
|
||||
@ -520,15 +540,14 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
|
||||
public = is_public?(activity)
|
||||
|
||||
if public && is_delete_activity?(activity) == false &&
|
||||
Mobilizon.CommonConfig.get([:instance, :allow_relay]) do
|
||||
if public && !is_delete_activity?(activity) && Config.get([:instance, :allow_relay]) do
|
||||
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
|
||||
Mobilizon.Service.ActivityPub.Relay.publish(activity)
|
||||
end
|
||||
|
||||
followers =
|
||||
if actor.followers_url in activity.recipients do
|
||||
Actor.get_full_external_followers(actor)
|
||||
Actors.list_external_followers_for_actor(actor)
|
||||
else
|
||||
[]
|
||||
end
|
||||
@ -664,8 +683,8 @@ defmodule Mobilizon.Service.ActivityPub do
|
||||
"""
|
||||
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map()
|
||||
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do
|
||||
{:ok, events, total_events} = Events.get_public_events_for_actor(actor, page, limit)
|
||||
{:ok, comments, total_comments} = Events.get_public_comments_for_actor(actor, page, limit)
|
||||
{:ok, events, total_events} = Events.list_public_events_for_actor(actor, page, limit)
|
||||
{:ok, comments, total_comments} = Events.list_public_comments_for_actor(actor, page, limit)
|
||||
|
||||
event_activities = Enum.map(events, &event_to_activity/1)
|
||||
|
||||
|
@ -33,7 +33,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
|
||||
"type" => String.to_existing_atom(object["type"]),
|
||||
"preferred_username" => object["preferredUsername"],
|
||||
"summary" => object["summary"],
|
||||
"url" => object["url"],
|
||||
"url" => object["id"],
|
||||
"name" => object["name"],
|
||||
"avatar" => avatar,
|
||||
"banner" => banner,
|
||||
|
@ -4,7 +4,6 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own internal one, and back
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Comment, as: CommentModel
|
||||
alias Mobilizon.Events.Event
|
||||
@ -20,7 +19,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map()) :: map()
|
||||
def as_to_model_data(object) do
|
||||
{:ok, %Actor{id: actor_id}} = Actors.get_or_fetch_by_url(object["actor"])
|
||||
{:ok, %Actor{id: actor_id}} = ActivityPub.get_or_fetch_by_url(object["actor"])
|
||||
Logger.debug("Inserting full comment")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
|
@ -8,23 +8,25 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
|
||||
Handles following and unfollowing relays and instances
|
||||
"""
|
||||
|
||||
alias Mobilizon.Activity
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.ActivityPub.Activity
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
|
||||
alias MobilizonWeb.API.Follows
|
||||
|
||||
require Logger
|
||||
|
||||
def get_actor do
|
||||
with {:ok, %Actor{} = actor} <-
|
||||
Actors.get_or_create_service_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
|
||||
Actors.get_or_create_actor_by_url("#{MobilizonWeb.Endpoint.url()}/relay") do
|
||||
actor
|
||||
end
|
||||
end
|
||||
|
||||
def follow(target_instance) do
|
||||
with %Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
|
||||
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
|
||||
{:ok, activity} <- Follows.follow(local_actor, target_actor) do
|
||||
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
|
||||
{:ok, activity}
|
||||
@ -37,7 +39,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
|
||||
|
||||
def unfollow(target_instance) do
|
||||
with %Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
|
||||
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
|
||||
{:ok, activity} <- Follows.unfollow(local_actor, target_actor) do
|
||||
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
|
||||
{:ok, activity}
|
||||
@ -50,7 +52,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
|
||||
|
||||
def accept(target_instance) do
|
||||
with %Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
|
||||
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
|
||||
{:ok, activity} <- Follows.accept(target_actor, local_actor) do
|
||||
{:ok, activity}
|
||||
end
|
||||
@ -58,7 +60,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
|
||||
|
||||
# def reject(target_instance) do
|
||||
# with %Actor{} = local_actor <- get_actor(),
|
||||
# {:ok, %Actor{} = target_actor} <- Actors.get_or_fetch_by_url(target_instance),
|
||||
# {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_by_url(target_instance),
|
||||
# {:ok, activity} <- Follows.reject(target_actor, local_actor) do
|
||||
# {:ok, activity}
|
||||
# end
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user