From 8a1dc0de92c8c99bd6216281355be829cbcfb21f Mon Sep 17 00:00:00 2001 From: Jorty Date: Thu, 28 Jun 2018 20:24:51 -0400 Subject: [PATCH] Refactor Mix tasks 1. Move Mix tasks into a `pleroma` namespace, to avoid collisions with dependent packages. 2. Rename and condense tasks into two `pleroma.user` and `pleroma.gen.instance` tasks for consistency with Hex and Phoenix. 3. Add additional functionality to the tasks to make them more user-friendly. Arguments with sensible defaults were demoted to flags and in the interactive `generate_config` (renamed to `pleroma.gen.instance`), flags were added to allow non-interactive use, though interactive use remains the primary interface. That task also now prompts the user for database parameters. 4. Documentation has been added to both tasks such that `mix help` now shows useful information. 5. Finally, use of IO.puts in tasks has been replaced with Mix.shell() equivalents to make the behavior more consistent with Mix tasks in other packages, and such that variables like MIX_QUIET are respected. The only exception is in `mix pleroma.user reset_password`, wherein the URL must always be printed regardless of the value of MIX_QUIET since that's its entire purpose. --- lib/mix/tasks/deactivate_user.ex | 13 -- lib/mix/tasks/fix_ap_users.ex | 28 --- lib/mix/tasks/generate_config.ex | 39 ---- lib/mix/tasks/generate_password_reset.ex | 27 --- lib/mix/tasks/make_moderator.ex | 30 --- lib/mix/tasks/pleroma/gen_instance.ex | 161 ++++++++++++++ lib/mix/tasks/{ => pleroma}/sample_config.eex | 12 +- lib/mix/tasks/pleroma/sample_psql.eex | 9 + lib/mix/tasks/pleroma/user.ex | 207 ++++++++++++++++++ lib/mix/tasks/register_user.ex | 22 -- lib/mix/tasks/rm_user.ex | 13 -- lib/mix/tasks/sample_psql.eex | 9 - lib/mix/tasks/set_locked.ex | 30 --- 13 files changed, 385 insertions(+), 215 deletions(-) delete mode 100644 lib/mix/tasks/deactivate_user.ex delete mode 100644 lib/mix/tasks/fix_ap_users.ex delete mode 100644 lib/mix/tasks/generate_config.ex delete mode 100644 lib/mix/tasks/generate_password_reset.ex delete mode 100644 lib/mix/tasks/make_moderator.ex create mode 100644 lib/mix/tasks/pleroma/gen_instance.ex rename lib/mix/tasks/{ => pleroma}/sample_config.eex (68%) create mode 100644 lib/mix/tasks/pleroma/sample_psql.eex create mode 100644 lib/mix/tasks/pleroma/user.ex delete mode 100644 lib/mix/tasks/register_user.ex delete mode 100644 lib/mix/tasks/rm_user.ex delete mode 100644 lib/mix/tasks/sample_psql.eex delete mode 100644 lib/mix/tasks/set_locked.ex diff --git a/lib/mix/tasks/deactivate_user.ex b/lib/mix/tasks/deactivate_user.ex deleted file mode 100644 index 96b3db6e4..000000000 --- a/lib/mix/tasks/deactivate_user.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Mix.Tasks.DeactivateUser do - use Mix.Task - alias Pleroma.User - - @shortdoc "Toggle deactivation status for a user" - def run([nickname]) do - Mix.Task.run("app.start") - - with user <- User.get_by_nickname(nickname) do - User.deactivate(user) - end - end -end diff --git a/lib/mix/tasks/fix_ap_users.ex b/lib/mix/tasks/fix_ap_users.ex deleted file mode 100644 index 7e970850e..000000000 --- a/lib/mix/tasks/fix_ap_users.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Mix.Tasks.FixApUsers do - use Mix.Task - import Ecto.Query - alias Pleroma.{Repo, User} - - @shortdoc "Grab all ap users again" - def run([]) do - Mix.Task.run("app.start") - - q = - from( - u in User, - where: fragment("? @> ?", u.info, ^%{"ap_enabled" => true}), - where: u.local == false - ) - - users = Repo.all(q) - - Enum.each(users, fn user -> - try do - IO.puts("Fetching #{user.nickname}") - Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(user.ap_id, false) - rescue - e -> IO.inspect(e) - end - end) - end -end diff --git a/lib/mix/tasks/generate_config.ex b/lib/mix/tasks/generate_config.ex deleted file mode 100644 index 70a110561..000000000 --- a/lib/mix/tasks/generate_config.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Mix.Tasks.GenerateConfig do - use Mix.Task - - @shortdoc "Generates a new config" - def run(_) do - IO.puts("Answer a few questions to generate a new config\n") - IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n") - domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim() - name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim() - email = IO.gets("What's your admin email address: ") |> String.trim() - - secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) - dbpass = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) - - resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", dbpass: dbpass) - - result = - EEx.eval_file( - "lib/mix/tasks/sample_config.eex", - domain: domain, - email: email, - name: name, - secret: secret, - dbpass: dbpass - ) - - IO.puts( - "\nWriting config to config/generated_config.exs.\n\nCheck it and configure your database, then copy it to either config/dev.secret.exs or config/prod.secret.exs" - ) - - File.write("config/generated_config.exs", result) - - IO.puts( - "\nWriting setup_db.psql, please run it as postgre superuser, i.e.: sudo su postgres -c 'psql -f config/setup_db.psql'" - ) - - File.write("config/setup_db.psql", resultSql) - end -end diff --git a/lib/mix/tasks/generate_password_reset.ex b/lib/mix/tasks/generate_password_reset.ex deleted file mode 100644 index 6bf640150..000000000 --- a/lib/mix/tasks/generate_password_reset.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Mix.Tasks.GeneratePasswordReset do - use Mix.Task - alias Pleroma.User - - @shortdoc "Generate password reset link for user" - def run([nickname]) do - Mix.Task.run("app.start") - - with %User{local: true} = user <- User.get_by_nickname(nickname), - {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do - IO.puts("Generated password reset token for #{user.nickname}") - - IO.puts( - "Url: #{ - Pleroma.Web.Router.Helpers.util_url( - Pleroma.Web.Endpoint, - :show_password_reset, - token.token - ) - }" - ) - else - _ -> - IO.puts("No local user #{nickname}") - end - end -end diff --git a/lib/mix/tasks/make_moderator.ex b/lib/mix/tasks/make_moderator.ex deleted file mode 100644 index a454a958e..000000000 --- a/lib/mix/tasks/make_moderator.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Mix.Tasks.SetModerator do - use Mix.Task - import Mix.Ecto - alias Pleroma.{Repo, User} - - @shortdoc "Set moderator status" - def run([nickname | rest]) do - Application.ensure_all_started(:pleroma) - - moderator = - case rest do - [moderator] -> moderator == "true" - _ -> true - end - - with %User{local: true} = user <- User.get_by_nickname(nickname) do - info = - user.info - |> Map.put("is_moderator", !!moderator) - - cng = User.info_changeset(user, %{info: info}) - {:ok, user} = User.update_and_set_cache(cng) - - IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}") - else - _ -> - IO.puts("No local user #{nickname}") - end - end -end diff --git a/lib/mix/tasks/pleroma/gen_instance.ex b/lib/mix/tasks/pleroma/gen_instance.ex new file mode 100644 index 000000000..94f2220b1 --- /dev/null +++ b/lib/mix/tasks/pleroma/gen_instance.ex @@ -0,0 +1,161 @@ +defmodule Mix.Tasks.Pleroma.Gen.Instance do + use Mix.Task + + @shortdoc "Generates the configuration for a new instance" + @moduledoc """ + Generates the configuration for a new instance. + + If any options are left unspecified, you will be prompted interactively. This + means the simplest invocation would be + + mix pleroma.gen.instance + + ## Options + + - `-f`, `--force` - overwrite any output files + - `-o PATH`, `--output PATH` - the output file for the generated configuration + - `--output-psql PATH` - the output file for the generated PostgreSQL setup + - `--domain DOMAIN` - the domain of your instance + - `--instance-name INSTANCE_NAME` - the name of your instance + - `--admin-email ADMIN_EMAIL` - the email address of the instance admin + - `--dbhost HOSTNAME` - the hostname of the PostgreSQL database to use + - `--dbname DBNAME` - the name of the database to use + - `--dbuser DBUSER` - the user (aka role) to use for the database connection + - `--dbpass DBPASS` - the password to use for the database connection + """ + + def run(rest) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [ + force: :boolean, + output: :string, + output_psql: :string, + domain: :string, + instance_name: :string, + admin_email: :string, + dbhost: :string, + dbname: :string, + dbuser: :string, + dbpass: :string + ], + aliases: [ + o: :output, + f: :force + ] + ) + + paths = + [config_path, psql_path] = [ + Keyword.get(options, :output, "config/generated_config.exs"), + Keyword.get(options, :output_psql, "config/setup_db.psql") + ] + + will_overwrite = Enum.filter(paths, &File.exists?/1) + proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false) + + unless not proceed? do + domain = + Keyword.get(options, :domain) || + Mix.shell().prompt("What domain will your instance use? (e.g. pleroma.soykaf.com)") + |> String.trim() + + name = + Keyword.get(options, :name) || + Mix.shell().prompt("What is the name of your instance? (e.g. Pleroma/Soykaf)") + |> String.trim() + + email = + Keyword.get(options, :admin_email) || + Mix.shell().prompt("What is your admin email address?") + |> String.trim() + + dbhost = + Keyword.get(options, :dbhost) || + case Mix.shell().prompt("What is the hostname of your database? [localhost]") do + "\n" -> "localhost" + dbhost -> dbhost |> String.trim() + end + + dbname = + Keyword.get(options, :dbname) || + case Mix.shell().prompt("What is the name of your database? [pleroma_dev]") do + "\n" -> "pleroma_dev" + dbname -> dbname |> String.trim() + end + + dbuser = + Keyword.get(options, :dbuser) || + case Mix.shell().prompt("What is the user used to connect to your database? [pleroma]") do + "\n" -> "pleroma" + dbuser -> dbuser |> String.trim() + end + + dbpass = + Keyword.get(options, :dbpass) || + case Mix.shell().prompt( + "What is the password used to connect to your database? [autogenerated]" + ) do + "\n" -> :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) + dbpass -> dbpass |> String.trim() + end + + secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) + + result_config = + EEx.eval_file( + "sample_config.eex" |> Path.expand(__DIR__), + domain: domain, + email: email, + name: name, + dbhost: dbhost, + dbname: dbname, + dbuser: dbuser, + dbpass: dbpass, + version: Pleroma.Mixfile.project() |> Keyword.get(:version), + secret: secret + ) + + result_psql = + EEx.eval_file( + "sample_psql.eex" |> Path.expand(__DIR__), + dbname: dbname, + dbuser: dbuser, + dbpass: dbpass + ) + + Mix.shell().info( + "Writing config to #{config_path}. You should rename it to config/prod.secret.exs or config/dev.secret.exs." + ) + + File.write(config_path, result_config) + Mix.shell().info("Writing #{psql_path}.") + File.write(psql_path, result_psql) + + Mix.shell().info( + "\n" <> + """ + To get started: + 1. Verify the contents of the generated files. + 2. Run `sudo -u postgres psql -f #{escape_sh_path(psql_path)}`. + """ <> + if config_path in ["config/dev.secret.exs", "config/prod.secret.exs"] do + "" + else + "3. Run `mv #{escape_sh_path(config_path)} 'config/prod.secret.exs'`." + end + ) + else + Mix.shell().error( + "The task would have overwritten the following files:\n" <> + (Enum.map(paths, &"- #{&1}\n") |> Enum.join("")) <> + "Rerun with `--force` to overwrite them." + ) + end + end + + defp escape_sh_path(path) do + ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') + end +end diff --git a/lib/mix/tasks/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex similarity index 68% rename from lib/mix/tasks/sample_config.eex rename to lib/mix/tasks/pleroma/sample_config.eex index 6db36fa09..066939981 100644 --- a/lib/mix/tasks/sample_config.eex +++ b/lib/mix/tasks/pleroma/sample_config.eex @@ -1,3 +1,8 @@ +# Pleroma instance configuration + +# NOTE: This file should not be committed to a repo or otherwise made public +# without removing sensitive information. + use Mix.Config config :pleroma, Pleroma.Web.Endpoint, @@ -16,11 +21,10 @@ config :pleroma, :media_proxy, redirect_on_failure: true #base_url: "https://cache.pleroma.social" -# Configure your database config :pleroma, Pleroma.Repo, adapter: Ecto.Adapters.Postgres, - username: "pleroma", + username: "<%= dbuser %>", password: "<%= dbpass %>", - database: "pleroma_dev", - hostname: "localhost", + database: "<%= dbname %>", + hostname: "<%= dbhost %>", pool_size: 10 diff --git a/lib/mix/tasks/pleroma/sample_psql.eex b/lib/mix/tasks/pleroma/sample_psql.eex new file mode 100644 index 000000000..66f76752f --- /dev/null +++ b/lib/mix/tasks/pleroma/sample_psql.eex @@ -0,0 +1,9 @@ +CREATE USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB; +-- in case someone runs this second time accidentally +ALTER USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB; +CREATE DATABASE <%= dbname %>; +ALTER DATABASE <%= dbname %> OWNER TO <%= dbuser %>; +\c <%= dbname %>; +--Extensions made by ecto.migrate that need superuser access +CREATE EXTENSION IF NOT EXISTS citext; +CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex new file mode 100644 index 000000000..c20fecaa1 --- /dev/null +++ b/lib/mix/tasks/pleroma/user.ex @@ -0,0 +1,207 @@ +defmodule Mix.Tasks.Pleroma.User do + use Mix.Task + alias Pleroma.{Repo, User} + + @shortdoc "Manages Pleroma users" + @moduledoc """ + Manages Pleroma users. + + ## Create a new user. + + mix pleroma.user new NICKNAME EMAIL [OPTION...] + + Options: + - `--name NAME` - the user's name (i.e., "Lain Iwakura") + - `--bio BIO` - the user's bio + - `--password PASSWORD` - the user's password + - `--moderator`/`--no-moderator` - whether the user is a moderator + + ## Delete the user's account. + + mix pleroma.user rm NICKNAME + + ## Deactivate or activate the user's account. + + mix pleroma.user toggle_activated NICKNAME + + ## Create a password reset link. + + mix pleroma.user reset_password NICKNAME + + ## Set the value of the given user's settings. + + mix pleroma.user set NICKNAME [OPTION...] + + Options: + - `--locked`/`--no-locked` - whether the user's account is locked + - `--moderator`/`--no-moderator` - whether the user is a moderator + """ + + def run(["new", nickname, email | rest]) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [ + name: :string, + bio: :string, + password: :string, + moderator: :boolean + ] + ) + + name = Keyword.get(options, :name, nickname) + bio = Keyword.get(options, :bio, "") + + {password, generated_password?} = + case Keyword.get(options, :password) do + nil -> + {:crypto.strong_rand_bytes(16) |> Base.encode64(), true} + + password -> + {password, false} + end + + moderator? = Keyword.get(options, :moderator, false) + + Mix.shell().info(""" + A user will be created with the following information: + - nickname: #{nickname} + - email: #{email} + - password: #{ + if(generated_password?, do: "[generated; a reset link will be created]", else: password) + } + - name: #{name} + - bio: #{bio} + - moderator: #{if(moderator?, do: "true", else: "false")} + """) + + proceed? = Mix.shell().yes?("Continue?") + + unless not proceed? do + Mix.Task.run("app.start") + + params = + %{ + nickname: nickname, + email: email, + password: password, + password_confirmation: password, + name: name, + bio: bio + } + |> IO.inspect() + + user = User.register_changeset(%User{}, params) + Repo.insert!(user) + + Mix.shell().info("User #{nickname} created") + + if moderator? do + run(["set", nickname, "--moderator"]) + end + + if generated_password? do + run(["reset_password", nickname]) + end + else + Mix.shell().info("User will not be created.") + end + end + + def run(["rm", nickname]) do + Mix.Task.run("app.start") + + with %User{local: true} = user <- User.get_by_nickname(nickname) do + User.delete(user) + end + + Mix.shell().info("User #{nickname} deleted.") + end + + def run(["toggle_activated", nickname]) do + Mix.Task.run("app.start") + + with user <- User.get_by_nickname(nickname) do + User.deactivate(user) + end + end + + def run(["reset_password", nickname]) do + Mix.Task.run("app.start") + + with %User{local: true} = user <- User.get_by_nickname(nickname), + {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do + Mix.shell().info("Generated password reset token for #{user.nickname}") + + IO.puts( + "URL: #{ + Pleroma.Web.Router.Helpers.util_url( + Pleroma.Web.Endpoint, + :show_password_reset, + token.token + ) + }" + ) + else + _ -> + Mix.shell().error("No local user #{nickname}") + end + end + + def run(["set", nickname | rest]) do + {options, [], []} = + OptionParser.parse( + rest, + strict: [ + moderator: :boolean, + locked: :boolean + ] + ) + + case Keyword.get(options, :moderator) do + nil -> nil + value -> set_moderator(nickname, value) + end + + case Keyword.get(options, :locked) do + nil -> nil + value -> set_locked(nickname, value) + end + end + + defp set_moderator(nickname, value) do + Application.ensure_all_started(:pleroma) + + with %User{local: true} = user <- User.get_by_nickname(nickname) do + info = + user.info + |> Map.put("is_moderator", value) + + cng = User.info_changeset(user, %{info: info}) + {:ok, user} = User.update_and_set_cache(cng) + + Mix.shell().info("Moderator status of #{nickname}: #{user.info["is_moderator"]}") + else + _ -> + Mix.shell().error("No local user #{nickname}") + end + end + + defp set_locked(nickname, value) do + Mix.Ecto.ensure_started(Repo, []) + + with %User{local: true} = user <- User.get_by_nickname(nickname) do + info = + user.info + |> Map.put("locked", value) + + cng = User.info_changeset(user, %{info: info}) + user = Repo.update!(cng) + + IO.puts("Locked status of #{nickname}: #{user.info["locked"]}") + else + _ -> + IO.puts("No local user #{nickname}") + end + end +end diff --git a/lib/mix/tasks/register_user.ex b/lib/mix/tasks/register_user.ex deleted file mode 100644 index e74721c49..000000000 --- a/lib/mix/tasks/register_user.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Mix.Tasks.RegisterUser do - use Mix.Task - alias Pleroma.{Repo, User} - - @shortdoc "Register user" - def run([name, nickname, email, bio, password]) do - Mix.Task.run("app.start") - - params = %{ - name: name, - nickname: nickname, - email: email, - password: password, - password_confirmation: password, - bio: bio - } - - user = User.register_changeset(%User{}, params) - - Repo.insert!(user) - end -end diff --git a/lib/mix/tasks/rm_user.ex b/lib/mix/tasks/rm_user.ex deleted file mode 100644 index 27521b745..000000000 --- a/lib/mix/tasks/rm_user.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Mix.Tasks.RmUser do - use Mix.Task - alias Pleroma.User - - @shortdoc "Permanently delete a user" - def run([nickname]) do - Mix.Task.run("app.start") - - with %User{local: true} = user <- User.get_by_nickname(nickname) do - User.delete(user) - end - end -end diff --git a/lib/mix/tasks/sample_psql.eex b/lib/mix/tasks/sample_psql.eex deleted file mode 100644 index bc22f166c..000000000 --- a/lib/mix/tasks/sample_psql.eex +++ /dev/null @@ -1,9 +0,0 @@ -CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB; --- in case someone runs this second time accidentally -ALTER USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB; -CREATE DATABASE pleroma_dev; -ALTER DATABASE pleroma_dev OWNER TO pleroma; -\c pleroma_dev; ---Extensions made by ecto.migrate that need superuser access -CREATE EXTENSION IF NOT EXISTS citext; -CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/lib/mix/tasks/set_locked.ex b/lib/mix/tasks/set_locked.ex deleted file mode 100644 index 2b3b18b09..000000000 --- a/lib/mix/tasks/set_locked.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Mix.Tasks.SetLocked do - use Mix.Task - import Mix.Ecto - alias Pleroma.{Repo, User} - - @shortdoc "Set locked status" - def run([nickname | rest]) do - ensure_started(Repo, []) - - locked = - case rest do - [locked] -> locked == "true" - _ -> true - end - - with %User{local: true} = user <- User.get_by_nickname(nickname) do - info = - user.info - |> Map.put("locked", !!locked) - - cng = User.info_changeset(user, %{info: info}) - user = Repo.update!(cng) - - IO.puts("locked status of #{nickname}: #{user.info["locked"]}") - else - _ -> - IO.puts("No local user #{nickname}") - end - end -end