diff --git a/.gitignore b/.gitignore index 571bd8abf..2c09e1644 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ release/ *.mo *.po~ .weblate +docker/production/.env diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile new file mode 100644 index 000000000..d4c54e2f3 --- /dev/null +++ b/docker/production/Dockerfile @@ -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"] diff --git a/docker/production/docker-compose.yml b/docker/production/docker-compose.yml new file mode 100644 index 000000000..7fe4824d9 --- /dev/null +++ b/docker/production/docker-compose.yml @@ -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 diff --git a/docker/production/docker-entrypoint.sh b/docker/production/docker-entrypoint.sh new file mode 100755 index 000000000..9e118d45a --- /dev/null +++ b/docker/production/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -e + +echo "-- Running migrations..." +/bin/mobilizon_ctl migrate + +echo "-- Starting!" +exec /bin/mobilizon start \ No newline at end of file diff --git a/docker/production/env.template b/docker/production/env.template new file mode 100644 index 000000000..6a037b6c2 --- /dev/null +++ b/docker/production/env.template @@ -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 diff --git a/docker/production/releases.exs b/docker/production/releases.exs new file mode 100644 index 000000000..d3284e0f9 --- /dev/null +++ b/docker/production/releases.exs @@ -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 diff --git a/lib/federation/activity_pub/federator.ex b/lib/federation/activity_pub/federator.ex index 74b947afe..d9027084f 100644 --- a/lib/federation/activity_pub/federator.ex +++ b/lib/federation/activity_pub/federator.ex @@ -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}) diff --git a/lib/mix/tasks/mobilizon/actors.ex b/lib/mix/tasks/mobilizon/actors.ex index 1e3bb1ce2..ccea79c73 100644 --- a/lib/mix/tasks/mobilizon/actors.ex +++ b/lib/mix/tasks/mobilizon/actors.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/actors/refresh.ex b/lib/mix/tasks/mobilizon/actors/refresh.ex index e384be889..d01c23710 100644 --- a/lib/mix/tasks/mobilizon/actors/refresh.ex +++ b/lib/mix/tasks/mobilizon/actors/refresh.ex @@ -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() diff --git a/lib/mix/tasks/mobilizon/actors/show.ex b/lib/mix/tasks/mobilizon/actors/show.ex index 8abab9e78..73f088796 100644 --- a/lib/mix/tasks/mobilizon/actors/show.ex +++ b/lib/mix/tasks/mobilizon/actors/show.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/common.ex b/lib/mix/tasks/mobilizon/common.ex index 074b8d547..3f292dcc8 100644 --- a/lib/mix/tasks/mobilizon/common.ex +++ b/lib/mix/tasks/mobilizon/common.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/create_bot.ex b/lib/mix/tasks/mobilizon/create_bot.ex index fce2b6166..2aea6166b 100644 --- a/lib/mix/tasks/mobilizon/create_bot.ex +++ b/lib/mix/tasks/mobilizon/create_bot.ex @@ -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}), diff --git a/lib/mix/tasks/mobilizon/ecto.ex b/lib/mix/tasks/mobilizon/ecto.ex new file mode 100644 index 000000000..5ce88e5e1 --- /dev/null +++ b/lib/mix/tasks/mobilizon/ecto.ex @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# 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 diff --git a/lib/mix/tasks/mobilizon/ecto/migrate.ex b/lib/mix/tasks/mobilizon/ecto/migrate.ex new file mode 100644 index 000000000..3e6c8812e --- /dev/null +++ b/lib/mix/tasks/mobilizon/ecto/migrate.ex @@ -0,0 +1,71 @@ +# Portions of this file are derived from Pleroma: +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# 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 diff --git a/lib/mix/tasks/mobilizon/ecto/rollback.ex b/lib/mix/tasks/mobilizon/ecto/rollback.ex new file mode 100644 index 000000000..694e948da --- /dev/null +++ b/lib/mix/tasks/mobilizon/ecto/rollback.ex @@ -0,0 +1,74 @@ +# Portions of this file are derived from Pleroma: +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# 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 diff --git a/lib/mix/tasks/mobilizon/groups.ex b/lib/mix/tasks/mobilizon/groups.ex index 412b38fe5..3d3d3a6f9 100644 --- a/lib/mix/tasks/mobilizon/groups.ex +++ b/lib/mix/tasks/mobilizon/groups.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/groups/refresh.ex b/lib/mix/tasks/mobilizon/groups/refresh.ex index a88e0d22d..6e5caac57 100644 --- a/lib/mix/tasks/mobilizon/groups/refresh.ex +++ b/lib/mix/tasks/mobilizon/groups/refresh.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/instance.ex b/lib/mix/tasks/mobilizon/instance.ex index a833edcdf..a5bf2d648 100644 --- a/lib/mix/tasks/mobilizon/instance.ex +++ b/lib/mix/tasks/mobilizon/instance.ex @@ -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." diff --git a/lib/mix/tasks/mobilizon/move_participant_stats.ex b/lib/mix/tasks/mobilizon/move_participant_stats.ex deleted file mode 100644 index 5c31be348..000000000 --- a/lib/mix/tasks/mobilizon/move_participant_stats.ex +++ /dev/null @@ -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 diff --git a/lib/mix/tasks/mobilizon/relay.ex b/lib/mix/tasks/mobilizon/relay.ex index 4dc463a6a..03c612bc2 100644 --- a/lib/mix/tasks/mobilizon/relay.ex +++ b/lib/mix/tasks/mobilizon/relay.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/relay/accept.ex b/lib/mix/tasks/mobilizon/relay/accept.ex new file mode 100644 index 000000000..88d594ff8 --- /dev/null +++ b/lib/mix/tasks/mobilizon/relay/accept.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/relay/follow.ex b/lib/mix/tasks/mobilizon/relay/follow.ex new file mode 100644 index 000000000..43c29f456 --- /dev/null +++ b/lib/mix/tasks/mobilizon/relay/follow.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/relay/refresh.ex b/lib/mix/tasks/mobilizon/relay/refresh.ex new file mode 100644 index 000000000..f788009b4 --- /dev/null +++ b/lib/mix/tasks/mobilizon/relay/refresh.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/relay/unfollow.ex b/lib/mix/tasks/mobilizon/relay/unfollow.ex new file mode 100644 index 000000000..c368c3fb5 --- /dev/null +++ b/lib/mix/tasks/mobilizon/relay/unfollow.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/setup_search.ex b/lib/mix/tasks/mobilizon/setup_search.ex deleted file mode 100644 index 4633a2a76..000000000 --- a/lib/mix/tasks/mobilizon/setup_search.ex +++ /dev/null @@ -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 diff --git a/lib/mix/tasks/mobilizon/site_map.ex b/lib/mix/tasks/mobilizon/site_map.ex index ef4162892..d35c94990 100644 --- a/lib/mix/tasks/mobilizon/site_map.ex +++ b/lib/mix/tasks/mobilizon/site_map.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/toot.ex b/lib/mix/tasks/mobilizon/toot.ex deleted file mode 100644 index 3473d8b56..000000000 --- a/lib/mix/tasks/mobilizon/toot.ex +++ /dev/null @@ -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 diff --git a/lib/mix/tasks/mobilizon/users.ex b/lib/mix/tasks/mobilizon/users.ex index e61a4e77a..aa3386b7f 100644 --- a/lib/mix/tasks/mobilizon/users.ex +++ b/lib/mix/tasks/mobilizon/users.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/users/delete.ex b/lib/mix/tasks/mobilizon/users/delete.ex index d769601a0..c72cd97a8 100644 --- a/lib/mix/tasks/mobilizon/users/delete.ex +++ b/lib/mix/tasks/mobilizon/users/delete.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/users/modify.ex b/lib/mix/tasks/mobilizon/users/modify.ex index 66e0b26ee..dd8a4181c 100644 --- a/lib/mix/tasks/mobilizon/users/modify.ex +++ b/lib/mix/tasks/mobilizon/users/modify.ex @@ -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() diff --git a/lib/mix/tasks/mobilizon/users/new.ex b/lib/mix/tasks/mobilizon/users/new.ex index d0802b329..453762798 100644 --- a/lib/mix/tasks/mobilizon/users/new.ex +++ b/lib/mix/tasks/mobilizon/users/new.ex @@ -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 diff --git a/lib/mix/tasks/mobilizon/users/show.ex b/lib/mix/tasks/mobilizon/users/show.ex index 81864bfe6..3c33aaf52 100644 --- a/lib/mix/tasks/mobilizon/users/show.ex +++ b/lib/mix/tasks/mobilizon/users/show.ex @@ -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: "" diff --git a/lib/mobilizon.ex b/lib/mobilizon.ex index 496deb893..378584b51 100644 --- a/lib/mobilizon.ex +++ b/lib/mobilizon.ex @@ -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}" diff --git a/lib/mobilizon/cli.ex b/lib/mobilizon/cli.ex new file mode 100644 index 000000000..91d105d3d --- /dev/null +++ b/lib/mobilizon/cli.ex @@ -0,0 +1,53 @@ +# Portions of this file are derived from Pleroma: +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# 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 diff --git a/lib/web/endpoint.ex b/lib/web/endpoint.ex index 16b27d541..8a0601be7 100644 --- a/lib/web/endpoint.ex +++ b/lib/web/endpoint.ex @@ -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"] ) diff --git a/lib/web/router.ex b/lib/web/router.ex index 37d5e4ab4..80f75d63f 100644 --- a/lib/web/router.ex +++ b/lib/web/router.ex @@ -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 diff --git a/rel/overlays/bin/mobilizon_ctl b/rel/overlays/bin/mobilizon_ctl new file mode 100755 index 000000000..458de578c --- /dev/null +++ b/rel/overlays/bin/mobilizon_ctl @@ -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 +# 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 \ No newline at end of file diff --git a/test/tasks/actors_test.exs b/test/tasks/actors_test.exs index a67c69468..934dd33e0 100644 --- a/test/tasks/actors_test.exs +++ b/test/tasks/actors_test.exs @@ -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 diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs index 5e64dfdd5..87f197718 100644 --- a/test/tasks/relay_test.exs +++ b/test/tasks/relay_test.exs @@ -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 diff --git a/test/tasks/users_test.exs b/test/tasks/users_test.exs index 10d88ba59..4c9cbc625 100644 --- a/test/tasks/users_test.exs +++ b/test/tasks/users_test.exs @@ -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