From 8d943f950f87e11fde3d16a6de4a0a0fb546168d Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 3 Jan 2019 11:34:31 +0100 Subject: [PATCH] New generate config task from Pleroma upstream & move tasks namespace Little fixes and tests Signed-off-by: Thomas Citharel --- lib/mix/tasks/generate_config.ex | 121 ------------- lib/mix/tasks/mobilizon/common.ex | 28 +++ lib/mix/tasks/{ => mobilizon}/create_bot.ex | 2 +- lib/mix/tasks/mobilizon/instance.ex | 171 ++++++++++++++++++ lib/mix/tasks/mobilizon/toot.ex | 20 ++ lib/mix/tasks/toot.ex | 15 -- lib/mobilizon_web/api/comments.ex | 3 +- .../activity_pub/transmogrifier_test.exs | 3 +- .../controllers/webfinger_controller_test.exs | 4 +- .../resolvers/comment_resolver_test.exs | 11 +- 10 files changed, 229 insertions(+), 149 deletions(-) delete mode 100644 lib/mix/tasks/generate_config.ex create mode 100644 lib/mix/tasks/mobilizon/common.ex rename lib/mix/tasks/{ => mobilizon}/create_bot.ex (94%) create mode 100644 lib/mix/tasks/mobilizon/instance.ex create mode 100644 lib/mix/tasks/mobilizon/toot.ex delete mode 100644 lib/mix/tasks/toot.ex diff --git a/lib/mix/tasks/generate_config.ex b/lib/mix/tasks/generate_config.ex deleted file mode 100644 index acb001787..000000000 --- a/lib/mix/tasks/generate_config.ex +++ /dev/null @@ -1,121 +0,0 @@ -defmodule Mix.Tasks.GenerateConfig do - use Mix.Task - - @moduledoc """ - Generate a new config - - ## Usage - ``mix generate_config`` - - This mix task is interactive, and will overwrite the environment file present at ``.env.production``. - - Inspired from Pleroma own generate_config task - """ - def run(_) do - IO.puts("Answer a few questions to generate a new config\n") - - override = - if File.exists?(".env.production") do - confirm("You already have an .env.production file, do you want to override it?") - else - nil - end - - if override == true do - IO.puts("\n--- THIS WILL OVERWRITE YOUR .env.production file! ---\n") - end - - if override != false do - domain = string_required("What is your domain name? (e.g. framameet.org): ") - name = string_required("What is the name of your instance? (e.g. Framameet): ") - email = email("What's your admin email address: ") - - if confirm("Is everything okay?") do - do_generate(domain, name, email) - else - IO.puts("\nYou cancelled installation\n") - end - else - IO.puts("\nYou cancelled installation\n") - end - end - - defp do_generate(domain, name, email) do - secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) - - # Try to avoid issues with some special caracters using url_encode64() - dbpass = :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64) - - resultSql = EEx.eval_file("support/postgresql/setup_db.psql", database_password: dbpass) - - result = - EEx.eval_file( - ".env.production.sample", - instance_domain: domain, - instance_name: name, - instance_email: email, - instance_secret: secret, - database_password: dbpass - ) - - IO.puts("\nWriting config to .env.production.\n\nCheck it and configure your database.") - - File.write(".env.production", result) - - IO.puts(""" - \nWriting setup_db.psql, please run it as postgres superuser, i.e.: sudo su postgres -c 'psql -f setup_db.psql'\n - You may delete the setup_db.psql file once it has been executed. - """) - - File.write("setup_db.psql", resultSql) - end - - # Taken from ex_prompt - @spec confirm(String.t()) :: boolean() - defp confirm(prompt) do - answer = - String.trim(prompt) - |> Kernel.<>(" [Yn] ") - |> string() - |> String.downcase() - - cond do - answer in ~w(yes y) -> true - answer in ~w(no n) -> false - true -> confirm(prompt) - end - end - - # Taken from ex_prompt - @spec string(String.t()) :: String.t() - defp string(prompt) do - case IO.gets(prompt) do - :eof -> "" - {:error, _reason} -> "" - str -> String.trim_trailing(str) - end - end - - # Taken from ex_prompt - @spec string_required(String.t()) :: String.t() - defp string_required(prompt) do - case string(prompt) do - "" -> string_required(prompt) - str -> str - end - end - - @spec email(String.t(), boolean()) :: String.t() - defp email(prompt, required \\ true) do - email_value = - case required do - true -> string_required(prompt) - _ -> string(prompt) - end - - case Mobilizon.Service.EmailChecker.valid?(email_value) do - false -> email(prompt, required) - _ -> email_value - end - end -end diff --git a/lib/mix/tasks/mobilizon/common.ex b/lib/mix/tasks/mobilizon/common.ex new file mode 100644 index 000000000..cb18bcbd2 --- /dev/null +++ b/lib/mix/tasks/mobilizon/common.ex @@ -0,0 +1,28 @@ +# Portions of this file are derived from Pleroma: +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/mix/tasks/pleroma/common.ex + +defmodule Mix.Tasks.Mobilizon.Common do + @moduledoc "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 -> + opt |> String.trim() + end + end + + def escape_sh_path(path) do + ~S(') <> String.replace(path, ~S('), ~S(\')) <> ~S(') + end +end diff --git a/lib/mix/tasks/create_bot.ex b/lib/mix/tasks/mobilizon/create_bot.ex similarity index 94% rename from lib/mix/tasks/create_bot.ex rename to lib/mix/tasks/mobilizon/create_bot.ex index 56975a584..510139aab 100644 --- a/lib/mix/tasks/create_bot.ex +++ b/lib/mix/tasks/mobilizon/create_bot.ex @@ -1,4 +1,4 @@ -defmodule Mix.Tasks.CreateBot do +defmodule Mix.Tasks.Mobilizon.CreateBot do @moduledoc """ Creates a bot from a source """ diff --git a/lib/mix/tasks/mobilizon/instance.ex b/lib/mix/tasks/mobilizon/instance.ex new file mode 100644 index 000000000..0f6c5d870 --- /dev/null +++ b/lib/mix/tasks/mobilizon/instance.ex @@ -0,0 +1,171 @@ +# Portions of this file are derived from Pleroma: +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/mix/tasks/pleroma/instance.ex + +defmodule Mix.Tasks.Mobilizon.Instance do + use Mix.Task + alias Mix.Tasks.Mobilizon.Common + + @shortdoc "Generates a new config" + @preferred_cli_env "prod" + @moduledoc """ + Generates a new config + + ## Usage + ``mix mobilizon.instance gen [OPTION...]`` + + If any options are left unspecified, you will be prompted interactively. + + ## 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(["gen" | options]) do + {options, [], []} = + OptionParser.parse( + options, + strict: [ + force: :boolean, + output: :string, + output_psql: :string, + domain: :string, + instance_name: :string, + admin_email: :string, + dbhost: :string, + dbname: :string, + dbuser: :string, + dbpass: :string, + dbport: :integer + ], + aliases: [ + o: :output, + f: :force + ] + ) + + paths = + [config_path, psql_path] = [ + Keyword.get(options, :output, ".env"), + Keyword.get(options, :output_psql, "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, port | _] = + String.split( + Common.get_option( + options, + :domain, + "What domain will your instance use? (e.g framameet.org)" + ), + ":" + ) ++ [443] + + name = + Common.get_option( + options, + :name, + "What is the name of your instance? (e.g. Framameet)" + ) + + email = Common.get_option(options, :admin_email, "What is your admin email address?") + + dbhost = + Common.get_option(options, :dbhost, "What is the hostname of your database?", "localhost") + + dbname = + Common.get_option( + options, + :dbname, + "What is the name of your database?", + "mobilizon_prod" + ) + + dbuser = + Common.get_option( + options, + :dbuser, + "What is the user used to connect to your database?", + "mobilizon" + ) + + dbpass = + Common.get_option( + options, + :dbpass, + "What is the password used to connect to your database?", + :crypto.strong_rand_bytes(64) |> Base.url_encode64() |> binary_part(0, 64), + "autogenerated" + ) + + secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) + + result_config = + EEx.eval_file( + ".env.sample" |> Path.expand(__DIR__ <> "../../../../../"), + instance_domain: domain, + instance_port: port, + instance_email: email, + instance_name: name, + database_host: dbhost, + database_name: dbname, + database_port: Keyword.get(options, :dbport, 5432), + database_username: dbuser, + database_password: dbpass, + version: Mobilizon.Mixfile.project() |> Keyword.get(:version), + instance_secret: secret + ) + + result_psql = + EEx.eval_file( + "support/postgresql/setup_db.psql" |> Path.expand(__DIR__ <> "../../../../../"), + database_name: dbname, + database_username: dbuser, + database_password: dbpass + ) + + Mix.shell().info( + "Writing config to #{config_path}. You should rename it to .env.production, .env.dev or .env.test" + ) + + 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. 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) + }`. + """ <> + if config_path in [".env.production", ".env.dev", ".env.test"] do + "" + else + "3. Run `mv #{Common.escape_sh_path(config_path)} '.env.production'`." + end + ) + else + Mix.shell().error( + "The task would have overwritten the following files:\n" <> + (Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <> + "Rerun with `-f/--force` to overwrite them." + ) + end + end +end diff --git a/lib/mix/tasks/mobilizon/toot.ex b/lib/mix/tasks/mobilizon/toot.ex new file mode 100644 index 000000000..9c0d9094c --- /dev/null +++ b/lib/mix/tasks/mobilizon/toot.ex @@ -0,0 +1,20 @@ +defmodule Mix.Tasks.Mobilizon.Toot do + @moduledoc """ + Creates a bot from a source + """ + + use Mix.Task + require Logger + + @shortdoc "Toot to an user" + def run([from, content]) do + Mix.Task.run("app.start") + + with {:ok, _} <- MobilizonWeb.API.Comments.create_comment(from, content) 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/toot.ex b/lib/mix/tasks/toot.ex deleted file mode 100644 index ea3e268c0..000000000 --- a/lib/mix/tasks/toot.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Mix.Tasks.Toot do - @moduledoc """ - Creates a bot from a source - """ - - use Mix.Task - require Logger - - @shortdoc "Toot to an user" - def run([from, content]) do - Mix.Task.run("app.start") - - MobilizonWeb.API.Comments.create_comment(from, content) - end -end diff --git a/lib/mobilizon_web/api/comments.ex b/lib/mobilizon_web/api/comments.ex index 4446b6811..c1e3d40af 100644 --- a/lib/mobilizon_web/api/comments.ex +++ b/lib/mobilizon_web/api/comments.ex @@ -18,7 +18,8 @@ defmodule MobilizonWeb.API.Comments do """ @spec create_comment(String.t(), String.t(), String.t()) :: {:ok, Activity.t()} | any() def create_comment(from_username, status, visibility \\ "public", inReplyToCommentURL \\ nil) do - with %Actor{url: url} = actor <- Actors.get_local_actor_by_name(from_username), + with {:local_actor, %Actor{url: url} = actor} <- + {:local_actor, Actors.get_local_actor_by_name(from_username)}, status <- String.trim(status), mentions <- Formatter.parse_mentions(status), inReplyToComment <- get_in_reply_to_comment(inReplyToCommentURL), diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs index 1ad689db8..ce269a15f 100644 --- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs +++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs @@ -729,8 +729,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do {:ok, activity} = MobilizonWeb.API.Comments.create_comment(actor.preferred_username, "hey") {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - assert modified["@context"] == - Mobilizon.Service.ActivityPub.Utils.make_json_ld_header()["@context"] + assert modified["@context"] == Utils.make_json_ld_header()["@context"] end test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do diff --git a/test/mobilizon_web/controllers/webfinger_controller_test.exs b/test/mobilizon_web/controllers/webfinger_controller_test.exs index 37b41bef8..e97db6f4c 100644 --- a/test/mobilizon_web/controllers/webfinger_controller_test.exs +++ b/test/mobilizon_web/controllers/webfinger_controller_test.exs @@ -22,12 +22,12 @@ defmodule MobilizonWeb.WebFingerTest do test "GET /.well-known/webfinger with local actor", %{conn: conn} do %Actor{preferred_username: username} = actor = insert(:actor) - conn = get(conn, "/.well-known/webfinger?resource=acct:#{username}@localhost:4001") + conn = get(conn, "/.well-known/webfinger?resource=acct:#{username}@mobilizon.test") assert json_response(conn, 200) == WebFinger.represent_actor(actor) end test "GET /.well-known/webfinger with non existent actor", %{conn: conn} do - conn = get(conn, "/.well-known/webfinger?resource=acct:notme@localhost:4001") + conn = get(conn, "/.well-known/webfinger?resource=acct:notme@mobilizon.test") assert response(conn, 404) == "Couldn't find user" end diff --git a/test/mobilizon_web/resolvers/comment_resolver_test.exs b/test/mobilizon_web/resolvers/comment_resolver_test.exs index 654eae0a0..7ddc3b612 100644 --- a/test/mobilizon_web/resolvers/comment_resolver_test.exs +++ b/test/mobilizon_web/resolvers/comment_resolver_test.exs @@ -1,11 +1,10 @@ defmodule MobilizonWeb.Resolvers.CommentResolverTest do use MobilizonWeb.ConnCase - alias Mobilizon.{Events, Actors} + alias Mobilizon.Actors alias Mobilizon.Actors.{Actor, User} alias MobilizonWeb.AbsintheHelpers - import Mobilizon.Factory - @comment %{text: "some body"} + @comment %{text: "I love this event"} setup %{conn: conn} do {:ok, %User{default_actor: %Actor{} = actor} = user} = @@ -16,12 +15,10 @@ defmodule MobilizonWeb.Resolvers.CommentResolverTest do describe "Comment Resolver" do test "create_comment/3 creates a comment", %{conn: conn, actor: actor, user: user} do - category = insert(:category) - mutation = """ mutation { createComment( - text: "I love this event", + text: "#{@comment.text}", actor_username: "#{actor.preferred_username}" ) { text, @@ -35,7 +32,7 @@ defmodule MobilizonWeb.Resolvers.CommentResolverTest do |> auth_conn(user) |> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) - assert json_response(res, 200)["data"]["createComment"]["text"] == "I love this event" + assert json_response(res, 200)["data"]["createComment"]["text"] == @comment.text end end end