diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 9eb6de9..58b5c6a 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -1,50 +1,48 @@ [ - {"deps/type_check/lib/type_check/spec.ex", - "The pattern can never match the type {:ok, [], _}."}, - {"lib/plugin.ex", "Function usage_tuples/0 has no local return."}, - {"lib/plugin.ex", "Function job_result/0 has no local return."}, - {"lib/plugin.ex", "Function plugin_job_result/0 has no local return."}, - {"lib/plugin.ex", "The guard clause can never succeed."}, - {"lib/services/discord.ex", "Function vips/0 has no local return."}, - {"lib/services/dummy.ex", - "@spec for ask_bot has more types than are returned by the function."}, - {"lib/services/dummy.ex", "Function dummy_channel_id/0 has no local return."}, - {"lib/services/dummy.ex", "Function msg_content/0 has no local return."}, - {"lib/services/dummy.ex", "Function msg_reference/0 has no local return."}, - {"lib/services/dummy.ex", "Function msg_tuple/0 has no local return."}, - {"lib/services/dummy.ex", "Function msg_tuple_incoming/0 has no local return."}, - {"lib/services/dummy.ex", "Function channel/0 has no local return."}, - {"lib/services/dummy.ex", "Function channel_buffers/0 has no local return."}, - {"lib/site_config.ex", "Function server_id/0 has no local return."}, - {"lib/site_config.ex", "Function schema/0 has no local return."}, - {"lib/site_config.ex", "Function cfg_list/0 has no local return."}, - {"lib/stampede.ex", "Function dm_tuple/0 has no local return."}, - {"lib/stampede.ex", "Function server_id/0 has no local return."}, - {"lib/stampede.ex", "Function prefix/0 has no local return."}, - {"lib/stampede.ex", "Function enabled_plugs/0 has no local return."}, - {"lib/stampede.ex", "Function channel_lock_action/0 has no local return."}, - {"lib/stampede.ex", "Function channel_lock_status/0 has no local return."}, - {"lib/stampede.ex", "Function timestamp/0 has no local return."}, - {"lib/stampede.ex", "Function bot_invoked_status/0 has no local return."}, - {"lib/stampede.ex", "Function module_function_args/0 has no local return."}, - {"lib/stampede.ex", "Function log_level/0 has no local return."}, - {"lib/stampede.ex", "Function log_msg/0 has no local return."}, - {"lib/stampede.ex", "Function kwlist/1 has no local return."}, - {"lib/stampede.ex", "Function kwlist/0 has no local return."}, - {"lib/stampede.ex", "Function str_list/0 has no local return."}, - {"lib/stampede.ex", "Function throw_internal_error/0 has no local return."}, - {"lib/stampede.ex", "Function throw_internal_error/1 only terminates with explicit exception."}, - {"lib/stampede/cfg_table.ex", "Function vips/0 has no local return."}, - {"lib/stampede/cfg_table.ex", "Function table_object/0 has no local return."}, - {"lib/stampede/external/python.ex", "The pattern can never match the type true."}, - {"lib/stampede/external/python.ex", - "Function do_dumb_down_elixir_term/1 will never be called."}, - {"lib/stampede/logger.ex", "Function logger_state/0 has no local return."}, - {"lib/stampede/tables/channel_locks.ex", "The guard clause can never succeed."}, - {"lib/stampede/tables/interactions.ex", "The guard clause can never succeed."}, - {"lib/stampede/traceback.ex", "Function t/0 has no local return."}, - {"lib/txt_block.ex", "Function block/0 has no local return."}, - {"lib/txt_block.ex", "Function type/0 has no local return."}, - {"lib/txt_block.ex", "Function t/0 has no local return."}, - {"lib/txt_block/md.ex", "The guard clause can never succeed."} +{"deps/type_check/lib/type_check/spec.ex", "The pattern can never match the type {:ok, [], _}."}, +{"lib/plugin.ex", "Function usage_tuples/0 has no local return."}, +{"lib/plugin.ex", "Function job_result/0 has no local return."}, +{"lib/plugin.ex", "Function plugin_job_result/0 has no local return."}, +{"lib/plugin.ex", "The guard clause can never succeed."}, +{"lib/services/discord.ex", "Function vips/0 has no local return."}, +{"lib/services/dummy.ex", "Function dummy_channel_id/0 has no local return."}, +{"lib/services/dummy.ex", "Function dummy_server_id/0 has no local return."}, +{"lib/services/dummy.ex", "Function msg_reference/0 has no local return."}, +{"lib/services/dummy.ex", "Function incoming_msg_tuple/0 has no local return."}, +{"lib/services/dummy.ex", "Function retrieved_msg_tuple/0 has no local return."}, +{"lib/services/dummy.ex", "Function channel_log/0 has no local return."}, +{"lib/services/dummy.ex", "Function server_log/0 has no local return."}, +{"lib/services/dummy/channel.ex", "Function t/0 has no local return."}, +{"lib/site_config.ex", "Function server_id/0 has no local return."}, +{"lib/site_config.ex", "Function schema/0 has no local return."}, +{"lib/site_config.ex", "Function cfg_list/0 has no local return."}, +{"lib/stampede.ex", "Function dm_tuple/0 has no local return."}, +{"lib/stampede.ex", "Function server_id/0 has no local return."}, +{"lib/stampede.ex", "Function prefix/0 has no local return."}, +{"lib/stampede.ex", "Function enabled_plugs/0 has no local return."}, +{"lib/stampede.ex", "Function channel_lock_action/0 has no local return."}, +{"lib/stampede.ex", "Function channel_lock_status/0 has no local return."}, +{"lib/stampede.ex", "Function timestamp/0 has no local return."}, +{"lib/stampede.ex", "Function bot_invoked_status/0 has no local return."}, +{"lib/stampede.ex", "Function module_function_args/0 has no local return."}, +{"lib/stampede.ex", "Function log_level/0 has no local return."}, +{"lib/stampede.ex", "Function log_msg/0 has no local return."}, +{"lib/stampede.ex", "Function kwlist/1 has no local return."}, +{"lib/stampede.ex", "Function kwlist/0 has no local return."}, +{"lib/stampede.ex", "Function str_list/0 has no local return."}, +{"lib/stampede.ex", "Function throw_internal_error/0 has no local return."}, +{"lib/stampede.ex", "Function throw_internal_error/1 only terminates with explicit exception."}, +{"lib/stampede/cfg_table.ex", "Function vips/0 has no local return."}, +{"lib/stampede/cfg_table.ex", "Function table_object/0 has no local return."}, +{"lib/stampede/external/python.ex", "The pattern can never match the type true."}, +{"lib/stampede/external/python.ex", "Function do_dumb_down_elixir_term/1 will never be called."}, +{"lib/stampede/logger.ex", "Function logger_state/0 has no local return."}, +{"lib/stampede/tables/channel_locks.ex", "The guard clause can never succeed."}, +{"lib/stampede/tables/dummy_msgs.ex", "The guard clause can never succeed."}, +{"lib/stampede/tables/interactions.ex", "The guard clause can never succeed."}, +{"lib/stampede/traceback.ex", "Function t/0 has no local return."}, +{"lib/txt_block.ex", "Function block/0 has no local return."}, +{"lib/txt_block.ex", "Function type/0 has no local return."}, +{"lib/txt_block.ex", "Function t/0 has no local return."}, +{"lib/txt_block/md.ex", "The guard clause can never succeed."}, ] diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 1a0064a..87f7eeb 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -42,13 +42,15 @@ jobs: id: deps-cache uses: actions/cache@v4 with: + # would be good to cache priv/plts but something isn't being cached right, + # and plt ends up expecting files managed by Nix which aren't there. + # example: :dialyzer.run error: File not found: /nix/store/*-erlang-26.2.5.1/lib/erlang/lib/dialyzer-5.1.3/ebin/erl_bif_types.beam path: | deps _build - priv/plts .nix-mix .nix-hex - key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/flake.*')) }}-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.*')) }} + key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/flake.*')) }}-${{ hashFiles(format('{0}{1}', github.workspace, '**/mix.*')) }} restore-keys: | ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/flake.*')) }} ${{ runner.os }}-mix- diff --git a/README.md b/README.md index d1a0a8e..538c054 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It is a sequel to the [Stampy](https://github.com/StampyAI/stampy) Discord bot. ## Use -[Download the Nix package manager](https://nixos.org/download/). This handles the development environment for Stampede. It runs on Linux, Mac, Windows, and Docker. It won't disrupt your system setup -- though once you start using it, it may be hard to stop. :smile: +[Download the Nix package manager](https://github.com/DeterminateSystems/nix-installer). This handles the development environment for Stampede. It runs on Linux, Mac, Windows, and Docker. It won't disrupt your system setup -- though once you start using it, it may be hard to stop. :smile: Once Nix is ready, just cd to the dev directory and run `nix develop .#` to load the dev environment, then `mix deps.get` to get the dependencies. Some commands: - `iex -S mix` to run Stampede in the `dev` environment, also providing the famous Elixir interactive shell. @@ -18,5 +18,29 @@ Once Nix is ready, just cd to the dev directory and run `nix develop .#` to load Configurations for servers are written in YAML and left in `./Sites/`. In different environments (such as `test` and `dev`) it will read configs from `./Sites_{environment-name}`. Check the service documentation for what options your service has available. -- `./lib/services` defines services where chat requests are incoming. -- `./lib/plugins` defines plugins which suggest potential responses, along with a confidence estimate for how relevant the response would be. Plugins which use resources or take time will offer a *callback* instead, which will only be called if no other plugins have higher confidence. + - `./lib/services` defines services where chat requests are incoming. They follow the standard set in `./lib/service.ex` +- `./lib/plugins` defines plugins which suggest potential responses, along with a confidence estimate for how relevant the response would be. Plugins which use resources or take time will offer a *callback* instead, which will only be called if no other plugins have higher confidence. They follow the standard set in `./lib/plugin.ex` + +### Updating `.dialyzer_ignore.exs` + +Often when changing code, you will end up with superfluous Dialyzer warnings. You can suppress them by updating the Dialyzer ignore file. To start: + +```bash +rm ./.dialyzer_ignore.exs +mix dialyzer --format ignore_file_strict &> .dialyzer_ignore.exs.incoming +``` + +Remove all but the Elixir tuples, which will look like this: + +```elixir +{"lib/stampede.ex", "Function server_id/0 has no local return."}, +``` + +Put a `[` at the start of the file and a `]` at the end, so they become one list of tuples. Now you can rename the file to its true name and format it: + +```bash +mv ./.dialyzer_ignore.exs{.incoming,} +mix format ./.dialyzer_ignore.exs +``` + +Now Dialyzer should not raise any more warnings. diff --git a/config/config.exs b/config/config.exs index 9f3e5b5..2fa1cd7 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,5 +1,10 @@ import Config +test_or_dev? = Mix.env() in [:test, :dev] + +prod? = Mix.env() == :prod +test? = Mix.env() == :test + # Extra metadata for the logger to keep stampede_metadata = [ :stampede_component, @@ -23,12 +28,8 @@ extra_metadata = stampede_metadata ++ nostrum_metadata -# Actually start configuring things -config :stampede, - compile_env: Mix.env() - config :stampede, :type_check, - enable_runtime_checks: Mix.env() in [:dev, :test], + enable_runtime_checks: test_or_dev?, debug: false config :stampede, Stampede.Scheduler, @@ -92,9 +93,43 @@ config :ex_unit, config :nostrum, :ffmpeg, false config :stampede, + compile_env: Mix.env(), + services_to_install: [ + Services.Discord + ], + # What will actually be started by stampede + services_to_start: + (if test? do + # NOTE: this will have to change if Service-specific tests start making sense + [Services.Dummy] + else + :all + end), + config_dir: + "./Sites" <> + (if prod? do + "" + else + "_#{Mix.env()}" + end), + # enable posting serious errors to the channel specified in :error_log_destination + log_post_serious_errors: true, + # enable file logging + log_to_file: true, + # clear tables associated with this compilation environment + clear_state: false, + error_log_destination: :unset, python_exe: System.fetch_env!("FLAKE_PYTHON"), python_plugin_dirs: ["./lib_py"] +env_specific_cfg = + "./config_#{Mix.env()}.exs" + |> Path.expand(__DIR__) + +if File.exists?(env_specific_cfg) do + import_config env_specific_cfg +end + for config <- "./*.secret.exs" |> Path.expand(__DIR__) |> Path.wildcard() do import_config config end diff --git a/config/config_test.exs b/config/config_test.exs new file mode 100644 index 0000000..38721e0 --- /dev/null +++ b/config/config_test.exs @@ -0,0 +1,10 @@ +import Config + +config :stampede, + services: [Services.Dummy], + log_to_file: false, + log_post_serious_errors: false, + clear_state: true + +config :stampede, + test_loaded: true diff --git a/flake.lock b/flake.lock index 181fb7e..3513701 100644 --- a/flake.lock +++ b/flake.lock @@ -46,11 +46,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1724440431, - "narHash": "sha256-9etXEOUtzeMgqg1u0wp+EdwG7RpmrAZ2yX516bMj2aE=", + "lastModified": 1725513492, + "narHash": "sha256-tyMUA6NgJSvvQuzB7A1Sf8+0XCHyfSPRx/b00o6K0uo=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "c8a54057aae480c56e28ef3e14e4960628ac495b", + "rev": "7570de7b9b504cfe92025dd1be797bf546f66528", "type": "github" }, "original": { @@ -82,11 +82,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1724224976, - "narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=", + "lastModified": 1725432240, + "narHash": "sha256-+yj+xgsfZaErbfYM3T+QvEE2hU7UuE+Jf0fJCJ8uPS0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c374d94f1536013ca8e92341b540eba4c22f9c62", + "rev": "ad416d066ca1222956472ab7d0555a6946746a80", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index d69f7e1..a762e42 100644 --- a/flake.nix +++ b/flake.nix @@ -23,8 +23,8 @@ flake-utils.lib.eachDefaultSystem ( system: let - # NOTE: change to true to enable commit checks - # when disabled, also run "pre-commit uninstall" to disable + # NOTE: change to false to disable commit checks + # when disabling, also run "pre-commit uninstall" to disable enablePreCommitChecks = true; pkgs = nixpkgs.legacyPackages.${system}; @@ -93,6 +93,8 @@ dialyzer = { enable = true; package = ex; + pass_filenames = false; + require_serial = true; }; custom-mix-test = { enable = true; diff --git a/flake.systems.nix b/flake.systems.nix deleted file mode 100644 index a2c5760..0000000 --- a/flake.systems.nix +++ /dev/null @@ -1,6 +0,0 @@ -[ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" -] diff --git a/lib/services/dummy.ex b/lib/services/dummy.ex index f133205..9b8c443 100644 --- a/lib/services/dummy.ex +++ b/lib/services/dummy.ex @@ -1,94 +1,51 @@ -defmodule Services.Dummy.Table do - @moduledoc false - @compile [:bin_opt_info, :recv_opt_info] - use TypeCheck - alias Stampede.Events.MsgReceived - alias Stampede.Events.ResponseToPost - alias Stampede, as: S - - use Memento.Table, - attributes: [:id, :datetime, :server_id, :channel, :user, :body, :referenced_msg_id], - type: :ordered_set, - access_mode: :read_write, - autoincrement: true, - index: [:datetime] - - def new(record) when is_map(record) do - record - |> Map.put_new(:id, nil) - |> Map.put_new(:datetime, S.time()) - |> then(&struct!(__MODULE__, &1 |> Map.to_list())) - end - - def validate!(record) when is_struct(record, __MODULE__) do - record - # |> TypeCheck.conforms!(%__MODULE__{ - # id: nil | integer(), - # datetime: S.timestamp(), - # server_id: atom(), - # channel: atom(), - # user: atom(), - # body: any(), - # referenced_msg_id: nil | integer() - # }) - end -end - defmodule Services.Dummy do @compile [:bin_opt_info, :recv_opt_info] - # TODO: this is not actually parallelized meaning it can't be used in benchmarks - # Maybe it should be a supervisor with a process for each thread? require Logger - use GenServer use TypeCheck use TypeCheck.Defstruct - alias Services.Dummy + require Ex2ms + alias Stampede.Tables + alias __MODULE__.Channel alias Stampede, as: S require S alias S.Events.{MsgReceived, ResponseToPost} require MsgReceived use Service + use Supervisor # Imaginary server types - @opaque! dummy_user_id :: atom() - @opaque! dummy_channel_id :: atom() | nil - @opaque! dummy_server_id :: identifier() | atom() + @type! dummy_user_id :: atom() + @type! dummy_channel_id :: atom() | nil + @type! dummy_server_id :: identifier() | atom() | {:dm, __MODULE__} @type! dummy_msg_id :: integer() # "one channel" - @typep! msg_content :: String.t() | nil - @typep! msg_reference :: nil | dummy_msg_id() - # internal representation of messages - @typep! msg_tuple :: - {id :: dummy_msg_id(), - {user :: dummy_user_id(), body :: msg_content(), ref :: msg_reference()}} - @typedoc """ - Tuple format for adding new messages - """ - @type! msg_tuple_incoming :: + @type! msg_content :: String.t() + @type! msg_reference :: nil | dummy_msg_id() + @type! incoming_msg_tuple :: {server_id :: dummy_server_id(), channel :: dummy_channel_id(), - user :: dummy_user_id(), body :: msg_content(), ref :: msg_reference()} - @type! channel :: list(msg_tuple()) - # multiple channels - @type! channel_buffers :: %{dummy_channel_id() => channel()} | %{} - - defstruct!( - servers: _ :: MapSet.t(atom()), - vip_ids: _ :: S.CfgTable.vips() - ) + user :: dummy_user_id(), formatted_text :: msg_content(), ref :: msg_reference()} + @type! retrieved_msg_tuple :: + {id :: dummy_msg_id(), + {user :: dummy_user_id(), text :: msg_content(), ref :: msg_reference()}} + @type! channel_log :: list(retrieved_msg_tuple()) + @type! server_log :: map(dummy_channel_id(), channel_log()) @system_user :server @bot_user :stampede + def system_user, do: @system_user + def bot_user, do: @bot_user + @schema NimbleOptions.new!( SiteConfig.merge_custom_schema( service: [ - default: Services.Dummy, - type: {:in, [Services.Dummy]} + default: __MODULE__, + type: {:in, [__MODULE__]} ], server_id: [ required: true, - type: :atom + type: {:or, [:atom, {:in, ["DM", {:dm, __MODULE__}]}]} ], error_channel_id: [ default: :error, @@ -107,73 +64,187 @@ defmodule Services.Dummy do This service can be used for testing and experimentation, by taking the role of a service relaying messages to Stampede. - It expects one config per service instance, where real services expect one per server. + The database of messages is all in one table, channels are made syncronous by one GenServer per channel doing all writing. SiteConfig/startup args: #{NimbleOptions.docs(@schema)} """ def site_config_schema(), do: @schema - # PUBLIC API FUNCTIONS - - @spec! start_link(Keyword.t()) :: :ignore | {:error, any} | {:ok, pid} @impl Service - def start_link(cfg_overrides \\ []) do - Logger.debug("starting Dummy GenServer, with cfg overrides: #{inspect(cfg_overrides)}") - GenServer.start_link(__MODULE__, cfg_overrides, name: __MODULE__) + def start_link([]) do + Supervisor.start_link(__MODULE__, name: __MODULE__.Supervisor) end - @doc "dev-facing option for getting bot responses" - @spec! ask_bot( + @impl Service + def send_msg({server_id, channel, user}, text, opts \\ []), + do: send_msg(server_id, channel, user, text, opts) + + @spec! send_msg( dummy_server_id(), dummy_channel_id(), dummy_user_id(), msg_content() | TxtBlock.t(), keyword() - ) :: - %{ - response: nil | ResponseToPost.t(), - posted_msg_id: dummy_msg_id(), - bot_response_msg_id: nil | dummy_msg_id() - } - | nil - | ResponseToPost.t() - def ask_bot(server_id, channel, user, text, opts \\ []) do + ) :: {:ok, dummy_msg_id()} + def send_msg(server_id, channel, user, text, opts \\ []) do formatted_text = TxtBlock.to_binary(text, __MODULE__) - GenServer.call( - __MODULE__, - {:ask_bot, {server_id, channel, user, formatted_text, opts[:ref]}, opts} - ) + Channel.add_msg({server_id, channel, user, formatted_text, opts[:ref]}) end - @impl Service - def send_msg({server_id, channel, user}, text, opts \\ []), - do: send_msg(server_id, channel, user, text, opts) - - # BUG: why does Dialyzer not acknowledge unwrapped nil and ResponseToPost? - @spec! send_msg( + @doc "dev-facing option for getting bot responses" + @spec! ask_bot( dummy_server_id(), dummy_channel_id(), dummy_user_id(), msg_content() | TxtBlock.t(), keyword() - ) :: {:ok, nil} - def send_msg(server_id, channel, user, text, opts \\ []) do - formatted_text = - TxtBlock.to_binary(text, __MODULE__) + ) :: + nil + | %{ + response: nil | ResponseToPost.t(), + posted_msg_id: dummy_msg_id(), + bot_response_msg_id: nil | dummy_msg_id() + } + | ResponseToPost.t() + def ask_bot(server_id, channel, user, unformatted_text, opts \\ []) do + return_id = Keyword.get(opts, :return_id, false) + + text = + TxtBlock.to_binary(unformatted_text, __MODULE__) + + {:ok, inciting_msg_id} = send_msg(server_id, channel, user, text, opts) + + case Stampede.CfgTable.get_cfg(__MODULE__, server_id) do + {:error, :server_notfound} -> + Logger.debug("Dummy ignoring unconfigured server #{server_id |> inspect()}") + nil + + {:ok, cfg} -> + inciting_msg_with_context = + inciting_msg_id + |> get_msg_object() + |> MsgReceived.add_context(cfg) + + case Plugin.get_top_response(cfg, inciting_msg_with_context) do + {response = %ResponseToPost{}, iid} when is_struct(response, ResponseToPost) -> + binary_response = + response + |> Map.update!(:text, fn blk -> + TxtBlock.to_binary(blk, __MODULE__) + end) + + {:ok, bot_response_msg_id} = + send_msg(server_id, channel, @bot_user, binary_response.text, + ref: response.origin_msg_id + ) + + S.Interact.finalize_interaction(iid, bot_response_msg_id) - GenServer.call( - __MODULE__, - {:add_msg, {server_id, channel, user, formatted_text, opts[:ref]}} + if return_id do + %{ + response: binary_response, + posted_msg_id: inciting_msg_id, + bot_response_msg_id: bot_response_msg_id + } + else + binary_response + end + + nil -> + nil + end + + _ -> + raise "Unexpected result from get_cfg" + end + end + + def default_server(), + do: {:dm, __MODULE__} + + def default_channel(), + do: :default_channel + + def ask_bot(unformatted_text) do + default_config = [ + server_id: "DM", + prefix: "!", + plugs: :all, + dm_handler: true, + bot_is_loud: true + ] + + # Register default server if it isn't already + _ = + case Stampede.CfgTable.get_cfg(__MODULE__, default_server()) do + {:error, :server_notfound} -> + default_config + |> new_server() + + {:ok, _} -> + :ok + end + + ask_bot(default_server(), default_channel(), @system_user, unformatted_text) + end + + def new_server(cfg_kwlist) when is_list(cfg_kwlist) do + cfg_kwlist + |> Keyword.put(:service, :dummy) + |> SiteConfig.validate!(site_config_schema(), [&hack_dummy_dm_handler/2]) + |> S.CfgTable.insert_cfg() + + # Just to be safe + Process.sleep(100) + + :ok + end + + @impl Service + def reload_configs() do + :ok + end + + @spec! get_msg_object(dummy_msg_id()) :: MsgReceived.t() + def get_msg_object(id) do + Tables.transaction!(fn -> + Memento.Query.read(Tables.DummyMsgs, id) + end) + |> into_msg() + end + + def into_msg(%Tables.DummyMsgs{ + id: id, + server_id: server_id, + channel: channel, + user: user, + body: body, + referenced_msg_id: ref + }) do + MsgReceived.new( + id: id, + body: body, + channel_id: channel, + author_id: user, + server_id: server_id, + referenced_msg_id: ref ) end + def via(server_id, channel_id) do + tag = :erlang.phash2({server_id, channel_id}) + {:via, Registry, {registry(), tag}} + end + + def registry(), do: __MODULE__.ChannelRegistry + @impl Service def format_plugin_fail( - _cfg = %{service: Services.Dummy}, - msg = %{service: Services.Dummy}, + _cfg = %{service: __MODULE__}, + msg = %{service: __MODULE__}, %{plugin: p, type: t, error: e, stacktrace: st} ) do error_type = @@ -202,49 +273,38 @@ defmodule Services.Dummy do def log_plugin_error(cfg, msg, error_info) do formatted = format_plugin_fail(cfg, msg, error_info) + |> TxtBlock.to_binary(__MODULE__) - # NOTE: as this function is generally being called inside a GenServer process, spawning a new thread is required. _ = - Task.start_link(fn -> - send_msg( - SiteConfig.fetch!(cfg, :server_id), - SiteConfig.fetch!(cfg, :error_channel_id), - @bot_user, - formatted - |> TxtBlock.to_binary(__MODULE__) - ) - end) + send_msg( + SiteConfig.fetch!(cfg, :server_id), + SiteConfig.fetch!(cfg, :error_channel_id), + @bot_user, + formatted + ) {:ok, formatted} end - def into_msg({id, server_id, channel, user, body, ref}) do - MsgReceived.new( - id: id, - body: body, - channel_id: channel, - author_id: user, - server_id: server_id, - referenced_msg_id: ref - ) - end - @impl Service - def dm?({_id, _server_id = {:dm, __MODULE__}, _channel, _user, _body, _ref}), + @spec! dm?(MsgReceived.t()) :: boolean() + def dm?(%MsgReceived{server_id: {:dm, __MODULE__}}), do: true - def dm?(_other), do: false + def dm?(%MsgReceived{}), do: false @impl Service def author_privileged?(server_id, author_id) do - GenServer.call(__MODULE__, {:author_privileged?, server_id, author_id}) + S.CfgTable.vips_configured(__MODULE__) + |> Map.get(server_id, MapSet.new()) + |> MapSet.member?(author_id) end @impl Service def at_bot?(_cfg, %{referenced_msg_id: ref}) do (ref || false) && - transaction!(fn -> - Memento.Query.read(__MODULE__.Table, ref) + Tables.transaction!(fn -> + Memento.Query.read(Tables.DummyMsgs, ref) |> case do nil -> false @@ -262,236 +322,71 @@ defmodule Services.Dummy do def txt_format(blk, kind), do: TxtBlock.Md.format(blk, kind) - @spec! channel_history(dummy_server_id(), dummy_channel_id()) :: channel() + @spec! channel_history(dummy_server_id(), dummy_channel_id()) :: channel_log() def channel_history(server_id, channel) do - GenServer.call(__MODULE__, {:channel_history, server_id, channel}) + Tables.transaction!(fn -> + Memento.Query.select( + Tables.DummyMsgs, + {:and, {:==, :server_id, server_id}, {:==, :channel, channel}} + # Ex2ms.fun do + # {id, _datetime, sid, cid, user, body, ref} + # when sid == ^server_id and cid == ^channel -> + # {id, {user, body, ref}} + # end + ) + |> Enum.map(fn + item = %Tables.DummyMsgs{} -> + {item.id, {item.user, item.body, item.referenced_msg_id}} + end) + end) end - @spec! server_dump(dummy_server_id()) :: channel_buffers() + @spec! server_dump(dummy_server_id()) :: server_log() def server_dump(server_id) do - GenServer.call(__MODULE__, {:server_dump, server_id}) - end - - def new_server(cfg_kwlist) when is_list(cfg_kwlist) do - cfg_kwlist - |> Keyword.put(:service, :dummy) - |> SiteConfig.validate!(site_config_schema()) - |> S.CfgTable.insert_cfg() - - Process.sleep(100) - - :ok - end - - def new_server(new_server_id, plugs \\ nil) when not is_list(new_server_id) do - args = - [ - server_id: new_server_id - ] ++ if plugs, do: [plugs: plugs], else: [] - - new_server(args) - end - - @impl Service - def reload_configs() do - GenServer.call(__MODULE__, :reload_configs) - end - - # PLUMBING - - @spec! update_state() :: %__MODULE__{} - defp update_state() do - struct!( - __MODULE__, - servers: S.CfgTable.servers_configured(__MODULE__), - vip_ids: S.CfgTable.vips_configured(__MODULE__) - ) - end - - @spec! update_state(%__MODULE__{}) :: %__MODULE__{} - defp update_state(_ignored_state) do - update_state() - end - - @impl GenServer - @spec! init(Keyword.t()) :: {:ok, %__MODULE__{}} - def init(_) do - # Service.register_logger(registry, __MODULE__, self()) - :ok = S.Tables.ensure_tables_exist([Services.Dummy.Table]) - {:ok, update_state()} - end - - @impl GenServer - def handle_call( - {:add_msg, msg_tuple = {server_id, _channel, _user, _text, _ref}}, - _from, - state - ) do - if server_id not in state.servers do - # ignore unconfigured server - {:reply, {:ok, nil}, state} - else - %{ - new_state: new_state_1 - } = do_add_new_msg(msg_tuple, state) - - {:reply, {:ok, nil}, new_state_1} - end - end - - def handle_call( - {:ask_bot, msg_tuple = {server_id, channel, _user, _text, _ref}, opts}, - _from, - state - ) do - if server_id not in state.servers do - # ignore unconfigured server - {:reply, nil, state} - else - %{ - posted_msg_id: inciting_msg_id, - posted_msg_object: inciting_msg, - new_state: new_state_1 - } = do_add_new_msg(msg_tuple, state) - - cfg = S.CfgTable.get_cfg!(__MODULE__, server_id) - - inciting_msg_with_context = - inciting_msg - |> MsgReceived.add_context(cfg) - - result = - case Plugin.get_top_response(cfg, inciting_msg_with_context) do - {response, iid} when is_struct(response, ResponseToPost) -> - binary_response = - response - |> Map.update!(:text, fn blk -> - TxtBlock.to_binary(blk, Services.Dummy) - end) - - %{new_state: new_state_2, posted_msg_id: bot_response_msg_id} = - do_post_response({server_id, channel}, binary_response, new_state_1) - - S.Interact.finalize_interaction(iid, bot_response_msg_id) - - {:reply, - %{ - response: binary_response, - posted_msg_id: inciting_msg_id, - bot_response_msg_id: bot_response_msg_id - }, new_state_2} - - nil -> - {:reply, %{response: nil, posted_msg_id: inciting_msg_id}, new_state_1} - end - - # if opts has key :return_id, returns the id of posted message along with any response msg - case Keyword.get(opts, :return_id, false) do - true -> - result - - false -> - {status, %{response: response}, state} = result - - {status, response, state} - end - end - end - - def handle_call({:channel_history, server_id, channel}, _from, state) do - if server_id not in state.servers, do: raise("Server not registered") - - history = do_get_channel_history(server_id, channel) - - {:reply, history, state} - end - - def handle_call({:server_dump, server_id}, _from, state) do - if server_id not in state.servers, do: raise("Server not registered") + Tables.transaction!(fn -> + Memento.Query.select( + Tables.DummyMsgs, + {:==, :server_id, server_id} + # Ex2ms.fun do + # {id, _datetime, sid, cid, user, body, ref} + # when sid == ^server_id and cid == ^channel -> + # {id, {user, body, ref}} + # end + ) + |> Enum.reduce(%{}, fn + item = %Tables.DummyMsgs{}, acc -> + this_msg = {item.id, {item.user, item.body, item.referenced_msg_id}} - dump = - transaction!(fn -> - Memento.Query.select( - __MODULE__.Table, - {:==, :server, server_id} - ) + Map.update(acc, item.channel, [this_msg], fn channel -> + [this_msg | channel] + end) end) - - {:reply, dump, state} + |> Map.new(fn {cid, channel} -> + {cid, Enum.reverse(channel)} + end) + end) end - def handle_call({:author_privileged?, _server_id, author_id}, _from, state) do - # TODO: make VIPs like Discord - case author_id do - @system_user -> - {:reply, true, state} + # Transform function for use in `SiteConfig.validate!/3`. This is a hack because it's dodging the duplicate checking done in `SiteConfig.make_configs_for_dm_handling/1`. + defp hack_dummy_dm_handler(kwlist, _schema) do + case kwlist[:server_id] do + "DM" -> + kwlist + |> Keyword.put(:server_id, S.make_dm_tuple(Services.Dummy)) + |> Keyword.put(:dm_handler, true) - _other -> - {:reply, false, state} + _ -> + kwlist end end - def handle_call(:reload_configs, _from, state) do - { - :reply, - :ok, - update_state(state) - } - end - - defp do_post_response({server_id, channel}, response, state) - when is_struct(response, ResponseToPost) do - {server_id, channel, @bot_user, response.text, response.origin_msg_id} - |> do_add_new_msg(state) - end - - @spec! do_add_new_msg(tuple(), %__MODULE__{}) :: %{ - posted_msg_id: dummy_msg_id(), - posted_msg_object: %MsgReceived{}, - new_state: %__MODULE__{} - } - defp do_add_new_msg(msg_tuple = {server_id, channel, user, text, ref}, state) do - record = - Dummy.Table.new(%{ - server_id: server_id, - channel: channel, - user: user, - body: text, - referenced_msg_id: ref - }) - - msg_id = - transaction!(fn -> - Memento.Query.write(record) - |> Map.fetch!(:id) - end) - - msg_object = into_msg(msg_tuple |> Tuple.insert_at(0, msg_id)) - - %{ - posted_msg_id: msg_id, - posted_msg_object: msg_object, - new_state: state - } - end - - def do_get_channel_history(server_id, channel) do - transaction!(fn -> - Memento.Query.select( - __MODULE__.Table, - [ - {:==, :channel, channel}, - {:==, :server_id, server_id} - ] - ) - |> Enum.map(fn - item -> - {item.id, {item.user, item.body, item.referenced_msg_id}} - end) - end) - end + @impl Supervisor + def init(_ \\ []) do + children = [ + {DynamicSupervisor, name: __MODULE__.ChannelSuper}, + {Registry, keys: :unique, name: registry()} + ] - defp transaction!(f) do - Memento.Transaction.execute!(f, 10) + Supervisor.init(children, strategy: :one_for_all) end end diff --git a/lib/services/dummy/channel.ex b/lib/services/dummy/channel.ex new file mode 100644 index 0000000..6cd9dbb --- /dev/null +++ b/lib/services/dummy/channel.ex @@ -0,0 +1,56 @@ +defmodule Services.Dummy.Channel do + use TypeCheck + alias Stampede.Tables.Ids + alias Stampede.Tables.DummyMsgs + alias Stampede.Tables + alias Services.Dummy + + use GenServer + + @type! t :: nil + + def start_link(via_spec) do + GenServer.start_link(__MODULE__, [], name: via_spec) + end + + @spec! add_msg(Dummy.incoming_msg_tuple()) :: {:ok, Dummy.dummy_msg_id()} + def add_msg({server_id, channel, user, formatted_text, ref}) do + via_spec = {:via, _, {reg, tag}} = Dummy.via(server_id, channel) + + _ = + case Registry.lookup(reg, tag) do + [_] -> + :done + + [] -> + {:ok, _} = DynamicSupervisor.start_child(Dummy.ChannelSuper, {__MODULE__, via_spec}) + end + + GenServer.call(via_spec, {:add_msg, {server_id, channel, user, formatted_text, ref}}) + end + + @spec! init([]) :: {:ok, t()} + def init([]) do + {:ok, nil} + end + + @spec! handle_call(any(), any(), t()) :: tuple() + def handle_call({:add_msg, msg}, _, state) do + {:reply, {:ok, do_add_msg(msg)}, state} + end + + @spec! do_add_msg(Dummy.incoming_msg_tuple()) :: Dummy.dummy_msg_id() + def do_add_msg(msg = {_server_id, _channel, _user, _formatted_text, _ref}) do + id = Ids.reserve_id(DummyMsgs) + + record = DummyMsgs.new(id, msg) + + Tables.transaction_sync!(fn -> + %DummyMsgs{} = Memento.Query.write(record) + + :ok + end) + + id + end +end diff --git a/lib/site_config.ex b/lib/site_config.ex index c2dcf14..1398373 100644 --- a/lib/site_config.ex +++ b/lib/site_config.ex @@ -255,16 +255,17 @@ defmodule SiteConfig do @doc """ Create a config with key {:dm, service} which all DMs for a service will be handled under. If server_id is not "DM", it will be duplicated with one for the server and - one for the DMs. + one for the DMs. This lets you use the same settings for a server and for DMs, when convenient. Collects all VIPs for that service and puts them in the DM config. """ + # TODO: make this happen across entire cfg table on every new config load. Maybe have a dedicated sanity-checking stage that can refuse bad configs. def make_configs_for_dm_handling(service_map) do Map.new(service_map, fn {service, site_map} -> dupe_checked = Enum.reduce( site_map, {Map.new(), MapSet.new(), MapSet.new()}, - # Accumulator keeps a map for the sites being processed, and a mapset to check for duplicate keys + # Accumulator keeps a map for the sites being processed, and two mapsets to check for duplicate keys fn {server_id, orig_cfg}, {site_acc, services_handled, service_vips} -> if not orig_cfg.dm_handler do { diff --git a/lib/stampede.ex b/lib/stampede.ex index 3c5f22d..917bb39 100644 --- a/lib/stampede.ex +++ b/lib/stampede.ex @@ -2,6 +2,7 @@ defmodule Stampede do @moduledoc """ Defines project-wide types and utility functions. """ + require Logger @compile [:bin_opt_info, :recv_opt_info] use TypeCheck @@ -50,8 +51,7 @@ defmodule Stampede do def confused_response(), do: {:italics, "confused beeping"} - def compilation_environment, - do: Application.get_env(:stampede, :compile_env) + def compilation_environment, do: Application.fetch_env!(:stampede, :compile_env) def throw_internal_error(text \\ "*screaming*") do raise "intentional internal error: #{text}" @@ -86,12 +86,34 @@ defmodule Stampede do {:via, PartitionSupervisor, {Stampede.QuickTaskSupers, self()}} end + @doc """ + Used for configs to name services when they can't pass real atoms. + """ @spec! services() :: map(service_name(), module()) - def services(), - do: %{ - discord: Services.Discord, - dummy: Services.Dummy - } + def services() do + Application.fetch_env!(:stampede, :installed_services) + |> Map.new(fn full_atom -> + { + full_atom |> downcase_last_atom(), + full_atom + } + end) + end + + @doc """ + iex> Stampede.downcase_last_atom(Services.Discord) + :discord + iex> Stampede.downcase_last_atom(A.B.C) + :c + """ + def downcase_last_atom(full_atom) do + full_atom + |> Atom.to_string() + |> String.split(".") + |> List.last() + |> String.downcase() + |> String.to_atom() + end def service_atom_to_name(atom) do services() @@ -116,57 +138,43 @@ defmodule Stampede do end @doc """ - If passed a text prefix, will match the start of the string. If passed a - regex, it will match whatever was given and return the first match group. - """ - @spec! strip_prefix(String.t() | Regex.t(), String.t()) :: false | String.t() - def strip_prefix(prefix, text) - ## here comes the "smart" """optimized""" solution - when is_binary(prefix) and - binary_part(text, 0, floor(bit_size(prefix) / 8)) == prefix do - binary_part(text, floor(bit_size(prefix) / 8), floor((bit_size(text) - bit_size(prefix)) / 8)) - end + Takes a prefix and a string. Returns the matched part, and the rest of the string. The prefix can be a single string, or a list of strings. - def strip_prefix(prefix, text) - when is_binary(prefix) and - binary_part(text, 0, floor(bit_size(prefix) / 8)) != prefix, - do: false + Here is a single prefix. - def strip_prefix(rex, text) when is_struct(rex, Regex) do - case Regex.run(rex, text) do - nil -> false - [_p, body] -> body - end - end + iex> alias Stampede, as: S + iex> S.split_prefix("!ping", "!") + {"!", "ping"} + iex> S.split_prefix("ping", "!") + {false, "ping"} + iex> S.split_prefix("!", "!") + {false, "!"} - def split_prefix(text, prefix) when is_struct(prefix, Regex) and is_binary(text) do - case Regex.split(prefix, text, include_captures: true, capture: :first, trim: true) do - [p, b] -> - {p, b} + Here is a binary list, which is more performant than the Regex module. - [^text] -> - {false, text} - - [] -> - {false, text} - end - end + iex> bl = ["S, ", "S ", "s, ", "s "] + iex> S.split_prefix("S, ping", bl) + {"S, ", "ping"} + """ def split_prefix(text, prefix) when is_binary(prefix) and is_binary(text) do case text do - # don't match prefix without message - <<^prefix::binary-size(floor(bit_size(prefix) / 8)), ""::binary>> -> - {false, text} - - # don't match prefix without message - <<^prefix::binary-size(floor(bit_size(prefix) / 8)), " "::binary>> -> - {false, text} - <<^prefix::binary-size(floor(bit_size(prefix) / 8)), _::binary>> -> - { - binary_part(text, 0, byte_size(prefix)), - binary_part(text, byte_size(prefix), byte_size(text) - byte_size(prefix)) - } + prefix_part = binary_part(text, 0, byte_size(prefix)) + msg_part = binary_part(text, byte_size(prefix), byte_size(text) - byte_size(prefix)) + + # final whitespace check + # TODO: benchmark with/without + case String.trim(msg_part) do + "" -> + {false, text} + + _ -> + { + prefix_part, + msg_part + } + end _ -> {false, text} @@ -324,6 +332,37 @@ defmodule Stampede do |> Kernel.<>("\n") end + def await_process!(name, tries \\ 100) + + def await_process!(name, 0) do + Logger.error(fn -> + [ + "Tried to find process ", + inspect(name), + " but it never registered." + ] + end) + + raise "Process #{inspect(name)} not found" + end + + def await_process!(name, tries) do + case Process.whereis(name) do + nil -> + Process.sleep(10) + await_process!(name, tries - 1) + + pid -> + pid + end + end + + def path_exists?(path) do + if File.exists?(path), + do: {:ok, path}, + else: {:error, "File not found"} + end + defmodule Debugging do @moduledoc false use TypeCheck diff --git a/lib/stampede/application.ex b/lib/stampede/application.ex index a6164c8..237b1cb 100644 --- a/lib/stampede/application.ex +++ b/lib/stampede/application.ex @@ -9,66 +9,22 @@ defmodule Stampede.Application do use Application - def startup_schema() do - NimbleOptions.new!( - installed_services: [ - type: {:or, [{:in, [[]]}, {:list, {:in, Map.keys(S.services())}}]}, - required: true, - doc: "Services installed as part of the mix project. Passed in from mix.exs" - ], - # installed_foreign_plugins: [ - # type: {:or, [{:in, [[]]}, {:list, {:in, Map.values(S.services())}}]}, - # required: true, - # doc: "Foreign Plugin sources installed as part of the mix project. Passed in from mix.exs" - # ], - services: [ - type: {:or, [{:in, [:all]}, {:list, {:in, Map.keys(S.services())}}]}, - default: :all, - doc: "what will actually be started by Stampede" - ], - config_dir: [ - type: :string, - default: "./Sites", - doc: "Will be read from :stampede/:config_dir if unset" - ], - log_to_file: [ - type: :boolean, - default: true, - doc: "enable file logging" - ], - log_post_serious_errors: [ - type: :boolean, - default: true, - doc: "enable posting serious errors to the channel specified in :error_log_destination" - ], - clear_state: [ - type: :boolean, - default: false, - doc: "clear tables associated with this compilation environment" - ] - ) - end - @impl Application - def start(_type, startup_override_args \\ []) do + def start(_type, _startup_override_args \\ []) do :ok = Logger.metadata(stampede_component: :application) - # first validation fills defaults startup_args = - NimbleOptions.validate!(startup_override_args, startup_schema()) - |> S.keyword_put_new_if_not_falsy( - :services, - Application.get_env(:stampede, :services, false) - ) - |> S.keyword_put_new_if_not_falsy( + [ + :log_to_file, + :log_post_serious_errors, + :error_log_destination, :config_dir, - Application.get_env(:stampede, :config_dir, false) - ) - |> Keyword.update!(:config_dir, fn dir -> - dir <> "_#{Stampede.compilation_environment()}" + :clear_state, + :services_to_start + ] + |> Enum.map(fn key -> + {key, Application.fetch_env!(:stampede, key)} end) - # ensure our transformation went correctly - |> NimbleOptions.validate!(startup_schema()) if startup_args[:log_to_file], do: :ok = Logger.add_handlers(:stampede) @@ -76,7 +32,7 @@ defmodule Stampede.Application do _ = if startup_args[:log_post_serious_errors] do - case Application.get_env(:stampede, :error_log_destination, :unset) do + case startup_args[:error_log_destination] do {error_service, channel_id} -> {:ok, _} = LoggerBackends.add(Stampede.Logger) @@ -99,8 +55,6 @@ defmodule Stampede.Application do Logger.info(":error_log_destination is false, not posting errors to anywhere") end - # TODO: move activation into service modules themselves - # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options children = make_children(startup_args) @@ -113,10 +67,6 @@ defmodule Stampede.Application do |> Map.fetch!(atom) end - def all_services(installed) do - installed |> Enum.map(&service_spec(&1)) - end - def make_children(startup_args) do default_children = [ {PartitionSupervisor, child_spec: Task.Supervisor, name: Stampede.QuickTaskSupers}, @@ -126,19 +76,14 @@ defmodule Stampede.Application do ] service_tuples = - case Keyword.fetch!(startup_args, :services) do - :all -> - installed = Keyword.fetch!(startup_args, :installed_services) - Logger.debug("Stampede starting all services: #{inspect(installed)}") - all_services(installed) - - name when is_atom(name) -> + case Keyword.fetch!(startup_args, :services_to_start) do + [name] -> Logger.debug("Stampede starting only #{name}") - [service_spec(name)] + [name] list when is_list(list) or is_tuple(list) -> Logger.debug("Stampede starting these: #{inspect(list)}") - Enum.map(list, &service_spec(&1)) + list end default_children ++ service_tuples diff --git a/lib/stampede/cfg_table.ex b/lib/stampede/cfg_table.ex index 5eabeda..cb66fc6 100644 --- a/lib/stampede/cfg_table.ex +++ b/lib/stampede/cfg_table.ex @@ -154,23 +154,49 @@ defmodule Stampede.CfgTable do try do f.(table) catch - _t, _e -> + t, e -> reraise( """ - Standard action with config failed. Now dumping state for examination. - If the error isn't caught, it will get raised after this. - """ <> - S.pp(table), + Standard action with config failed. + + Table dump: + #{S.pp(table)} + + What went wrong: + #{Exception.format(t, e)} + """, __STACKTRACE__ ) end end + @spec! get_cfg(S.service_name(), S.server_id()) :: + {:ok, SiteConfig.t()} + | {:error, :server_notfound | :service_notfound} + def get_cfg(service, id) do + table = table_dump() + + case Map.fetch(table, service) do + {:ok, servers} -> + case Map.fetch(servers, id) do + {:ok, cfg} -> + {:ok, cfg} + + :error -> + {:error, :server_notfound} + end + + :error -> + {:error, :service_notfound} + end + end + @spec! get_cfg!(S.service_name(), S.server_id()) :: SiteConfig.t() def get_cfg!(service, id) do - table_dump() - |> Map.fetch!(service) - |> Map.fetch!(id) + case get_cfg(service, id) do + {:ok, cfg} -> cfg + error -> raise inspect(error) + end end @doc """ diff --git a/lib/stampede/interact.ex b/lib/stampede/interact.ex index 215843a..ffce604 100644 --- a/lib/stampede/interact.ex +++ b/lib/stampede/interact.ex @@ -296,10 +296,10 @@ defmodule Stampede.Interact do @spec! do_write_channellock!(%ChannelLocks{}) :: :ok def do_write_channellock!(record) do - _ = ChannelLocks.validate!(record) + %ChannelLocks{} = ChannelLocks.validate!(record) transaction!(fn -> - _ = Memento.Query.write(record) + %ChannelLocks{} = Memento.Query.write(record) end) :ok diff --git a/lib/stampede/tables.ex b/lib/stampede/tables.ex index 41bc8ea..40b4bd4 100644 --- a/lib/stampede/tables.ex +++ b/lib/stampede/tables.ex @@ -5,9 +5,9 @@ defmodule Stampede.Tables do require Logger use TypeCheck alias Stampede, as: S - alias S.Tables.{Ids, Interactions, ChannelLocks} + alias S.Tables.{Ids, Interactions, ChannelLocks, DummyMsgs} - @mnesia_tables [Ids, Interactions, ChannelLocks] + @mnesia_tables [Ids, Interactions, ChannelLocks, DummyMsgs] def mnesia_tables(), do: @mnesia_tables def init(args) do diff --git a/lib/stampede/tables/dummy_msgs.ex b/lib/stampede/tables/dummy_msgs.ex new file mode 100644 index 0000000..b8a68cc --- /dev/null +++ b/lib/stampede/tables/dummy_msgs.ex @@ -0,0 +1,55 @@ +defmodule Stampede.Tables.DummyMsgs do + @moduledoc false + @compile [:bin_opt_info, :recv_opt_info] + use TypeCheck + alias Stampede, as: S + + use Memento.Table, + attributes: [:id, :datetime, :server_id, :channel, :user, :body, :referenced_msg_id], + type: :ordered_set, + access_mode: :read_write, + autoincrement: true, + storage_properties: [ + ets: [ + write_concurrency: :auto, + read_concurrency: true, + decentralized_counters: true + ] + ] + + def new(id, {server_id, channel, user, formatted_text, ref}) do + [ + id: id, + datetime: S.time(), + server_id: server_id, + channel: channel, + user: user, + body: formatted_text, + referenced_msg_id: ref + ] + |> then(&struct!(__MODULE__, &1)) + |> validate!() + end + + alias Services.Dummy, as: D + + def validate!(record) when is_struct(record, __MODULE__) do + if S.enable_typechecking?() do + record + |> TypeCheck.conforms!(%__MODULE__{ + id: D.dummy_msg_id(), + datetime: S.timestamp(), + server_id: D.dummy_server_id(), + channel: D.dummy_channel_id(), + user: D.dummy_user_id(), + body: binary(), + referenced_msg_id: nil | integer() + }) + else + record + end + end + + def validate!(record) when not is_struct(record, __MODULE__), + do: raise("Not a #{__MODULE__} instance.\n" <> S.pp(record)) +end diff --git a/lib/txt_block.ex b/lib/txt_block.ex index 2f561bf..33d015b 100644 --- a/lib/txt_block.ex +++ b/lib/txt_block.ex @@ -26,6 +26,8 @@ defmodule TxtBlock do @type! t :: [] | nonempty_list(lazy(t())) | String.t() | lazy(block) @spec! to_binary(t(), module()) :: String.t() + def to_binary(blk, _) when is_binary(blk), do: blk + def to_binary(blk, service_name) do to_str_list(blk, service_name) |> IO.iodata_to_binary() diff --git a/mix.exs b/mix.exs index 1dc277c..10b1395 100644 --- a/mix.exs +++ b/mix.exs @@ -4,13 +4,12 @@ defmodule Stampede.MixProject do def project do [ app: :stampede, - version: "0.1.1-dev", + version: "0.1.2-dev", elixir: "~> 1.16", start_permanent: Mix.env() == :prod, deps: deps(), dialyzer: dialyzer(), preferred_cli_env: [release: :prod, compile: :prod, test: :test], - aliases: [test: "test --no-start"], # Appears to not work at all erlc_options: [ strong_validation: true, @@ -28,42 +27,63 @@ defmodule Stampede.MixProject do ] end + defp service_requirements_map() do + %{ + Services.Discord => fn config_acc -> + config_acc + |> Keyword.update!(:extra_applications, fn app_list -> + [:certifi, :gun, :inets, :jason, :kcl, :mime | app_list] + end) + end + } + end + + def application do + configure_app(Application.fetch_env!(:stampede, :services_to_install)) + end + @doc "Dynamically configure app dependencies for given services" + def configure_app(:all) do + service_requirements_map() + |> Map.keys() + |> configure_app() + end + def configure_app(list) when is_list(list), do: configure_app(list, nil) - def configure_app(mod_list, nil) when is_list(mod_list) do + defp configure_app(mod_list, nil) when is_list(mod_list) do configure_app(mod_list, extra_applications: [:logger, :runtime_tools], - mod: {Stampede.Application, [installed_services: [:dummy]]}, - included_applications: [] + mod: {Stampede.Application, []}, + included_applications: [], + env: [ + installed_services: [Services.Dummy] + ] ) end - def configure_app([first | rest], config_acc) when is_list(config_acc) do - case first do - :discord -> - new_acc = - config_acc - |> Keyword.update!(:mod, fn {mod, kwlist} -> - {mod, - Keyword.update!(kwlist, :installed_services, fn list -> - [:discord | list] - end)} - end) - |> Keyword.update!(:extra_applications, fn app_list -> - [:certifi, :gun, :inets, :jason, :kcl, :mime | app_list] - end) - - configure_app(rest, new_acc) - end + defp configure_app([first_service_name | rest], config_acc) when is_list(config_acc) do + update_fun = Map.fetch!(service_requirements_map(), first_service_name) + + new_acc = + update_fun.(config_acc) + |> Keyword.update!(:env, fn env_ls -> + # always add service name to :installed_services + Keyword.update!(env_ls, :installed_services, fn installed -> + if first_service_name in installed, + do: + raise( + "There's no reason for #{first_service_name |> inspect()} to already be there??" + ) + + [first_service_name | installed] + end) + end) + + configure_app(rest, new_acc) end - def configure_app([], config_acc) when is_list(config_acc), do: config_acc - - def application do - # TODO: determine with config.exs - configure_app([:discord]) - end + defp configure_app([], config_acc) when is_list(config_acc), do: config_acc defp deps do [ @@ -124,7 +144,13 @@ defmodule Stampede.MixProject do {:observer_cli, "~> 1.7", only: :dev}, # Persistant storage, particularly interaction logging - {:memento, "~> 0.3.2"} + {:memento, "~> 0.3.2"}, + + # Erlang match specifications in Elixir style + {:ex2ms, "~> 1.7"}, + + # storage for Services.Dummy + {:ets, "~> 0.9.0"} ## NOTE: this would be great if it supported TOML # {:confispex, "~> 1.1"}, # https://hexdocs.pm/confispex/api-reference.html diff --git a/mix.lock b/mix.lock index 62bba25..a03a994 100644 --- a/mix.lock +++ b/mix.lock @@ -18,44 +18,46 @@ "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"}, "doumi_port": {:hex, :doumi_port, "0.6.0", "0ca6adcc753d9c2a8489adea336a199b4e208823cc5f9368ff34db8df140a6c4", [:mix], [{:erlport, "~> 0.10", [hex: :erlport, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "1d10b5680be9538132d5fb943cf58a25dc9bd8852d1d2ddcc887afa8a05896f9"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"}, "eflambe": {:hex, :eflambe, "0.3.1", "ef0a35084fad1f50744496730a9662782c0a9ebf449d3e03143e23295c5926ea", [:rebar3], [{:meck, "0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "58d5997be606d4e269e9e9705338e055281fdf3e4935cc902c8908e9e4516c5f"}, "eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"}, "equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "erlport": {:hex, :erlport, "0.11.0", "8bb46a520e6eb9146e655fbf9b824433d9d532194667069d9aa45696aae9684b", [:rebar3], [], "hexpm", "8eb136ccaf3948d329b8d1c3278ad2e17e2a7319801bc4cc2da6db278204eee4"}, + "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, + "ex2ms": {:hex, :ex2ms, "1.7.0", "45b9f523d0b777667ded60070d82d871a37e294f0b6c5b8eca86771f00f82ee1", [:mix], [], "hexpm", "2589eee51f81f1b1caa6d08c990b1ad409215fe6f64c73f73c67d36ed10be827"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, - "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, - "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "expo": {:hex, :expo, "1.0.1", "f9e2f984f5b8d195815d52d0ba264798c12c8d2f2606f76fa4c60e8ebe39474d", [:mix], [], "hexpm", "f250b33274e3e56513644858c116f255d35c767c2b8e96a512fe7839ef9306a1"}, "extractable": {:hex, :extractable, "1.0.1", "2cf2b213cbda82eb578c821de48fa313cddfa7573b256bce4306248f56b44509", [:mix], [{:type_check, "~> 0.8", [hex: :type_check, repo: "hexpm", optional: false]}], "hexpm", "820e9db7550a62c7d63b760a8da51ab2341ba425563955db005baadcb403b046"}, - "fast_yaml": {:hex, :fast_yaml, "1.0.36", "65413a34a570fd4e205a460ba602e4ee7a682f35c22d2e1c839025dbf515105c", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "1abe8f758fc2a86b08edff80bbc687cfd41ebc1412cfec0ef4a0acfcd032052f"}, - "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "fast_yaml": {:hex, :fast_yaml, "1.0.37", "f71d472fbf787ccd161b914d1eb486116a0f4f2e835337a378fbd31b59d2e74b", [:rebar3], [{:p1_utils, "1.0.26", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "8de868721bf7e2172414f7d3148ede0f3c922b496455cd625dd5c4429515a769"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, - "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, + "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"}, "insertable": {:hex, :insertable, "1.0.0", "879c7023b5491bbb694dff78422c94faded616fda963e529916343bc6e35162f", [:mix], [{:type_check, "~> 0.8", [hex: :type_check, repo: "hexpm", optional: false]}], "hexpm", "4b3f60169221a1ace67fdfe8b0d3c35708c098e55df549d4a374b9a7b0255af4"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "kcl": {:hex, :kcl, "1.4.2", "8b73a55a14899dc172fcb05a13a754ac171c8165c14f65043382d567922f44ab", [:mix], [{:curve25519, ">= 1.0.4", [hex: :curve25519, repo: "hexpm", optional: false]}, {:ed25519, "~> 1.3", [hex: :ed25519, repo: "hexpm", optional: false]}, {:poly1305, "~> 1.0", [hex: :poly1305, repo: "hexpm", optional: false]}, {:salsa20, "~> 1.0", [hex: :salsa20, repo: "hexpm", optional: false]}], "hexpm", "9f083dd3844d902df6834b258564a82b21a15eb9f6acdc98e8df0c10feeabf05"}, "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, "logger_json": {:hex, :logger_json, "5.1.2", "7dde5f6dff814aba033f045a3af9408f5459bac72357dc533276b47045371ecf", [:mix], [{:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "ed42047e5c57a60d0fa1450aef36bc016d0f9a5e6c0807ebb0c03d8895fb6ebc"}, "logstash_logger_formatter": {:hex, :logstash_logger_formatter, "1.1.4", "ac5ed3d7f52c3ac1d2b35190a84c1eb1ebbf8f73471effa372621568bc819dba", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "e25908720dcaf3ab8004340e5ebcff3d26b0c7ee41ac5ccdcec779abe63ccf69"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "memento": {:hex, :memento, "0.3.2", "38cfc8ff9bcb1adff7cbd0f3b78a762636b86dff764729d1c82d0464c539bdd0", [:mix], [], "hexpm", "25cf691a98a0cb70262f4a7543c04bab24648cb2041d937eb64154a8d6f8012b"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"}, "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "norm": {:hex, :norm, "0.13.0", "2c562113f3205e3f195ee288d3bd1ab903743e7e9f3282562c56c61c4d95dec4", [:mix], [{:stream_data, "~> 0.5", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "447cc96dd2d0e19dcb37c84b5fc0d6842aad69386e846af048046f95561d46d7"}, "nostrum": {:hex, :nostrum, "0.9.1", "52832df6adcd09389d83074bbb7f9e634eb110f178566e6df64314d981e0d0ed", [:mix], [{:castle, "~> 0.3.0", [hex: :castle, repo: "hexpm", optional: false]}, {:certifi, "~> 2.13", [hex: :certifi, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:kcl, "~> 1.4", [hex: :kcl, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "d5697109e07bd1f747b3d2a74b69d003c12210ab12e57ac54d83dcf087de34f5"}, - "observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"}, - "p1_utils": {:hex, :p1_utils, "1.0.25", "2d39b5015a567bbd2cc7033eeb93a7c60d8c84efe1ef69a3473faa07fa268187", [:rebar3], [], "hexpm", "9219214428f2c6e5d3187ff8eb9a8783695c2427420be9a259840e07ada32847"}, + "observer_cli": {:hex, :observer_cli, "1.7.5", "cf73407c40ba3933a4be8be5cdbfcd647a7ec24b49f1d75e912ae1f2e58bc5d4", [:mix, :rebar3], [{:recon, "~> 2.5.5", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "872cf8e833a3a71ebd05420692678ec8aaede8fd96c805a4687398f6b23a3014"}, + "p1_utils": {:hex, :p1_utils, "1.0.26", "67b0c4ac9fa3ba3ef563b31aa111b0a004439a37fac85e027f1c3617e1c7ec6c", [:rebar3], [], "hexpm", "d0379e8c1156b98bd64f8129c1de022fcca4f2fdb7486ce73bf0ed2c3376b04c"}, "poly1305": {:hex, :poly1305, "1.0.4", "7cdc8961a0a6e00a764835918cdb8ade868044026df8ef5d718708ea6cc06611", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "e14e684661a5195e149b3139db4a1693579d4659d65bba115a307529c47dbc3b"}, "quantum": {:hex, :quantum, "3.5.3", "ee38838a07761663468145f489ad93e16a79440bebd7c0f90dc1ec9850776d99", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.14 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "500fd3fa77dcd723ed9f766d4a175b684919ff7b6b8cfd9d7d0564d58eba8734"}, "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"}, @@ -64,12 +66,12 @@ "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "type_check": {:hex, :type_check, "0.13.5", "b41ce95808546e9913d7d56d0886ae8298c4298c9ff596fd7987c5000a2d2316", [:mix], [{:credo, "~> 1.5", [hex: :credo, repo: "hexpm", optional: true]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "6196910e3cec69b4e2cec71f8d9f243532aeeb37924a86e8256bc5ee05b3fd00"}, "uinta": {:hex, :uinta, "0.13.0", "336239c4401814025337b4a5ebea1528be0ba5e391f42690550448c7a034b50b", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "aa5067d26360b18d6e72a96c2921f98f1d7692b40bc9411ea2d677e01ef6b34d"}, "vapor": {:hex, :vapor, "0.10.0", "547a94b381093dea61a4ca2200109385b6e44b86d72d1ebf93e5ac1a8873bc3c", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:norm, "~> 0.9", [hex: :norm, repo: "hexpm", optional: false]}, {:toml, "~> 0.3", [hex: :toml, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.1", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "ee6d089a71309647a0a2a2ae6cf3bea61739a983e8c1310d53ff04b1493afbc1"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, - "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"}, } diff --git a/test/foreign_plugins_test.exs b/test/foreign_plugins_test.exs index f504bee..e51666d 100644 --- a/test/foreign_plugins_test.exs +++ b/test/foreign_plugins_test.exs @@ -1,6 +1,5 @@ defmodule ForeignPluginsTest do use ExUnit.Case, async: true - import ExUnit.CaptureLog alias Services.Dummy, as: D alias Stampede, as: S import AssertValue @@ -17,19 +16,6 @@ defmodule ForeignPluginsTest do vip_ids: MapSet.new([:server]), bot_is_loud: false } - setup_all do - %{ - app_pid: - Stampede.Application.start( - :normal, - installed_services: [:dummy], - services: [:dummy], - log_to_file: false, - log_post_serious_errors: false, - clear_state: true - ) - } - end setup context do id = context.test diff --git a/test/stampede_stateless_test.exs b/test/stampede_stateless_test.exs index 5369352..e7cf1d0 100644 --- a/test/stampede_stateless_test.exs +++ b/test/stampede_stateless_test.exs @@ -29,6 +29,13 @@ defmodule StampedeStatelessTest do bot_is_loud: false } + setup_all do + unless Application.get_env(:stampede, :test_loaded, false), + do: raise("Test config not loaded") + + :ok + end + describe "stateless functions" do test "split_prefix text" do assert_value S.split_prefix("!ping", "!") == {"!", "ping"} @@ -55,6 +62,12 @@ defmodule StampedeStatelessTest do assert_value SiteConfig.check_prefixes_for_conflicts(bl) == :no_conflict end + test "split_prefix whitespace sanity" do + assert S.split_prefix("! ", "!") == {false, "! "} + assert S.split_prefix("! ", "!") == {false, "! "} + assert S.split_prefix("S, ", "S, ") == {false, "S, "} + end + test "cfg prefix conflict sorting" do rev = ["a", "b", "c", "aa", "ab", "ba", "bc", "aaa", "aba", "bbc", "cac", "aaaa", "ddddd"] @@ -218,8 +231,6 @@ defmodule StampedeStatelessTest do %{cfg.service => %{cfg.server_id => cfg}} |> SiteConfig.make_configs_for_dm_handling() - # |> IO.inspect(pretty: true) # Debug - key = S.make_dm_tuple(cfg.service) assert key == diff --git a/test/stampede_test.exs b/test/stampede_test.exs index c120729..2506cc9 100644 --- a/test/stampede_test.exs +++ b/test/stampede_test.exs @@ -4,7 +4,6 @@ defmodule StampedeTest do alias Stampede, as: S alias Services.Dummy, as: D import AssertValue - doctest Stampede @confused_response S.confused_response() |> TxtBlock.to_binary(Services.Dummy) @@ -30,22 +29,16 @@ defmodule StampedeTest do vip_ids: MapSet.new([:server]), bot_is_loud: false } + setup_all do - for app <- Application.spec(:stampede, :applications) do - Application.ensure_all_started(app) - end + # NOTE: sanity checks + unless Application.get_env(:stampede, :test_loaded, false), + do: raise("Test config not loaded") + + unless Process.whereis(Services.Dummy.registry()) != nil, + do: raise("Dummy server probably not running") - %{ - app_pid: - Stampede.Application.start( - :normal, - installed_services: [:dummy], - services: [:dummy], - log_to_file: false, - log_post_serious_errors: false, - clear_state: true - ) - } + :ok end setup context do @@ -198,6 +191,10 @@ defmodule StampedeTest do assert r.text == "pong!" end + + test "Direct messaging with D.ask_bot/1" do + assert_value Map.fetch!(D.ask_bot("ping"), :text) == "pong!" + end end describe "dummy server channels" do @@ -274,7 +271,7 @@ defmodule StampedeTest do test "Why plugin returns error on bad ID", s do D.ask_bot(s.id, :t1, :u1, "!Why did you say that, specifically?", - ref: {s.id, :t1, :system, 9999} + ref: 9_999_999_999_999_999 ) |> Map.fetch!(:text) |> Plugins.Why.Debugging.probably_a_missing_interaction()