Merge branch 'Pascoual/mobilizon-docker-multi-stage-prod' into 'master'

Docker support

See merge request framasoft/mobilizon!674
This commit is contained in:
Thomas Citharel 2020-10-31 12:30:55 +01:00
commit c0591567f4
40 changed files with 786 additions and 301 deletions

1
.gitignore vendored
View File

@ -38,3 +38,4 @@ release/
*.mo
*.po~
.weblate
docker/production/.env

View File

@ -0,0 +1,42 @@
# First build the application assets
FROM node:alpine as assets
RUN apk add --no-cache python build-base
COPY js .
RUN yarn install \
&& yarn run build
# Then, build the application binary
FROM elixir:alpine AS builder
RUN apk add --no-cache build-base git cmake
COPY mix.exs mix.lock ./
ENV MIX_ENV=prod
RUN mix local.hex --force \
&& mix local.rebar --force \
&& mix deps.get
COPY lib ./lib
COPY priv ./priv
COPY config ./config
COPY rel ./rel
COPY docker/production/releases.exs ./config/
COPY --from=assets ./priv/static ./priv/static
RUN mix phx.digest \
&& mix release
# Finally setup the app
FROM alpine
RUN apk add --no-cache openssl ncurses-libs file
USER nobody
EXPOSE 4000
COPY --from=builder --chown=nobody:nobody _build/prod/rel/mobilizon ./
COPY docker/production/docker-entrypoint.sh ./
ENTRYPOINT ["./docker-entrypoint.sh"]

View File

@ -0,0 +1,42 @@
version: "3"
services:
mobilizon:
image: framasoft/mobilizon
environment:
- MOBILIZON_INSTANCE_NAME
- MOBILIZON_INSTANCE_HOST
- MOBILIZON_INSTANCE_EMAIL
- MOBILIZON_REPLY_EMAIL
- MOBILIZON_ADMIN_EMAIL
- MOBILIZON_INSTANCE_REGISTRATIONS_OPEN
- MOBILIZON_DATABASE_USERNAME=${POSTGRES_USER}
- MOBILIZON_DATABASE_PASSWORD=${POSTGRES_PASSWORD}
- MOBILIZON_DATABASE_DBNAME=${POSTGRES_DB}
- MOBILIZON_DATABASE_HOST=db
- MOBILIZON_INSTANCE_SECRET_KEY_BASE
- MOBILIZON_INSTANCE_SECRET_KEY
- MOBILIZON_SMTP_SERVER
- MOBILIZON_SMTP_HOSTNAME
- MOBILIZON_SMTP_PORT
- MOBILIZON_SMTP_SSL
- MOBILIZON_SMTP_USERNAME
- MOBILIZON_SMTP_PASSWORD
volumes:
- ./public/uploads:/app/uploads
ports:
- "4000:4000"
db:
image: postgis/postgis
volumes:
- ./db:/var/lib/postgresql/data
environment:
- POSTGRES_USER
- POSTGRES_PASSWORD
- POSTGRES_DB
networks:
default:
ipam:
driver: default

View File

@ -0,0 +1,9 @@
#!/bin/sh
set -e
echo "-- Running migrations..."
/bin/mobilizon_ctl migrate
echo "-- Starting!"
exec /bin/mobilizon start

View File

@ -0,0 +1,20 @@
# Copy this file to .env, then update it with your own settings
# Database settings
POSTGRES_USER=mobilizon
POSTGRES_PASSWORD=changethis
POSTGRES_DB=mobilizon
# Instance configuration
MOBILIZON_INSTANCE_NAME=My Mobilizon Instance
MOBILIZON_INSTANCE_HOST=mobilizon.lan
MOBILIZON_INSTANCE_SECRET_KEY_BASE=changethis
MOBILIZON_INSTANCE_SECRET_KEY=changethis
MOBILIZON_INSTANCE_EMAIL=noreply@mobilizon.lan
MOBILIZON_REPLY_EMAIL=contact@mobilizon.lan
# Email settings
MOBILIZON_SMTP_SERVER=localhost
MOBILIZON_SMTP_HOSTNAME=localhost
MOBILIZON_SMTP_USERNAME=noreply@mobilizon.lan
MOBILIZON_SMTP_PASSWORD=password

View File

@ -0,0 +1,51 @@
# Mobilizon instance configuration
import Config
config :mobilizon, Mobilizon.Web.Endpoint,
server: true,
url: [host: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.lan")],
http: [port: 4000],
secret_key_base: System.get_env("MOBILIZON_INSTANCE_SECRET_KEY_BASE", "changethis")
config :mobilizon, Mobilizon.Web.Auth.Guardian,
secret_key: System.get_env("MOBILIZON_INSTANCE_SECRET_KEY", "changethis")
config :mobilizon, :instance,
name: System.get_env("MOBILIZON_INSTANCE_NAME", "Mobilizon"),
description: "Change this to a proper description of your instance",
hostname: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.lan"),
registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN", "false") == "true",
demo: false,
allow_relay: true,
federating: true,
email_from: System.get_env("MOBILIZON_INSTANCE_EMAIL", "noreply@mobilizon.lan"),
email_reply_to: System.get_env("MOBILIZON_REPLY_EMAIL", "noreply@mobilizon.lan")
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local,
uploads: System.get_env("MOBILIZON_UPLOADS", "/app/uploads")
config :mobilizon, Mobilizon.Storage.Repo,
adapter: Ecto.Adapters.Postgres,
username: System.get_env("MOBILIZON_DATABASE_USERNAME", "username"),
password: System.get_env("MOBILIZON_DATABASE_PASSWORD", "password"),
database: System.get_env("MOBILIZON_DATABASE_DBNAME", "mobilizon"),
hostname: System.get_env("MOBILIZON_DATABASE_HOST", "postgres"),
port: 5432,
pool_size: 10
config :mobilizon, Mobilizon.Web.Email.Mailer,
adapter: Bamboo.SMTPAdapter,
server: System.get_env("MOBILIZON_SMTP_SERVER", "localhost"),
hostname: System.get_env("MOBILIZON_SMTP_HOSTNAME", "localhost"),
port: System.get_env("MOBILIZON_SMTP_PORT", "25"),
username: System.get_env("MOBILIZON_SMTP_USERNAME", nil),
password: System.get_env("MOBILIZON_SMTP_PASSWORD", nil),
tls: :if_available,
allowed_tls_versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"],
ssl: System.get_env("MOBILIZON_SMTP_SSL", "false"),
retries: 1,
no_mx_lookups: false,
auth: :if_available

View File

@ -79,7 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
def enqueue(type, payload, priority \\ 1) do
Logger.debug("enqueue something with type #{inspect(type)}")
if Mix.env() == :test do
if Application.fetch_env!(:mobilizon, :env) == :test do
handle(type, payload)
else
GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})

View File

@ -6,12 +6,18 @@ defmodule Mix.Tasks.Mobilizon.Actors do
use Mix.Task
alias Mix.Tasks
import Mix.Tasks.Mobilizon.Common
@shortdoc "Manages Mobilizon actors"
@impl Mix.Task
def run(_) do
Mix.shell().info("\nAvailable tasks:")
Tasks.Help.run(["--search", "mobilizon.actors."])
shell_info("\nAvailable tasks:")
if mix_shell?() do
Tasks.Help.run(["--search", "mobilizon.actors."])
else
show_subtasks_for_module(__MODULE__)
end
end
end

View File

@ -7,6 +7,7 @@ defmodule Mix.Tasks.Mobilizon.Actors.Refresh do
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Storage.Repo
import Ecto.Query
import Mix.Tasks.Mobilizon.Common
require Logger
@shortdoc "Refresh an actor or all actors"
@ -26,11 +27,11 @@ defmodule Mix.Tasks.Mobilizon.Actors.Refresh do
verbose = Keyword.get(options, :verbose, false)
Mix.Task.run("app.start")
start_mobilizon()
total = count_actors()
Mix.shell().info("""
shell_info("""
#{total} actors to process
""")
@ -62,22 +63,22 @@ defmodule Mix.Tasks.Mobilizon.Actors.Refresh do
@impl Mix.Task
def run([preferred_username]) do
Mix.Task.run("app.start")
start_mobilizon()
case ActivityPub.make_actor_from_nickname(preferred_username) do
{:ok, %Actor{}} ->
Mix.shell().info("""
shell_info("""
Actor #{preferred_username} refreshed
""")
{:actor, nil} ->
Mix.raise("Error: No such actor")
shell_error("Error: No such actor")
end
end
@impl Mix.Task
def run(_) do
Mix.raise("mobilizon.actors.refresh requires an username as argument or --all as an option")
shell_error("mobilizon.actors.refresh requires an username as argument or --all as an option")
end
@spec make_actor(String.t(), boolean()) :: any()

View File

@ -5,16 +5,17 @@ defmodule Mix.Tasks.Mobilizon.Actors.Show do
use Mix.Task
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
import Mix.Tasks.Mobilizon.Common
@shortdoc "Show a Mobilizon user details"
@impl Mix.Task
def run([preferred_username]) do
Mix.Task.run("app.start")
start_mobilizon()
case {:actor, Actors.get_actor_by_name_with_preload(preferred_username)} do
{:actor, %Actor{} = actor} ->
Mix.shell().info("""
shell_info("""
Informations for the actor #{actor.preferred_username}:
- Type: #{actor.type}
- Domain: #{if is_nil(actor.domain), do: "Local", else: actor.domain}
@ -24,11 +25,11 @@ defmodule Mix.Tasks.Mobilizon.Actors.Show do
""")
{:actor, nil} ->
Mix.raise("Error: No such actor")
shell_error("Error: No such actor")
end
end
def run(_) do
Mix.raise("mobilizon.actors.show requires an username as argument")
shell_error("mobilizon.actors.show requires an username as argument")
end
end

View File

@ -8,32 +8,107 @@ defmodule Mix.Tasks.Mobilizon.Common do
Common functions to be reused in mix tasks
"""
def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do
display = if defname || defval, do: "#{prompt} [#{defname || defval}]", else: "#{prompt}"
Keyword.get(options, opt) ||
case Mix.shell().prompt(display) do
"\n" ->
case defval do
nil ->
get_option(options, opt, prompt, defval)
defval ->
defval
end
opt ->
String.trim(opt)
end
end
def start_mobilizon do
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
{:ok, _} = Application.ensure_all_started(:mobilizon)
end
def get_option(options, opt, prompt, defval \\ nil, defname \\ nil) do
Keyword.get(options, opt) || shell_prompt(prompt, defval, defname)
end
def shell_prompt(prompt, defval \\ nil, defname \\ nil) do
prompt_message = "#{prompt} [#{defname || defval}] "
input =
if mix_shell?(),
do: Mix.shell().prompt(prompt_message),
else: :io.get_line(prompt_message)
case input do
"\n" ->
case defval do
nil ->
shell_prompt(prompt, defval, defname)
defval ->
defval
end
input ->
String.trim(input)
end
end
def shell_yes?(message) do
if mix_shell?(),
do: Mix.shell().yes?("Continue?"),
else: shell_prompt(message, "Continue?") in ~w(Yn Y y)
end
def shell_info(message) do
if mix_shell?(),
do: Mix.shell().info(message),
else: IO.puts(message)
end
def shell_error(message) do
if mix_shell?(),
do: Mix.shell().error(message),
else: IO.puts(:stderr, message)
end
@doc "Performs a safe check whether `Mix.shell/0` is available (does not raise if Mix is not loaded)"
def mix_shell?, do: :erlang.function_exported(Mix, :shell, 0)
def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end
@type task_module :: atom
@doc """
Gets the shortdoc for the given task `module`.
Returns the shortdoc or `nil`.
"""
@spec shortdoc(task_module) :: String.t() | nil
def shortdoc(module) when is_atom(module) do
case List.keyfind(module.__info__(:attributes), :shortdoc, 0) do
{:shortdoc, [shortdoc]} -> shortdoc
_ -> nil
end
end
def show_subtasks_for_module(module_name) do
tasks = list_subtasks_for_module(module_name)
max = Enum.reduce(tasks, 0, fn {name, _doc}, acc -> max(byte_size(name), acc) end)
Enum.each(tasks, fn {name, doc} ->
shell_info("#{String.pad_trailing(name, max + 2)} # #{doc}")
end)
end
@spec list_subtasks_for_module(atom()) :: list({String.t(), String.t()})
def list_subtasks_for_module(module_name) do
Application.load(:mobilizon)
{:ok, modules} = :application.get_key(:mobilizon, :modules)
module_name = to_string(module_name)
modules
|> Enum.filter(fn module ->
String.starts_with?(to_string(module), to_string(module_name)) &&
to_string(module) != to_string(module_name)
end)
|> Enum.map(&format_module/1)
end
defp format_module(module) do
{format_name(to_string(module)), shortdoc(module)}
end
defp format_name("Elixir.Mix.Tasks.Mobilizon." <> task_name) do
String.downcase(task_name)
end
end

View File

@ -8,12 +8,13 @@ defmodule Mix.Tasks.Mobilizon.CreateBot do
alias Mobilizon.{Actors, Users}
alias Mobilizon.Actors.Bot
alias Mobilizon.Users.User
import Mix.Tasks.Mobilizon.Common
require Logger
@shortdoc "Create bot"
def run([email, name, summary, type, url]) do
Mix.Task.run("app.start")
start_mobilizon()
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
actor <- Actors.register_bot(%{name: name, summary: summary}),

View File

@ -0,0 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Mobilizon.Ecto do
@moduledoc """
Provides tools for Ecto-related tasks (such as migrations)
"""
@doc """
Ensures the given repository's migrations path exists on the file system.
"""
@spec ensure_migrations_path(Ecto.Repo.t(), Keyword.t()) :: String.t()
def ensure_migrations_path(repo, opts) do
path = opts[:migrations_path] || Path.join(source_repo_priv(repo), "migrations")
path =
case Path.type(path) do
:relative ->
Path.join(Application.app_dir(:mobilizon), path)
:absolute ->
path
end
if not File.dir?(path) do
raise_missing_migrations(Path.relative_to_cwd(path), repo)
end
path
end
@doc """
Returns the private repository path relative to the source.
"""
def source_repo_priv(repo) do
config = repo.config()
priv = config[:priv] || "priv/#{repo |> Module.split() |> List.last() |> Macro.underscore()}"
Path.join(Application.app_dir(:mobilizon), priv)
end
defp raise_missing_migrations(path, repo) do
raise("""
Could not find migrations directory #{inspect(path)}
for repo #{inspect(repo)}.
This may be because you are in a new project and the
migration directory has not been created yet. Creating an
empty directory at the path above will fix this error.
If you expected existing migrations to be found, please
make sure your repository has been properly configured
and the configured path exists.
""")
end
end

View File

@ -0,0 +1,71 @@
# Portions of this file are derived from Pleroma:
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Mobilizon.Ecto.Migrate do
use Mix.Task
import Mix.Tasks.Mobilizon.Common
alias Mix.Tasks.Mobilizon.Ecto, as: EctoTask
require Logger
@shortdoc "Wrapper on `ecto.migrate` task."
@aliases [
n: :step,
v: :to
]
@switches [
all: :boolean,
step: :integer,
to: :integer,
quiet: :boolean,
log_sql: :boolean,
strict_version_order: :boolean,
migrations_path: :string
]
@repo Mobilizon.Storage.Repo
@moduledoc """
Changes `Logger` level to `:info` before start migration.
Changes level back when migration ends.
## Start migration
mix mobilizon.ecto.migrate [OPTIONS]
Options:
- see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Migrate.html
"""
@impl true
def run(args \\ []) do
start_mobilizon()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
if Application.get_env(:mobilizon, @repo)[:ssl] do
Application.ensure_all_started(:ssl)
end
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,
else: Keyword.put(opts, :all, true)
opts =
if opts[:quiet],
do: Keyword.merge(opts, log: false, log_sql: false),
else: opts
path = EctoTask.ensure_migrations_path(@repo, opts)
level = Logger.level()
Logger.configure(level: :info)
{:ok, _, _} = Ecto.Migrator.with_repo(@repo, &Ecto.Migrator.run(&1, path, :up, opts))
Logger.configure(level: level)
end
end

View File

@ -0,0 +1,74 @@
# Portions of this file are derived from Pleroma:
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Mobilizon.Ecto.Rollback do
use Mix.Task
import Mix.Tasks.Mobilizon.Common
alias Mix.Tasks.Mobilizon.Ecto, as: EctoTask
require Logger
@shortdoc "Wrapper on `ecto.rollback` task"
@aliases [
n: :step,
v: :to
]
@switches [
all: :boolean,
step: :integer,
to: :integer,
start: :boolean,
quiet: :boolean,
log_sql: :boolean,
migrations_path: :string
]
@repo Mobilizon.Storage.Repo
@moduledoc """
Changes `Logger` level to `:info` before start rollback.
Changes level back when rollback ends.
## Start rollback
mix mobilizon.ecto.rollback
Options:
- see https://hexdocs.pm/ecto/2.0.0/Mix.Tasks.Ecto.Rollback.html
"""
@impl true
def run(args \\ []) do
start_mobilizon()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
if Application.get_env(:mobilizon, @repo)[:ssl] do
Application.ensure_all_started(:ssl)
end
opts =
if opts[:to] || opts[:step] || opts[:all],
do: opts,
else: Keyword.put(opts, :step, 1)
opts =
if opts[:quiet],
do: Keyword.merge(opts, log: false, log_sql: false),
else: opts
path = EctoTask.ensure_migrations_path(@repo, opts)
level = Logger.level()
Logger.configure(level: :info)
if Mobilizon.Config.get(:env) == :test do
Logger.info("Rollback succesfully")
else
{:ok, _, _} = Ecto.Migrator.with_repo(@repo, &Ecto.Migrator.run(&1, path, :down, opts))
end
Logger.configure(level: level)
end
end

View File

@ -6,12 +6,13 @@ defmodule Mix.Tasks.Mobilizon.Groups do
use Mix.Task
alias Mix.Tasks
import Mix.Tasks.Mobilizon.Common
@shortdoc "Manages Mobilizon groups"
@impl Mix.Task
def run(_) do
Mix.shell().info("\nAvailable tasks:")
shell_info("\nAvailable tasks:")
Tasks.Help.run(["--search", "mobilizon.groups."])
end
end

View File

@ -6,12 +6,13 @@ defmodule Mix.Tasks.Mobilizon.Groups.Refresh do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Refresher
import Mix.Tasks.Mobilizon.Common
@shortdoc "Refresh a group private informations from an account member"
@impl Mix.Task
def run([group_url, on_behalf_of]) do
Mix.Task.run("app.start")
start_mobilizon()
with %Actor{} = actor <- Actors.get_local_actor_by_name(on_behalf_of) do
res = Refresher.fetch_group(group_url, actor)
@ -20,7 +21,7 @@ defmodule Mix.Tasks.Mobilizon.Groups.Refresh do
end
def run(_) do
Mix.raise(
shell_error(
"mobilizon.groups.refresh requires a group URL and an actor username which is member of the group as arguments"
)
end

View File

@ -29,7 +29,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
use Mix.Task
alias Mix.Tasks.Mobilizon.Common
import Mix.Tasks.Mobilizon.Common
@preferred_cli_env "prod"
@ -70,7 +70,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
if proceed? do
[domain, port | _] =
String.split(
Common.get_option(
get_option(
options,
:domain,
"What domain will your instance use? (e.g mobilizon.org)"
@ -79,25 +79,24 @@ defmodule Mix.Tasks.Mobilizon.Instance do
) ++ [443]
name =
Common.get_option(
get_option(
options,
:instance_name,
"What is the name of your instance? (e.g. Mobilizon)"
)
email =
Common.get_option(
get_option(
options,
:admin_email,
"What's the address email will be send with?",
"noreply@#{domain}"
)
dbhost =
Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
dbname =
Common.get_option(
get_option(
options,
:dbname,
"What is the name of your database?",
@ -105,7 +104,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
)
dbuser =
Common.get_option(
get_option(
options,
:dbuser,
"What is the user used to connect to your database?",
@ -113,7 +112,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
)
dbpass =
Common.get_option(
get_option(
options,
:dbpass,
"What is the password used to connect to your database?",
@ -122,7 +121,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
)
listen_port =
Common.get_option(
get_option(
options,
:listen_port,
"What port will the app listen to (leave it if you are using the default setup with nginx)?",
@ -160,24 +159,24 @@ defmodule Mix.Tasks.Mobilizon.Instance do
database_password: dbpass
)
Mix.shell().info("Writing config to #{config_path}.")
shell_info("Writing config to #{config_path}.")
File.write(config_path, result_config)
Mix.shell().info("Writing #{psql_path}.")
shell_info("Writing #{psql_path}.")
File.write(psql_path, result_psql)
Mix.shell().info(
shell_info(
"\n" <>
"""
To get started:
1. Check the contents of the generated files.
2. Run `sudo -u postgres psql -f #{Common.escape_sh_path(psql_path)} && rm #{
Common.escape_sh_path(psql_path)
2. Run `sudo -u postgres psql -f #{escape_sh_path(psql_path)} && rm #{
escape_sh_path(psql_path)
}`.
"""
)
else
Mix.shell().error(
shell_error(
"The task would have overwritten the following files:\n" <>
(will_overwrite |> Enum.map(&"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `-f/--force` to overwrite them."

View File

@ -1,68 +0,0 @@
defmodule Mix.Tasks.Mobilizon.MoveParticipantStats do
@moduledoc """
Temporary task to move participant stats in the events table
This task will be removed in version 1.0.0-beta.3
"""
use Mix.Task
import Ecto.Query
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Events.ParticipantRole
alias Mobilizon.Storage.Repo
require Logger
@shortdoc "Move participant stats to events table"
def run([]) do
Mix.Task.run("app.start")
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts(
"\nStarting inserting participants stats into #{nb_events} events, this can take a while…\n"
)
insert_participants_stats_into_events(events, nb_events)
end
defp insert_participants_stats_into_events([%Event{url: url} = event | events], nb_events) do
with roles <- ParticipantRole.__enum_map__(),
counts <-
Enum.reduce(roles, %{}, fn role, acc ->
Map.put(acc, role, count_participants(event, role))
end),
{:ok, _} <-
Events.update_event(event, %{
participant_stats: counts
}) do
Logger.debug("Added participants stats to event #{url}")
else
{:error, res} ->
Logger.error("Error while adding participants stats to event #{url} : #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_participants_stats_into_events(events, nb_events)
end
defp insert_participants_stats_into_events([], nb_events) do
IO.puts("\nFinished inserting participant stats for #{nb_events} events!\n")
end
defp count_participants(%Event{id: event_id}, role) when is_atom(role) do
event_id
|> Events.count_participants_query()
|> Events.filter_role(role)
|> Repo.aggregate(:count, :id)
end
end

View File

@ -22,60 +22,19 @@ defmodule Mix.Tasks.Mobilizon.Relay do
use Mix.Task
alias Mix.Tasks.Mobilizon.Common
alias Mobilizon.Federation.ActivityPub.Relay
alias Mix.Tasks
import Mix.Tasks.Mobilizon.Common
@shortdoc "Manages remote relays"
def run(["follow", target]) do
Common.start_mobilizon()
case Relay.follow(target) do
{:ok, _activity, _follow} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
@impl Mix.Task
def run(_) do
shell_info("\nAvailable tasks:")
{:error, e} ->
IO.puts(:stderr, "Error while following #{target}: #{inspect(e)}")
end
end
def run(["unfollow", target]) do
Common.start_mobilizon()
case Relay.unfollow(target) do
{:ok, _activity, _follow} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while unfollowing #{target}: #{inspect(e)}")
end
end
def run(["accept", target]) do
Common.start_mobilizon()
case Relay.accept(target) do
{:ok, _activity} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while accept #{target} follow: #{inspect(e)}")
end
end
def run(["refresh", target]) do
Common.start_mobilizon()
IO.puts("Refreshing #{target}, this can take a while.")
case Relay.refresh(target) do
:ok ->
IO.puts("Refreshed #{target}")
err ->
IO.puts(:stderr, "Error while refreshing #{target}: #{inspect(err)}")
if mix_shell?() do
Tasks.Help.run(["--search", "mobilizon.relay."])
else
show_subtasks_for_module(__MODULE__)
end
end
end

View File

@ -0,0 +1,28 @@
defmodule Mix.Tasks.Mobilizon.Relay.Accept do
@moduledoc """
Task to accept an instance follow request
"""
use Mix.Task
alias Mobilizon.Federation.ActivityPub.Relay
import Mix.Tasks.Mobilizon.Common
@shortdoc "Accept an instance follow request"
@impl Mix.Task
def run([target]) do
start_mobilizon()
case Relay.accept(target) do
{:ok, _activity} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while accept #{target} follow: #{inspect(e)}")
end
end
def run(_) do
shell_error("mobilizon.relay.accept requires an instance hostname as arguments")
end
end

View File

@ -0,0 +1,28 @@
defmodule Mix.Tasks.Mobilizon.Relay.Follow do
@moduledoc """
Task to follow an instance
"""
use Mix.Task
alias Mobilizon.Federation.ActivityPub.Relay
import Mix.Tasks.Mobilizon.Common
@shortdoc "Follow an instance"
@impl Mix.Task
def run([target]) do
start_mobilizon()
case Relay.follow(target) do
{:ok, _activity, _follow} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while following #{target}: #{inspect(e)}")
end
end
def run(_) do
shell_error("mobilizon.relay.follow requires an instance hostname as arguments")
end
end

View File

@ -0,0 +1,28 @@
defmodule Mix.Tasks.Mobilizon.Relay.Refresh do
@moduledoc """
Task to refresh an instance details
"""
use Mix.Task
alias Mobilizon.Federation.ActivityPub.Relay
import Mix.Tasks.Mobilizon.Common
@shortdoc "Refresh an instance informations and crawl their outbox"
@impl Mix.Task
def run([target]) do
start_mobilizon()
IO.puts("Refreshing #{target}, this can take a while.")
case Relay.refresh(target) do
:ok ->
IO.puts("Refreshed #{target}")
err ->
IO.puts(:stderr, "Error while refreshing #{target}: #{inspect(err)}")
end
end
def run(_) do
shell_error("mobilizon.relay.refresh requires an instance hostname as arguments")
end
end

View File

@ -0,0 +1,28 @@
defmodule Mix.Tasks.Mobilizon.Relay.Unfollow do
@moduledoc """
Task to unfollow an instance
"""
use Mix.Task
alias Mobilizon.Federation.ActivityPub.Relay
import Mix.Tasks.Mobilizon.Common
@shortdoc "Unfollow an instance"
@impl Mix.Task
def run([target]) do
start_mobilizon()
case Relay.unfollow(target) do
{:ok, _activity, _follow} ->
# put this task to sleep to allow the genserver to push out the messages
:timer.sleep(500)
{:error, e} ->
IO.puts(:stderr, "Error while unfollowing #{target}: #{inspect(e)}")
end
end
def run(_) do
shell_error("mobilizon.relay.unfollow requires an instance hostname as arguments")
end
end

View File

@ -1,50 +0,0 @@
defmodule Mix.Tasks.Mobilizon.SetupSearch do
@moduledoc """
Temporary task to insert search data from existing events
This task will be removed in version 1.0.0-beta.3
"""
use Mix.Task
import Ecto.Query
alias Mobilizon.Events.Event
alias Mobilizon.Service.Workers
alias Mobilizon.Storage.Repo
require Logger
@shortdoc "Insert search data"
def run([]) do
Mix.Task.run("app.start")
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts("\nStarting setting up search for #{nb_events} events, this can take a while…\n")
insert_search_event(events, nb_events)
end
defp insert_search_event([%Event{url: url} = event | events], nb_events) do
case Workers.BuildSearch.insert_search_event(event) do
{:ok, _} ->
Logger.debug("Added event #{url} to the search")
{:error, res} ->
Logger.error("Error while adding event #{url} to the search: #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_search_event(events, nb_events)
end
defp insert_search_event([], nb_events) do
IO.puts("\nFinished setting up search for #{nb_events} events!\n")
end
end

View File

@ -4,7 +4,7 @@ defmodule Mix.Tasks.Mobilizon.SiteMap do
"""
use Mix.Task
alias Mix.Tasks.Mobilizon.Common
import Mix.Tasks.Mobilizon.Common
alias Mobilizon.Service.SiteMap
alias Mobilizon.Web.Endpoint
@ -12,10 +12,10 @@ defmodule Mix.Tasks.Mobilizon.SiteMap do
@shortdoc "Generates a new Sitemap"
def run(["generate"]) do
Common.start_mobilizon()
start_mobilizon()
with {:ok, :ok} <- SiteMap.generate_sitemap() do
Mix.shell().info("Sitemap saved to #{Endpoint.url()}/sitemap.xml")
shell_info("Sitemap saved to #{Endpoint.url()}/sitemap.xml")
end
end
end

View File

@ -1,30 +0,0 @@
defmodule Mix.Tasks.Mobilizon.Toot do
@moduledoc """
Creates a bot from a source.
"""
use Mix.Task
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.GraphQL.API.Comments
require Logger
@shortdoc "Toot to an user"
def run([from, text]) do
Mix.Task.run("app.start")
with {:local_actor, %Actor{} = actor} <- {:local_actor, Actors.get_local_actor_by_name(from)},
{:ok, _, _} <- Comments.create_comment(%{actor: actor, text: text}) do
Mix.shell().info("Tooted")
else
{:local_actor, _, _} ->
Mix.shell().error("Failed to toot.\nActor #{from} doesn't exist")
_ ->
Mix.shell().error("Failed to toot.")
end
end
end

View File

@ -6,12 +6,18 @@ defmodule Mix.Tasks.Mobilizon.Users do
use Mix.Task
alias Mix.Tasks
import Mix.Tasks.Mobilizon.Common
@shortdoc "Manages Mobilizon users"
@impl Mix.Task
def run(_) do
Mix.shell().info("\nAvailable tasks:")
Tasks.Help.run(["--search", "mobilizon.users."])
shell_info("\nAvailable tasks:")
if mix_shell?() do
Tasks.Help.run(["--search", "mobilizon.users."])
else
show_subtasks_for_module(__MODULE__)
end
end
end

View File

@ -5,6 +5,7 @@ defmodule Mix.Tasks.Mobilizon.Users.Delete do
use Mix.Task
alias Mobilizon.Users
alias Mobilizon.Users.User
import Mix.Tasks.Mobilizon.Common
@shortdoc "Deletes a Mobilizon user"
@ -26,25 +27,25 @@ defmodule Mix.Tasks.Mobilizon.Users.Delete do
assume_yes? = Keyword.get(options, :assume_yes, false)
keep_email? = Keyword.get(options, :keep_email, false)
Mix.Task.run("app.start")
start_mobilizon()
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
true <- assume_yes? or Mix.shell().yes?("Continue with deleting user #{user.email}?"),
true <- assume_yes? or shell_yes?("Continue with deleting user #{user.email}?"),
{:ok, %User{} = user} <-
Users.delete_user(user, reserve_email: keep_email?) do
Mix.shell().info("""
shell_info("""
The user #{user.email} has been deleted
""")
else
{:error, :user_not_found} ->
Mix.raise("Error: No such user")
shell_error("Error: No such user")
_ ->
Mix.raise("User has not been deleted.")
shell_error("User has not been deleted.")
end
end
def run(_) do
Mix.raise("mobilizon.users.delete requires an email as argument")
shell_error("mobilizon.users.delete requires an email as argument")
end
end

View File

@ -3,6 +3,7 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
Task to modify an existing Mobilizon user
"""
use Mix.Task
import Mix.Tasks.Mobilizon.Common
alias Mobilizon.Users
alias Mobilizon.Users.User
@ -31,10 +32,10 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
new_email = Keyword.get(options, :email)
if disable? && enable? do
Mix.raise("Can't use both --enabled and --disable options at the same time.")
shell_error("Can't use both --enabled and --disable options at the same time.")
end
Mix.Task.run("app.start")
start_mobilizon()
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
attrs <- %{},
@ -53,7 +54,7 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
),
{:makes_changes, true} <- {:makes_changes, attrs != %{}},
{:ok, %User{} = user} <- Users.update_user(user, attrs) do
Mix.shell().info("""
shell_info("""
An user has been modified with the following information:
- email: #{user.email}
- Role: #{user.role}
@ -61,23 +62,23 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
""")
else
{:makes_changes, false} ->
Mix.shell().info("No change has been made")
shell_info("No change has been made")
{:error, :user_not_found} ->
Mix.raise("Error: No such user")
shell_error("Error: No such user")
{:error, %Ecto.Changeset{errors: errors}} ->
Mix.shell().error(inspect(errors))
Mix.raise("User has not been modified because of the above reason.")
shell_error(inspect(errors))
shell_error("User has not been modified because of the above reason.")
err ->
Mix.shell().error(inspect(err))
Mix.raise("User has not been modified because of an unknown reason.")
shell_error(inspect(err))
shell_error("User has not been modified because of an unknown reason.")
end
end
def run(_) do
Mix.raise("mobilizon.users.new requires an email as argument")
shell_error("mobilizon.users.new requires an email as argument")
end
@spec process_new_value(map(), atom(), any(), any()) :: map()

View File

@ -3,6 +3,7 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
Task to create a new user
"""
use Mix.Task
import Mix.Tasks.Mobilizon.Common
alias Mobilizon.Users
alias Mobilizon.Users.User
@ -40,7 +41,7 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
:crypto.strong_rand_bytes(16) |> Base.encode64() |> binary_part(0, 16)
)
Mix.Task.run("app.start")
start_mobilizon()
case Users.register(%{
email: email,
@ -51,7 +52,7 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
confirmation_token: nil
}) do
{:ok, %User{} = user} ->
Mix.shell().info("""
shell_info("""
An user has been created with the following information:
- email: #{user.email}
- password: #{password}
@ -60,16 +61,16 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
""")
{:error, %Ecto.Changeset{errors: errors}} ->
Mix.shell().error(inspect(errors))
Mix.raise("User has not been created because of the above reason.")
shell_error(inspect(errors))
shell_error("User has not been created because of the above reason.")
err ->
Mix.shell().error(inspect(err))
Mix.raise("User has not been created because of an unknown reason.")
shell_error(inspect(err))
shell_error("User has not been created because of an unknown reason.")
end
end
def run(_) do
Mix.raise("mobilizon.users.new requires an email as argument")
shell_error("mobilizon.users.new requires an email as argument")
end
end

View File

@ -4,7 +4,7 @@ defmodule Mix.Tasks.Mobilizon.Users.Show do
"""
use Mix.Task
import Mix.Tasks.Mobilizon.Common
alias Mobilizon.Actors.Actor
alias Mobilizon.Users
alias Mobilizon.Users.User
@ -13,11 +13,11 @@ defmodule Mix.Tasks.Mobilizon.Users.Show do
@impl Mix.Task
def run([email]) do
Mix.Task.run("app.start")
start_mobilizon()
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
actors <- Users.get_actors_for_user(user) do
Mix.shell().info("""
shell_info("""
Informations for the user #{user.email}:
- Activated: #{user.confirmed_at}
- Disabled: #{user.disabled}
@ -26,12 +26,12 @@ defmodule Mix.Tasks.Mobilizon.Users.Show do
""")
else
{:error, :user_not_found} ->
Mix.raise("Error: No such user")
shell_error("Error: No such user")
end
end
def run(_) do
Mix.raise("mobilizon.users.show requires an email as argument")
shell_error("mobilizon.users.show requires an email as argument")
end
defp display_actors([]), do: ""

View File

@ -21,7 +21,7 @@ defmodule Mobilizon do
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@env Mix.env()
@env Application.fetch_env!(:mobilizon, :env)
@spec named_version :: String.t()
def named_version, do: "#{@name} #{@version}"

53
lib/mobilizon/cli.ex Normal file
View File

@ -0,0 +1,53 @@
# Portions of this file are derived from Pleroma:
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mobilizon.CLI do
@moduledoc """
CLI wrapper for releases
"""
alias Mix.Tasks.Mobilizon.Ecto.{Migrate, Rollback}
def run(args) do
[task | args] = String.split(args)
case task do
"migrate" -> migrate(args)
"rollback" -> rollback(args)
task -> mix_task(task, args)
end
end
defp mix_task(task, args) do
Application.load(:mobilizon)
{:ok, modules} = :application.get_key(:mobilizon, :modules)
module =
Enum.find(modules, fn module ->
module = Module.split(module)
case module do
["Mix", "Tasks", "Mobilizon" | rest] ->
String.downcase(Enum.join(rest, ".")) == task
_ ->
false
end
end)
if module do
module.run(args)
else
IO.puts("The task #{task} does not exist")
end
end
def migrate(args) do
Migrate.run(args)
end
def rollback(args) do
Rollback.run(args)
end
end

View File

@ -38,8 +38,7 @@ defmodule Mobilizon.Web.Endpoint do
at: "/",
from: {:mobilizon, "priv/static"},
gzip: false,
only:
~w(index.html manifest.json service-worker.js css fonts images js favicon.ico robots.txt),
only: ~w(index.html manifest.json service-worker.js css fonts img js favicon.ico robots.txt),
only_matching: ["precache-manifest"]
)

View File

@ -169,7 +169,7 @@ defmodule Mobilizon.Web.Router do
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
if Mix.env() in [:dev, :e2e] do
if Application.fetch_env!(:mobilizon, :env) in [:dev, :e2e] do
# If using Phoenix
forward("/sent_emails", Bamboo.SentEmailViewerPlug)
end

41
rel/overlays/bin/mobilizon_ctl Executable file
View File

@ -0,0 +1,41 @@
#!/bin/sh
# Portions of this file are derived from Pleroma:
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
if [ -z "$1" ] || [ "$1" = "help" ]; then
echo "Usage: $(basename "$0") COMMAND [ARGS]
The known commands are:
migrate
Execute database migrations (needs to be done after updates)
rollback [VERSION]
Rollback database migrations (needs to be done before downgrading)
and any mix tasks under Mobilizon namespace, for example \`mix mobilizon.user.show COMMAND\` is
equivalent to \`$(basename "$0") user.show COMMAND\`
By default mobilizon_ctl will try calling into a running instance to execute non migration-related commands,
if for some reason this is undesired, set MOBILIZON_CTL_RPC_DISABLED environment variable.
"
else
SCRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT")
FULL_ARGS="$*"
ACTION="$1"
if [ $# -gt 0 ]; then
shift
fi
if [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$MOBILIZON_CTL_RPC_DISABLED" = true ]; then
"$SCRIPTPATH"/mobilizon eval 'Mobilizon.CLI.run("'"$FULL_ARGS"'")'
else
"$SCRIPTPATH"/mobilizon rpc 'Mobilizon.CLI.run("'"$FULL_ARGS"'")'
fi
fi

View File

@ -48,7 +48,9 @@ defmodule Mix.Tasks.Mobilizon.ActorsTest do
end
test "show non-existing actor" do
assert_raise Mix.Error, "Error: No such actor", fn -> Show.run([@username]) end
Show.run([@username])
assert_received {:mix_shell, :error, [message]}
assert message =~ "Error: No such actor"
end
end
end

View File

@ -9,6 +9,7 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower}
alias Mix.Tasks.Mobilizon.Relay.{Follow, Unfollow}
alias Mobilizon.Federation.ActivityPub.Relay
@ -17,7 +18,7 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do
use_cassette "relay/fetch_relay_follow" do
target_instance = "mobilizon1.com"
Mix.Tasks.Mobilizon.Relay.run(["follow", target_instance])
Follow.run([target_instance])
local_actor = Relay.get_actor()
assert local_actor.url =~ "/relay"
@ -35,7 +36,7 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do
use_cassette "relay/fetch_relay_unfollow" do
target_instance = "mobilizon1.com"
Mix.Tasks.Mobilizon.Relay.run(["follow", target_instance])
Follow.run([target_instance])
%Actor{} = local_actor = Relay.get_actor()
@ -44,7 +45,7 @@ defmodule Mix.Tasks.Mobilizon.RelayTest do
assert %Follower{} = Actors.is_following(local_actor, target_actor)
Mix.Tasks.Mobilizon.Relay.run(["unfollow", target_instance])
Unfollow.run([target_instance])
refute Actors.is_following(local_actor, target_actor)
end

View File

@ -42,9 +42,15 @@ defmodule Mix.Tasks.Mobilizon.UsersTest do
test "create with already used email" do
insert(:user, email: @email)
assert_raise Mix.Error, "User has not been created because of the above reason.", fn ->
New.run([@email])
end
New.run([@email])
# Debug message
assert_received {:mix_shell, :error, [message]}
assert message =~
"[email: {\"This email is already used.\", [constraint: :unique, constraint_name: \"users_email_index\"]}]"
assert_received {:mix_shell, :error, [message]}
assert message =~ "User has not been created because of the above reason."
end
end
@ -62,7 +68,9 @@ defmodule Mix.Tasks.Mobilizon.UsersTest do
end
test "delete non-existing user" do
assert_raise Mix.Error, "Error: No such user", fn -> Delete.run([@email, "-y"]) end
Delete.run([@email, "-y"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "Error: No such user"
end
end
@ -87,7 +95,9 @@ defmodule Mix.Tasks.Mobilizon.UsersTest do
end
test "show non-existing user" do
assert_raise Mix.Error, "Error: No such user", fn -> Show.run([@email]) end
Show.run([@email])
assert_received {:mix_shell, :error, [message]}
assert message =~ "Error: No such user"
end
end
@ -160,11 +170,9 @@ defmodule Mix.Tasks.Mobilizon.UsersTest do
end
test "enable and disable at the same time" do
assert_raise Mix.Error,
"Can't use both --enabled and --disable options at the same time.",
fn ->
Modify.run([@email, "--disable", "--enable"])
end
Modify.run([@email, "--disable", "--enable"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "Can't use both --enabled and --disable options at the same time."
end
end
end