Allow to use Mix tasks inside Releases

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
master
Thomas Citharel 2 years ago committed by prichier
parent a269d77044
commit 01f746a5d2
  1. 10
      lib/mix/tasks/mobilizon/actors.ex
  2. 13
      lib/mix/tasks/mobilizon/actors/refresh.ex
  3. 9
      lib/mix/tasks/mobilizon/actors/show.ex
  4. 107
      lib/mix/tasks/mobilizon/common.ex
  5. 3
      lib/mix/tasks/mobilizon/create_bot.ex
  6. 54
      lib/mix/tasks/mobilizon/ecto.ex
  7. 71
      lib/mix/tasks/mobilizon/ecto/migrate.ex
  8. 74
      lib/mix/tasks/mobilizon/ecto/rollback.ex
  9. 3
      lib/mix/tasks/mobilizon/groups.ex
  10. 5
      lib/mix/tasks/mobilizon/groups/refresh.ex
  11. 31
      lib/mix/tasks/mobilizon/instance.ex
  12. 68
      lib/mix/tasks/mobilizon/move_participant_stats.ex
  13. 59
      lib/mix/tasks/mobilizon/relay.ex
  14. 28
      lib/mix/tasks/mobilizon/relay/accept.ex
  15. 28
      lib/mix/tasks/mobilizon/relay/follow.ex
  16. 28
      lib/mix/tasks/mobilizon/relay/refresh.ex
  17. 28
      lib/mix/tasks/mobilizon/relay/unfollow.ex
  18. 50
      lib/mix/tasks/mobilizon/setup_search.ex
  19. 6
      lib/mix/tasks/mobilizon/site_map.ex
  20. 30
      lib/mix/tasks/mobilizon/toot.ex
  21. 10
      lib/mix/tasks/mobilizon/users.ex
  22. 13
      lib/mix/tasks/mobilizon/users/delete.ex
  23. 21
      lib/mix/tasks/mobilizon/users/modify.ex
  24. 15
      lib/mix/tasks/mobilizon/users/new.ex
  25. 10
      lib/mix/tasks/mobilizon/users/show.ex
  26. 52
      lib/mobilizon/cli.ex
  27. 4
      test/tasks/actors_test.exs
  28. 7
      test/tasks/relay_test.exs
  29. 28
      test/tasks/users_test.exs

@ -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

@ -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()

@ -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

@ -8,32 +8,107 @@ defmodule Mix.Tasks.Mobilizon.Common do
Common functions to be reused in mix tasks
"""
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
display = if defname || defval, do: "#{prompt} [#{defname || defval}]", else: "#{prompt}"
Keyword.get(options, opt) || shell_prompt(prompt, defval, defname)
end
def shell_prompt(prompt, defval \\ nil, defname \\ nil) do
prompt_message = "#{prompt} [#{defname || defval}] "
Keyword.get(options, opt) ||
case Mix.shell().prompt(display) do
"\n" ->
case defval do
nil ->
get_option(options, opt, prompt, defval)
input =
if mix_shell?(),
do: Mix.shell().prompt(prompt_message),
else: :io.get_line(prompt_message)
defval ->
defval
end
case input do
"\n" ->
case defval do
nil ->
shell_prompt(prompt, defval, defname)
opt ->
String.trim(opt)
end
defval ->
defval
end
input ->
String.trim(input)
end
end
def start_mobilizon do
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
def shell_yes?(message) do
if mix_shell?(),
do: Mix.shell().yes?("Continue?"),
else: shell_prompt(message, "Continue?") in ~w(Yn Y y)
end
{:ok, _} = Application.ensure_all_started(:mobilizon)
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

@ -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}),

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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."

@ -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

@ -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)
{: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}")
@impl Mix.Task
def run(_) do
shell_info("\nAvailable tasks:")
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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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()

@ -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

@ -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: ""

@ -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
@app :mobilizon
@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)
def migrate do
Application.load(@app)
module =
Enum.find(modules, fn module ->
module = Module.split(module)
for repo <- Application.fetch_env!(@app, :ecto_repos) do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
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

@ -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

@ -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