From 01f746a5d2b8021df20ceee4f13c9a07e3dcec46 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 30 Oct 2020 15:16:01 +0100 Subject: [PATCH] Allow to use Mix tasks inside Releases Signed-off-by: Thomas Citharel --- lib/mix/tasks/mobilizon/actors.ex | 10 +- lib/mix/tasks/mobilizon/actors/refresh.ex | 13 +- lib/mix/tasks/mobilizon/actors/show.ex | 9 +- lib/mix/tasks/mobilizon/common.ex | 113 +++++++++++++++--- lib/mix/tasks/mobilizon/create_bot.ex | 3 +- lib/mix/tasks/mobilizon/ecto.ex | 54 +++++++++ lib/mix/tasks/mobilizon/ecto/migrate.ex | 71 +++++++++++ lib/mix/tasks/mobilizon/ecto/rollback.ex | 74 ++++++++++++ lib/mix/tasks/mobilizon/groups.ex | 3 +- lib/mix/tasks/mobilizon/groups/refresh.ex | 5 +- lib/mix/tasks/mobilizon/instance.ex | 31 +++-- .../tasks/mobilizon/move_participant_stats.ex | 68 ----------- lib/mix/tasks/mobilizon/relay.ex | 59 ++------- lib/mix/tasks/mobilizon/relay/accept.ex | 28 +++++ lib/mix/tasks/mobilizon/relay/follow.ex | 28 +++++ lib/mix/tasks/mobilizon/relay/refresh.ex | 28 +++++ lib/mix/tasks/mobilizon/relay/unfollow.ex | 28 +++++ lib/mix/tasks/mobilizon/setup_search.ex | 50 -------- lib/mix/tasks/mobilizon/site_map.ex | 6 +- lib/mix/tasks/mobilizon/toot.ex | 30 ----- lib/mix/tasks/mobilizon/users.ex | 10 +- lib/mix/tasks/mobilizon/users/delete.ex | 13 +- lib/mix/tasks/mobilizon/users/modify.ex | 21 ++-- lib/mix/tasks/mobilizon/users/new.ex | 15 +-- lib/mix/tasks/mobilizon/users/show.ex | 10 +- lib/mobilizon/cli.ex | 52 +++++++- test/tasks/actors_test.exs | 4 +- test/tasks/relay_test.exs | 7 +- test/tasks/users_test.exs | 28 +++-- 29 files changed, 570 insertions(+), 301 deletions(-) create mode 100644 lib/mix/tasks/mobilizon/ecto.ex create mode 100644 lib/mix/tasks/mobilizon/ecto/migrate.ex create mode 100644 lib/mix/tasks/mobilizon/ecto/rollback.ex delete mode 100644 lib/mix/tasks/mobilizon/move_participant_stats.ex create mode 100644 lib/mix/tasks/mobilizon/relay/accept.ex create mode 100644 lib/mix/tasks/mobilizon/relay/follow.ex create mode 100644 lib/mix/tasks/mobilizon/relay/refresh.ex create mode 100644 lib/mix/tasks/mobilizon/relay/unfollow.ex delete mode 100644 lib/mix/tasks/mobilizon/setup_search.ex delete mode 100644 lib/mix/tasks/mobilizon/toot.ex 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/cli.ex b/lib/mobilizon/cli.ex index 969c125f0..91d105d3d 100644 --- a/lib/mobilizon/cli.ex +++ b/lib/mobilizon/cli.ex @@ -1,11 +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 - @app :mobilizon + @moduledoc """ + CLI wrapper for releases + """ + alias Mix.Tasks.Mobilizon.Ecto.{Migrate, Rollback} - def migrate do - Application.load(@app) + def run(args) do + [task | args] = String.split(args) - for repo <- Application.fetch_env!(@app, :ecto_repos) do - {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) + 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/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