Allow to use Mix tasks inside Releases

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-10-30 15:16:01 +01:00 committed by prichier
parent a269d77044
commit 01f746a5d2
29 changed files with 570 additions and 301 deletions

View File

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

View File

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

View File

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

View File

@ -8,32 +8,107 @@ defmodule Mix.Tasks.Mobilizon.Common do
Common functions to be reused in mix tasks 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 def start_mobilizon do
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
{:ok, _} = Application.ensure_all_started(:mobilizon) {:ok, _} = Application.ensure_all_started(:mobilizon)
end 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 def escape_sh_path(path) do
~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(')
end 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 end

View File

@ -8,12 +8,13 @@ defmodule Mix.Tasks.Mobilizon.CreateBot do
alias Mobilizon.{Actors, Users} alias Mobilizon.{Actors, Users}
alias Mobilizon.Actors.Bot alias Mobilizon.Actors.Bot
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mix.Tasks.Mobilizon.Common
require Logger require Logger
@shortdoc "Create bot" @shortdoc "Create bot"
def run([email, name, summary, type, url]) do 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), with {:ok, %User{} = user} <- Users.get_user_by_email(email, true),
actor <- Actors.register_bot(%{name: name, summary: summary}), 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 use Mix.Task
alias Mix.Tasks alias Mix.Tasks
import Mix.Tasks.Mobilizon.Common
@shortdoc "Manages Mobilizon groups" @shortdoc "Manages Mobilizon groups"
@impl Mix.Task @impl Mix.Task
def run(_) do def run(_) do
Mix.shell().info("\nAvailable tasks:") shell_info("\nAvailable tasks:")
Tasks.Help.run(["--search", "mobilizon.groups."]) Tasks.Help.run(["--search", "mobilizon.groups."])
end end
end end

View File

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

View File

@ -29,7 +29,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
use Mix.Task use Mix.Task
alias Mix.Tasks.Mobilizon.Common import Mix.Tasks.Mobilizon.Common
@preferred_cli_env "prod" @preferred_cli_env "prod"
@ -70,7 +70,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
if proceed? do if proceed? do
[domain, port | _] = [domain, port | _] =
String.split( String.split(
Common.get_option( get_option(
options, options,
:domain, :domain,
"What domain will your instance use? (e.g mobilizon.org)" "What domain will your instance use? (e.g mobilizon.org)"
@ -79,25 +79,24 @@ defmodule Mix.Tasks.Mobilizon.Instance do
) ++ [443] ) ++ [443]
name = name =
Common.get_option( get_option(
options, options,
:instance_name, :instance_name,
"What is the name of your instance? (e.g. Mobilizon)" "What is the name of your instance? (e.g. Mobilizon)"
) )
email = email =
Common.get_option( get_option(
options, options,
:admin_email, :admin_email,
"What's the address email will be send with?", "What's the address email will be send with?",
"noreply@#{domain}" "noreply@#{domain}"
) )
dbhost = dbhost = get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost")
dbname = dbname =
Common.get_option( get_option(
options, options,
:dbname, :dbname,
"What is the name of your database?", "What is the name of your database?",
@ -105,7 +104,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
) )
dbuser = dbuser =
Common.get_option( get_option(
options, options,
:dbuser, :dbuser,
"What is the user used to connect to your database?", "What is the user used to connect to your database?",
@ -113,7 +112,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
) )
dbpass = dbpass =
Common.get_option( get_option(
options, options,
:dbpass, :dbpass,
"What is the password used to connect to your database?", "What is the password used to connect to your database?",
@ -122,7 +121,7 @@ defmodule Mix.Tasks.Mobilizon.Instance do
) )
listen_port = listen_port =
Common.get_option( get_option(
options, options,
:listen_port, :listen_port,
"What port will the app listen to (leave it if you are using the default setup with nginx)?", "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 database_password: dbpass
) )
Mix.shell().info("Writing config to #{config_path}.") shell_info("Writing config to #{config_path}.")
File.write(config_path, result_config) File.write(config_path, result_config)
Mix.shell().info("Writing #{psql_path}.") shell_info("Writing #{psql_path}.")
File.write(psql_path, result_psql) File.write(psql_path, result_psql)
Mix.shell().info( shell_info(
"\n" <> "\n" <>
""" """
To get started: To get started:
1. Check the contents of the generated files. 1. Check the contents of the generated files.
2. Run `sudo -u postgres psql -f #{Common.escape_sh_path(psql_path)} && rm #{ 2. Run `sudo -u postgres psql -f #{escape_sh_path(psql_path)} && rm #{
Common.escape_sh_path(psql_path) escape_sh_path(psql_path)
}`. }`.
""" """
) )
else else
Mix.shell().error( shell_error(
"The task would have overwritten the following files:\n" <> "The task would have overwritten the following files:\n" <>
(will_overwrite |> Enum.map(&"- #{&1}\n") |> Enum.join("")) <> (will_overwrite |> Enum.map(&"- #{&1}\n") |> Enum.join("")) <>
"Rerun with `-f/--force` to overwrite them." "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 use Mix.Task
alias Mix.Tasks.Mobilizon.Common alias Mix.Tasks
import Mix.Tasks.Mobilizon.Common
alias Mobilizon.Federation.ActivityPub.Relay
@shortdoc "Manages remote relays" @shortdoc "Manages remote relays"
def run(["follow", target]) do
Common.start_mobilizon()
case Relay.follow(target) do @impl Mix.Task
{:ok, _activity, _follow} -> def run(_) do
# put this task to sleep to allow the genserver to push out the messages shell_info("\nAvailable tasks:")
:timer.sleep(500)
{:error, e} -> if mix_shell?() do
IO.puts(:stderr, "Error while following #{target}: #{inspect(e)}") Tasks.Help.run(["--search", "mobilizon.relay."])
end else
end show_subtasks_for_module(__MODULE__)
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)}")
end end
end 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 use Mix.Task
alias Mix.Tasks.Mobilizon.Common import Mix.Tasks.Mobilizon.Common
alias Mobilizon.Service.SiteMap alias Mobilizon.Service.SiteMap
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
@ -12,10 +12,10 @@ defmodule Mix.Tasks.Mobilizon.SiteMap do
@shortdoc "Generates a new Sitemap" @shortdoc "Generates a new Sitemap"
def run(["generate"]) do def run(["generate"]) do
Common.start_mobilizon() start_mobilizon()
with {:ok, :ok} <- SiteMap.generate_sitemap() do 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 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 use Mix.Task
alias Mix.Tasks alias Mix.Tasks
import Mix.Tasks.Mobilizon.Common
@shortdoc "Manages Mobilizon users" @shortdoc "Manages Mobilizon users"
@impl Mix.Task @impl Mix.Task
def run(_) do def run(_) do
Mix.shell().info("\nAvailable tasks:") shell_info("\nAvailable tasks:")
if mix_shell?() do
Tasks.Help.run(["--search", "mobilizon.users."]) Tasks.Help.run(["--search", "mobilizon.users."])
else
show_subtasks_for_module(__MODULE__)
end
end end
end end

View File

@ -5,6 +5,7 @@ defmodule Mix.Tasks.Mobilizon.Users.Delete do
use Mix.Task use Mix.Task
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mix.Tasks.Mobilizon.Common
@shortdoc "Deletes a Mobilizon user" @shortdoc "Deletes a Mobilizon user"
@ -26,25 +27,25 @@ defmodule Mix.Tasks.Mobilizon.Users.Delete do
assume_yes? = Keyword.get(options, :assume_yes, false) assume_yes? = Keyword.get(options, :assume_yes, false)
keep_email? = Keyword.get(options, :keep_email, 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), 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} <- {:ok, %User{} = user} <-
Users.delete_user(user, reserve_email: keep_email?) do Users.delete_user(user, reserve_email: keep_email?) do
Mix.shell().info(""" shell_info("""
The user #{user.email} has been deleted The user #{user.email} has been deleted
""") """)
else else
{:error, :user_not_found} -> {: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
end end
def run(_) do 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
end end

View File

@ -3,6 +3,7 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
Task to modify an existing Mobilizon user Task to modify an existing Mobilizon user
""" """
use Mix.Task use Mix.Task
import Mix.Tasks.Mobilizon.Common
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -31,10 +32,10 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
new_email = Keyword.get(options, :email) new_email = Keyword.get(options, :email)
if disable? && enable? do 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 end
Mix.Task.run("app.start") start_mobilizon()
with {:ok, %User{} = user} <- Users.get_user_by_email(email), with {:ok, %User{} = user} <- Users.get_user_by_email(email),
attrs <- %{}, attrs <- %{},
@ -53,7 +54,7 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
), ),
{:makes_changes, true} <- {:makes_changes, attrs != %{}}, {:makes_changes, true} <- {:makes_changes, attrs != %{}},
{:ok, %User{} = user} <- Users.update_user(user, attrs) do {:ok, %User{} = user} <- Users.update_user(user, attrs) do
Mix.shell().info(""" shell_info("""
An user has been modified with the following information: An user has been modified with the following information:
- email: #{user.email} - email: #{user.email}
- Role: #{user.role} - Role: #{user.role}
@ -61,23 +62,23 @@ defmodule Mix.Tasks.Mobilizon.Users.Modify do
""") """)
else else
{:makes_changes, false} -> {:makes_changes, false} ->
Mix.shell().info("No change has been made") shell_info("No change has been made")
{:error, :user_not_found} -> {:error, :user_not_found} ->
Mix.raise("Error: No such user") shell_error("Error: No such user")
{:error, %Ecto.Changeset{errors: errors}} -> {:error, %Ecto.Changeset{errors: errors}} ->
Mix.shell().error(inspect(errors)) shell_error(inspect(errors))
Mix.raise("User has not been modified because of the above reason.") shell_error("User has not been modified because of the above reason.")
err -> err ->
Mix.shell().error(inspect(err)) shell_error(inspect(err))
Mix.raise("User has not been modified because of an unknown reason.") shell_error("User has not been modified because of an unknown reason.")
end end
end end
def run(_) do 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
@spec process_new_value(map(), atom(), any(), any()) :: map() @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 Task to create a new user
""" """
use Mix.Task use Mix.Task
import Mix.Tasks.Mobilizon.Common
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Users.User 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) :crypto.strong_rand_bytes(16) |> Base.encode64() |> binary_part(0, 16)
) )
Mix.Task.run("app.start") start_mobilizon()
case Users.register(%{ case Users.register(%{
email: email, email: email,
@ -51,7 +52,7 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
confirmation_token: nil confirmation_token: nil
}) do }) do
{:ok, %User{} = user} -> {:ok, %User{} = user} ->
Mix.shell().info(""" shell_info("""
An user has been created with the following information: An user has been created with the following information:
- email: #{user.email} - email: #{user.email}
- password: #{password} - password: #{password}
@ -60,16 +61,16 @@ defmodule Mix.Tasks.Mobilizon.Users.New do
""") """)
{:error, %Ecto.Changeset{errors: errors}} -> {:error, %Ecto.Changeset{errors: errors}} ->
Mix.shell().error(inspect(errors)) shell_error(inspect(errors))
Mix.raise("User has not been created because of the above reason.") shell_error("User has not been created because of the above reason.")
err -> err ->
Mix.shell().error(inspect(err)) shell_error(inspect(err))
Mix.raise("User has not been created because of an unknown reason.") shell_error("User has not been created because of an unknown reason.")
end end
end end
def run(_) do 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
end end

View File

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

View File

@ -1,11 +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 defmodule Mobilizon.CLI do
@app :mobilizon @moduledoc """
CLI wrapper for releases
"""
alias Mix.Tasks.Mobilizon.Ecto.{Migrate, Rollback}
def migrate do def run(args) do
Application.load(@app) [task | args] = String.split(args)
for repo <- Application.fetch_env!(@app, :ecto_repos) do case task do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) "migrate" -> migrate(args)
"rollback" -> rollback(args)
task -> mix_task(task, args)
end end
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 end

View File

@ -48,7 +48,9 @@ defmodule Mix.Tasks.Mobilizon.ActorsTest do
end end
test "show non-existing actor" do 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 end
end end

View File

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

View File

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