diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..33f7dff --- /dev/null +++ b/.credo.exs @@ -0,0 +1,14 @@ +%{ + configs: [ + %{ + name: "default", + checks: %{ + disabled: [ + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + ] + } + } + ] +} diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index f4c5a39..e9d3e0b 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -1,31 +1,32 @@ [ {"deps/nostrum/lib/nostrum/consumer.ex", "Function Nostrum.ConsumerGroup.join/1 does not exist."}, {"lib/service/discord.ex", "Function Nostrum.Api.create_message/2 does not exist."}, + {"lib/service/discord.ex", "Function Nostrum.Api.get_user!/1 does not exist."}, + {"lib/service/discord.ex", "Function Nostrum.Struct.User.full_name/1 does not exist."}, {"lib/service/discord.ex", "Function Nostrum.Api.get_channel_message/2 does not exist."}, {"lib/service/discord.ex", "Function Nostrum.Cache.Me.get/0 does not exist."}, - {"lib/service/discord.ex", "Function Nostrum.Struct.User.full_name/1 does not exist."}, {"lib/stampede/application.ex", "Function Mix.env/0 does not exist."}, {"lib/stampede/interact.ex", "Function Mix.env/0 does not exist."}, {"/build/source/lib/elixir/lib/gen_server.ex", "Callback info about the Nostrum.Consumer behaviour is not available."}, + {"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/service/discord.ex", "Function logger_state/0 has no local return."}, {"lib/service/dummy.ex", "@spec for send_msg has more types than are returned by the function."}, {"lib/service/dummy.ex", "Function dummy_channel_id/0 has no local return."}, - {"lib/service/dummy.ex", "Function dummy_msg_id/0 has no local return."}, {"lib/service/dummy.ex", "Function msg_content/0 has no local return."}, {"lib/service/dummy.ex", "Function msg_reference/0 has no local return."}, {"lib/service/dummy.ex", "Function msg_tuple/0 has no local return."}, {"lib/service/dummy.ex", "Function msg_tuple_incoming/0 has no local return."}, {"lib/service/dummy.ex", "Function channel/0 has no local return."}, {"lib/service/dummy.ex", "Function channel_buffers/0 has no local return."}, - {"lib/service/dummy.ex", "Function dummy_servers/0 has no local return."}, {"lib/site_config.ex", "Function schema/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 prefix/0 has no local return."}, {"lib/stampede.ex", "Function module_function_args/0 has no local return."}, - {"lib/stampede.ex", "Function io_list/0 has no local return."}, + {"lib/stampede.ex", "Function str_list/0 has no local return."}, {"lib/stampede.ex", "Function traceback/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."}, @@ -33,6 +34,7 @@ {"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/interact.ex", "Function mod_state/0 has no local return."}, - {"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/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."}, ] diff --git a/.gitignore b/.gitignore index 26c565d..fd5fc73 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ erl_crash.dump ### Elixir Patch ### /doc +/.elixir-tools # Nix resources /.nix-mix diff --git a/config/config.exs b/config/config.exs index 36ad905..50effd3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,12 +1,31 @@ import Config +stampede_metadata = [ + :stampede_component, + :stampede_msg_id, + :stampede_plugin, + :interaction_id +] + +nostrum_metadata = [:shard, :guild, :channel] + +extra_metadata = + [ + :crash_reason, + :error_code, + :file, + :line + ] ++ + stampede_metadata ++ + nostrum_metadata + config :stampede, compile_env: Mix.env() config :logger, :console, level: :debug, # extra nostrum metadata - metadata: [:shard, :guild, :channel] + metadata: extra_metadata config :logger, handle_otp_reports: true, @@ -17,7 +36,7 @@ config :stampede, :logger, [ {:handler, :file_log, :logger_std_h, %{ config: %{ - file: ~c"logs/stampede.log", + file: ~c"logs/#{Mix.env()}/#{node()}.log", filesync_repeat_interval: 5000, file_check: 5000, max_no_bytes: 10_000_000, @@ -29,7 +48,7 @@ config :stampede, :logger, [ format: {LogstashLoggerFormatter, :format}, colors: [enabled: false], level: :all, - metadata: [:shard, :guild, :channel, :stampede_component, :interaction_id] + metadata: extra_metadata ) }} ] @@ -41,6 +60,6 @@ config :mnesia, # Notice the single quotes dir: ~c".mnesia/#{Mix.env()}/#{node()}" -for config <- "../config/*.secret.exs" |> Path.expand(__DIR__) |> Path.wildcard() do +for config <- "./*.secret.exs" |> Path.expand(__DIR__) |> Path.wildcard() do import_config config end diff --git a/lib/plugin.ex b/lib/plugin.ex index c1aec4c..dad9829 100644 --- a/lib/plugin.ex +++ b/lib/plugin.ex @@ -1,6 +1,29 @@ +defmodule PluginCrashInfo do + use TypeCheck + use TypeCheck.Defstruct + + defstruct!( + plugin: _ :: module(), + type: _ :: :throw | :error, + error: _ :: Exception.t(), + stacktrace: _ :: Exception.stacktrace() + ) + + defmacro new(kwlist) do + quote do + struct!( + unquote(__MODULE__), + unquote(kwlist) + ) + end + end +end + defmodule Plugin do use TypeCheck require Logger + require PluginCrashInfo + alias PluginCrashInfo, as: CrashInfo alias Stampede, as: S alias S.{Msg, Response, Interaction} require Interaction @@ -13,7 +36,7 @@ defmodule Plugin do """ @type! usage_tuples :: list(String.t() | {String.t(), String.t()}) @callback process_msg(SiteConfig.t(), Msg.t()) :: nil | Response.t() - @callback is_at_module(SiteConfig.t(), Msg.t()) :: boolean() | {:cleaned, text :: String.t()} + @callback at_module?(SiteConfig.t(), Msg.t()) :: boolean() | {:cleaned, text :: String.t()} @callback usage() :: usage_tuples() @callback description() :: String.t() @@ -22,7 +45,7 @@ defmodule Plugin do @behaviour unquote(__MODULE__) @impl Plugin - def is_at_module(cfg, msg) do + def at_module?(cfg, msg) do # Should we process the message? text = SiteConfig.fetch!(cfg, :prefix) @@ -35,12 +58,26 @@ defmodule Plugin do end end - defoverridable is_at_module: 2 + defoverridable at_module?: 2 end end + @doc "returns loaded modules using the Plugin behavior." + @spec! ls() :: MapSet.t(module()) def ls() do - S.find_submodules(__MODULE__) + S.find_submodules(Plugin) + |> Enum.reduce(MapSet.new(), fn + mod, acc -> + b = + mod.__info__(:attributes) + |> Keyword.get(:behaviour, []) + + if Plugin in b do + MapSet.put(acc, mod) + else + acc + end + end) end def default_plugin_mfa(plug, [cfg, msg]) do @@ -76,25 +113,29 @@ defmodule Plugin do } catch t, e -> - error_type = - case t do - :error -> - "an error" - - :throw -> - "a throw" - end - - st = Exception.format(t, e, __STACKTRACE__) - - log = """ - Message from #{inspect(msg.author_id)} lead to #{error_type} in plugin #{m}: - #{st} - """ - - Logger.error(log) - - _ = spawn(SiteConfig.fetch!(cfg, :service), :log_plugin_error, [cfg, log]) + st = __STACKTRACE__ + + error_info = + CrashInfo.new(plugin: m, type: t, error: e, stacktrace: st) + + {:ok, formatted} = + Service.apply_service_function( + cfg, + :log_plugin_error, + [cfg, msg, error_info] + ) + + Logger.error( + fn -> + formatted + |> TxtBlock.to_str_list(:logger) + |> IO.iodata_to_binary() + end, + crash_reason: {e, st}, + stampede_component: SiteConfig.fetch!(cfg, :service), + stampede_msg_id: msg.id, + stampede_plugin: m + ) {:job_error, {e, st}} end @@ -191,7 +232,6 @@ defmodule Plugin do ) |> S.Interact.record_interaction!() - # TODO: logging interactions chosen_response %Response{callback: {mod, fun, args}} -> @@ -200,7 +240,9 @@ defmodule Plugin do new_tb = [ traceback, - "\nTop response was a callback, so i called it. It responded with: \n\"#{followup.text}\"", + "\nTop response was a callback, so i called it. It responded with: \n\"", + followup.text, + "\"", followup.why ] @@ -225,7 +267,13 @@ defmodule Plugin do Map.update!(response, :why, fn tb -> [ - "Channel #{msg.channel_id} was locked to module #{m}, function #{f}, so we called it.\n" + "Channel ", + msg.channel_id |> inspect(), + "was locked to module ", + m |> inspect(), + ", function ", + "f", + ", so we called it.\n" | tb ] end) @@ -268,7 +316,11 @@ defmodule Plugin do end) end - @spec! resolve_responses(list(plugin_job_result())) :: map() + @spec! resolve_responses(nonempty_list(plugin_job_result())) :: %{ + # NOTE: reversing order from 'nil | response' to 'response | nil' makes Dialyzer not count nil? + r: nil | S.Response.t(), + tb: S.traceback() + } def resolve_responses(tlist) do do_rr(tlist, nil, []) end @@ -286,8 +338,10 @@ defmodule Plugin do traceback ) do do_rr(rest, chosen_response, [ - traceback - | "\nWe asked #{inspect(plug)}, and it decided not to answer." + traceback, + "\nWe asked ", + plug |> inspect(), + ", and it decided not to answer." ]) end @@ -297,8 +351,10 @@ defmodule Plugin do traceback ) do do_rr(rest, chosen_response, [ - traceback - | "\nWe asked #{inspect(plug)}, but it timed out." + traceback, + "\nWe asked ", + plug |> inspect(), + ", but it timed out." ]) end @@ -311,17 +367,23 @@ defmodule Plugin do if response.callback do [ traceback, - "\nWe asked #{inspect(plug)}, and it responded with confidence #{inspect(response.confidence)} offering a callback.\nWhen asked why, it said: \"", + "\nWe asked ", + plug |> inspect(), + ", and it responded with confidence ", + response.confidence |> inspect(), + " offering a callback.\nWhen asked why, it said: \"", response.why, "\"" ] else [ traceback, - """ - We asked #{inspect(plug)}, and it responded with confidence #{inspect(response.confidence)}: - #{S.markdown_quote(response.text)} - """, + "\nWe asked ", + plug |> inspect(), + ", and it responded with confidence ", + response.confidence |> inspect(), + ":\n", + {:quote_block, response.text}, "When asked why, it said: \"", response.why, "\"" @@ -330,8 +392,8 @@ defmodule Plugin do if chosen_response == nil do do_rr(rest, response, [ - tb - | "\nWe chose this response." + tb, + "\nWe chose this response." ]) else do_rr(rest, chosen_response, tb) @@ -347,8 +409,12 @@ defmodule Plugin do rest, chosen_response, [ - traceback - | "\nWe asked #{inspect(plug)}, but there was an error of type #{inspect(val)}." + traceback, + "\nWe asked ", + plug |> inspect(), + ", but there was an error of type ", + val |> inspect(), + "." ] ) end diff --git a/lib/plugin/test.ex b/lib/plugin/test.ex index 927f3bc..8806b34 100644 --- a/lib/plugin/test.ex +++ b/lib/plugin/test.ex @@ -15,7 +15,8 @@ defmodule Plugin.Test do {"a", "(shows channel locks work)"}, {"timeout", "(shows that plugins which time out won't disrupt other plugins)"}, {"raise", "(raises an error which should be reported)"}, - {"throw", "(causes a throw which should be reported)"} + {"throw", "(causes a throw which should be reported)"}, + {"formatting", "(tests plugin text formatting)"} ] end @@ -27,7 +28,7 @@ defmodule Plugin.Test do @spec! process_msg(any(), S.Msg.t()) :: nil | S.Response.t() @impl Plugin def process_msg(cfg, msg) do - case is_at_module(cfg, msg) do + case at_module?(cfg, msg) do {:cleaned, "ping"} -> S.Response.new( confidence: 10, @@ -82,7 +83,7 @@ defmodule Plugin.Test do end def lock_callback(cfg, msg, :b) do - case is_at_module(cfg, msg) do + case at_module?(cfg, msg) do {:cleaned, "b"} -> S.Response.new( confidence: 10, @@ -106,7 +107,7 @@ defmodule Plugin.Test do end def lock_callback(cfg, msg, :c) do - case is_at_module(cfg, msg) do + case at_module?(cfg, msg) do {:cleaned, "c"} -> S.Response.new( confidence: 10, diff --git a/lib/plugin/why.ex b/lib/plugin/why.ex index 0ab4725..dc3a434 100644 --- a/lib/plugin/why.ex +++ b/lib/plugin/why.ex @@ -41,7 +41,7 @@ defmodule Plugin.Why do at_module = at_module_regex() - case is_at_module(cfg, msg) do + case at_module?(cfg, msg) do false -> nil @@ -65,7 +65,7 @@ defmodule Plugin.Why do {:ok, traceback} -> Response.new( confidence: valid_confidence, - text: traceback, + text: traceback |> TxtBlock.to_str_list(cfg.service), origin_msg_id: msg.id, why: ["User asked why I said something, so I told them."] ) diff --git a/lib/service.ex b/lib/service.ex index b896daa..963668b 100644 --- a/lib/service.ex +++ b/lib/service.ex @@ -1,20 +1,30 @@ defmodule Service do use TypeCheck + alias Stampede, as: S @callback site_config_schema() :: NimbleOptions.t() @callback into_msg(service_message :: term()) :: %Stampede.Msg{} + @callback dm?(service_message :: term()) :: boolean() @callback send_msg(destination :: term(), text :: binary(), opts :: keyword()) :: term() - @callback log_plugin_error(cfg :: struct(), log :: binary()) :: :ok + @callback log_plugin_error( + cfg :: SiteConfig.t(), + message :: S.Msg.t(), + error_info :: PluginCrashInfo.t() + ) :: {:ok, formatted :: TxtBlock.t()} @callback log_serious_error( log_msg :: {level :: Stampede.log_level(), _gl :: term(), {module :: Logger, message :: term(), _timestamp :: term(), _metadata :: term()}} ) :: :ok @callback reload_configs() :: :ok | {:error, any()} - @callback author_is_privileged(server_id :: any(), author_id :: any()) :: boolean() + @callback author_privileged?(server_id :: any(), author_id :: any()) :: boolean() - @callback txt_source_block(txt :: binary()) :: binary() - @callback txt_quote_block(txt :: binary()) :: binary() + @callback txt_format(blk :: TxtBlock.t(), type :: TxtBlock.type()) :: S.str_list() + @callback format_plugin_fail( + cfg :: SiteConfig.t(), + msg :: S.Msg.t(), + error_info :: PluginCrashInfo.t() + ) :: TxtBlock.t() @callback start_link(Keyword.t()) :: :ignore | {:error, any} | {:ok, pid} @@ -39,13 +49,22 @@ defmodule Service do end # service polymorphism basically - @spec! apply_service_function(SiteConfig.t(), atom(), list()) :: any() + @spec! apply_service_function(SiteConfig.t() | atom(), atom(), list()) :: any() def apply_service_function(cfg, func_name, args) - when is_atom(func_name) and is_list(args) do - SiteConfig.fetch!(cfg, :service) - |> apply(func_name, args) + when is_map(cfg) do + cfg + |> SiteConfig.fetch!(:service) + |> apply_service_function(func_name, args) end - def txt_source_block(cfg, text), - do: apply_service_function(cfg, :txt_source_block, [text]) + def apply_service_function(service_name, func_name, args) when is_atom(service_name) do + apply(service_name, func_name, args) + end + + # TODO: move into service-generic Stampede.Logger + def txt_format(blk, type, :logger), + do: TxtBlock.Md.format(blk, type) + + def txt_format(blk, type, cfg_or_service), + do: apply_service_function(cfg_or_service, :txt_format, [blk, type]) end diff --git a/lib/service/discord.ex b/lib/service/discord.ex index ceef400..b1edf67 100644 --- a/lib/service/discord.ex +++ b/lib/service/discord.ex @@ -1,6 +1,7 @@ defmodule Service.Discord do alias Stampede, as: S alias S.{Msg} + require Msg use TypeCheck use Supervisor, restart: :permanent require Logger @@ -34,7 +35,12 @@ defmodule Service.Discord do end @impl Service - def send_msg(channel_id, msg, _opts \\ []) do + def send_msg(channel_id, msg, opts \\ []) + + def send_msg(channel_id, msg, opts) when is_list(msg), + do: send_msg(channel_id, msg |> IO.iodata_to_binary(), opts) + + def send_msg(channel_id, msg, _opts) when is_binary(msg) do r = S.text_chunk_regex(@character_limit) for chunk <- @@ -65,7 +71,13 @@ defmodule Service.Discord do if try < 5 do IO.puts( :stderr, - "send_msg: discord message send failure ##{try}, error #{inspect(e, pretty: true)}. Trying again..." + [ + "send_msg: discord message send failure ##", + try, + ", error ", + e |> S.pp(), + ". Trying again..." + ] ) :ok = Process.sleep(500) @@ -79,40 +91,76 @@ defmodule Service.Discord do end @impl Service - def log_plugin_error(cfg, log) do + def format_plugin_fail( + _cfg, + msg = %{service: Service.Discord}, + %PluginCrashInfo{plugin: p, type: t, error: e, stacktrace: st} + ) do + error_type = + case t do + :error -> + "an error" + + :throw -> + "a throw" + end + + [ + "Message from ", + msg.author_id |> Nostrum.Api.get_user!() |> Nostrum.Struct.User.full_name() |> inspect(), + " lead to ", + error_type, + " in plugin ", + inspect(p), + ":\n\n", + {:source_block, [S.pp(e), "\n", S.pp(st)]} + ] + end + + @impl Service + def log_plugin_error(cfg, msg, error_info) do channel_id = SiteConfig.fetch!(cfg, :error_channel_id) + formatted = format_plugin_fail(cfg, msg, error_info) _ = - Nostrum.Api.create_message( - channel_id, - content: log - ) + spawn(fn -> + _ = + send_msg( + channel_id, + formatted + ) + end) - :ok + {:ok, formatted} end @impl Service def log_serious_error(log_msg = {level, _gl, {Logger, message, _timestamp, _metadata}}) do try do # TODO: disable if Discord not connected/working - IO.puts("log_serious_error recieved:\n#{inspect(log_msg, pretty: true)}") + IO.puts(["log_serious_error recieved:\n", inspect(log_msg, pretty: true)]) channel_id = Application.fetch_env!(:stampede, :serious_error_channel_id) - log = - """ - Erlang-level error #{inspect(level)}: - #{message |> S.pp() |> txt_source_block()} - """ + log = [ + "Erlang-level error ", + inspect(level), + "\n", + message + |> TxtBlock.to_str_list(Service.Discord) + |> Service.Discord.txt_format(:source_block) + ] _ = send_msg(channel_id, log) catch t, e -> - IO.puts(""" - ERROR: Logging serious error to Discord failed. We have no option, and resending would probably cause an infinite loop. - - Here's the error: - #{S.pp({t, e})} - """) + IO.puts([ + """ + ERROR: Logging serious error to Discord failed. We have no option, and resending would probably cause an infinite loop. + + Here's the error: + """, + S.pp({t, e}) + ]) end :ok @@ -124,19 +172,16 @@ defmodule Service.Discord do end @impl Service - def author_is_privileged(server_id, author_id) do - GenServer.call(__MODULE__.Handler, {:author_is_privileged, server_id, author_id}) + def author_privileged?(server_id, author_id) do + GenServer.call(__MODULE__.Handler, {:author_privileged?, server_id, author_id}) end @impl Service - def txt_source_block(txt) when is_binary(txt), - do: S.markdown_source(txt) + def txt_format(blk, kind), + do: TxtBlock.Md.format(blk, kind) @impl Service - def txt_quote_block(txt) when is_binary(txt), - do: S.markdown_quote(txt) - - def is_dm(msg), do: msg.guild_id == nil + def dm?(msg), do: msg.guild_id == nil @spec! get_referenced_msg(Msg.t()) :: {:ok, Msg.t()} | {:error, any()} def get_referenced_msg(msg) do @@ -194,10 +239,10 @@ defmodule Service.Discord.Handler do vip_ids: _ :: vips() ) - @spec! is_vip_in_this_context(vips(), Discord.discord_guild_id(), Discord.discord_author_id()) :: + @spec! vip_in_this_context?(vips(), Discord.discord_guild_id(), Discord.discord_author_id()) :: boolean() - def is_vip_in_this_context(vips, server_id, author_id), - do: S.is_vip_in_this_context(vips, server_id, author_id) + def vip_in_this_context?(vips, server_id, author_id), + do: S.vip_in_this_context?(vips, server_id, author_id) def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__) @@ -234,10 +279,10 @@ defmodule Service.Discord.Handler do {:reply, :ok, new_state} end - def handle_call({:author_is_privileged, server_id, author_id}, _from, state) do + def handle_call({:author_privileged?, server_id, author_id}, _from, state) do { :reply, - is_vip_in_this_context(state.vip_ids, server_id, author_id), + vip_in_this_context?(state.vip_ids, server_id, author_id), state } end @@ -260,22 +305,31 @@ defmodule Service.Discord.Handler do discord_msg.guild_id in state.guild_ids -> do_msg_create(discord_msg) - Discord.is_dm(discord_msg) -> - if is_vip_in_this_context(state.vip_ids, discord_msg.guild_id, discord_msg.author.id) do + Discord.dm?(discord_msg) -> + if vip_in_this_context?(state.vip_ids, discord_msg.guild_id, discord_msg.author.id) do do_msg_create(discord_msg) else - Logger.warning(""" - User wanted to DM but is not in vip_ids. \ - Username: #{discord_msg.author |> Nostrum.Struct.User.full_name() |> inspect()} \ - Message: - #{discord_msg.content |> Discord.txt_quote_block()} - """) + Logger.warning(fn -> + [ + "User wanted to DM but is not in vip_ids. \\\n", + "Username: ", + discord_msg.author |> Nostrum.Struct.User.full_name() |> inspect(), + " \\\n", + "Message:\n", + {:quote_block, discord_msg.content} |> TxtBlock.to_str_list(Service.Discord) + ] + end) end true -> - Logger.error( - "guild #{discord_msg.guild_id |> inspect()} NOT found in #{inspect(state.guild_ids)}" - ) + Logger.error(fn -> + [ + "guild ", + discord_msg.guild_id |> inspect(), + " NOT found in ", + inspect(state.guild_ids) + ] + end) end end @@ -332,7 +386,11 @@ end defmodule Service.Discord.Logger do @doc """ Listens for global errors raised from Erlang's logger system. If an error gets thrown in this module or children it would cause an infinite loop. + """ + + # TODO: turn into service-generic Stampede.Logger + use TypeCheck @behaviour :gen_event # alias Stampede, as: S diff --git a/lib/service/dummy.ex b/lib/service/dummy.ex index 0077b22..66bf14e 100644 --- a/lib/service/dummy.ex +++ b/lib/service/dummy.ex @@ -40,6 +40,7 @@ defmodule Service.Dummy do alias Stampede, as: S require S alias S.{Msg, Response} + require Msg use Service @@ -137,16 +138,51 @@ defmodule Service.Dummy do end @impl Service - def log_plugin_error(cfg, log) do + def format_plugin_fail( + _cfg = %{service: Service.Dummy}, + msg = %{service: Service.Dummy}, + %{plugin: p, type: t, error: e, stacktrace: st} + ) do + error_type = + case t do + :error -> + "an error" + + :throw -> + "a throw" + end + + [ + "Message from ", + inspect(msg.author_id), + " lead to ", + error_type, + " in plugin ", + inspect(p), + ":\n\n", + {:source_block, [S.pp(e), "\n", S.pp(st)]} + ] + end + + @impl Service + def log_plugin_error(cfg, msg, error_info) do + formatted = + format_plugin_fail(cfg, msg, error_info) + _ = - send_msg( - SiteConfig.fetch!(cfg, :server_id), - SiteConfig.fetch!(cfg, :error_channel_id), - @system_user, - log - ) + spawn(fn -> + # NOTE: as this function is generally being called inside a GenServer process, spawning a new thread is required. + send_msg( + SiteConfig.fetch!(cfg, :server_id), + SiteConfig.fetch!(cfg, :error_channel_id), + @system_user, + formatted + |> TxtBlock.to_str_list(__MODULE__) + |> IO.iodata_to_binary() + ) + end) - :ok + {:ok, formatted} end # TODO @@ -166,20 +202,19 @@ defmodule Service.Dummy do end @impl Service - def reload_configs() do - GenServer.call(__MODULE__, :reload_configs) - end + def dm?({_id, _server_id = {:dm, __MODULE__}, _channel, _user, _body, _ref}), + do: true - @impl Service - def author_is_privileged(server_id, author_id) do - GenServer.call(__MODULE__, {:author_is_privileged, server_id, author_id}) - end + def dm?(_other), do: false @impl Service - def txt_source_block(txt) when is_binary(txt), do: S.markdown_source(txt) + def author_privileged?(server_id, author_id) do + GenServer.call(__MODULE__, {:author_privileged?, server_id, author_id}) + end @impl Service - def txt_quote_block(txt) when is_binary(txt), do: S.markdown_quote(txt) + def txt_format(blk, kind), + do: TxtBlock.Md.format(blk, kind) @spec! channel_history(dummy_server_id(), dummy_channel_id()) :: channel() def channel_history(server_id, channel) do @@ -211,6 +246,11 @@ defmodule Service.Dummy do new_server(args) end + @impl Service + def reload_configs() do + GenServer.call(__MODULE__, :reload_configs) + end + # PLUMBING @spec! update_state() :: %__MODULE__{} @@ -314,7 +354,7 @@ defmodule Service.Dummy do {:reply, dump, state} end - def handle_call({:author_is_privileged, _server_id, author_id}, _from, state) do + def handle_call({:author_privileged?, _server_id, author_id}, _from, state) do case author_id do @system_user -> {:reply, true, state} diff --git a/lib/stampede.ex b/lib/stampede.ex index 75b864d..efe6708 100644 --- a/lib/stampede.ex +++ b/lib/stampede.ex @@ -12,8 +12,12 @@ defmodule Stampede do @type! module_function_args :: {module(), atom(), tuple() | list()} # BUG: type_check issue #189, iolist() # this stand-in isn't type complete but it'll do - @type! io_list :: String.t() | [] | maybe_improper_list() - @type! traceback :: io_list() + @type! str_list :: + String.t() + | [] + | nonempty_list(lazy(Stampede.str_list())) + + @type! traceback :: TxtBlock.t() @type! enabled_plugs :: :all | [] | nonempty_list(module()) @type! channel_lock_action :: false | {:lock, channel_id(), module_function_args()} | {:unlock, channel_id()} @@ -22,37 +26,37 @@ defmodule Stampede do @type! timestamp :: String.t() @type! service_name :: atom() - @doc "use TypeCheck types in NimpleOptions, takes type expressions like @type!" - defmacro ntc(type) do - quote do - {:custom, TypeCheck, :dynamic_conforms, [TypeCheck.Type.build(unquote(type))]} - end - end + def confused_response(), + do: "*confused beeping*" - @spec! txt_indent(String.t(), String.t()) :: String.t() - def txt_indent(str, prefix \\ " ") when is_binary(str) do - String.split(str, "\n") - |> Enum.map(&[prefix | [&1 | "\n"]]) - |> IO.iodata_to_binary() + def throw_internal_error(msg \\ "*screaming*") do + raise "intentional internal error: #{msg}" end - @spec! markdown_quote(String.t()) :: String.t() - def markdown_quote(str) when is_binary(str) do - txt_indent(str, "> ") + @spec! author_privileged?( + %{server_id: any()}, + %{author_id: any()} + ) :: boolean() + def author_privileged?(cfg, msg) do + Service.apply_service_function(cfg, :author_privileged?, [cfg.server_id, msg.author_id]) end - @spec! markdown_source(String.t()) :: String.t() - def markdown_source(txt) when is_binary(txt) do - """ - ``` - #{txt} - ``` - """ + @spec! vip_in_this_context?(map(), server_id(), user_id()) :: boolean() + def vip_in_this_context?(vips, nil, author_id), + do: author_id in Map.values(vips) + + def vip_in_this_context?(vips, server_id, author_id) do + Enum.any?(vips, fn {this_server, this_author} -> + author_id == this_author and this_server == server_id + end) end - # sef via(key) do - # {:via, Registry, {Stampede.Registry, key}} - # end + @doc "use TypeCheck types in NimpleOptions, takes type expressions same as @type!" + defmacro ntc(type) do + quote do + {:custom, TypeCheck, :dynamic_conforms, [TypeCheck.Type.build(unquote(type))]} + end + end def quick_task_via() do {:via, PartitionSupervisor, {Stampede.QuickTaskSupers, self()}} @@ -74,11 +78,17 @@ defmodule Stampede do @spec! find_submodules(module()) :: MapSet.t(module()) def find_submodules(module_name) do :code.all_available() - |> Enum.map(&(elem(&1, 0) |> to_string)) - |> Enum.filter(&String.starts_with?(&1, to_string(module_name) <> ".")) - |> Enum.sort() - |> Enum.map(&String.to_atom/1) - |> MapSet.new() + |> Enum.reduce(MapSet.new(), fn + {name, _location, _loaded}, acc -> + name + |> List.to_string() + |> String.starts_with?(to_string(module_name) <> ".") + |> if do + MapSet.put(acc, List.to_atom(name)) + else + acc + end + end) end @doc """ @@ -123,17 +133,10 @@ defmodule Stampede do end end - def confused_response(), - do: "*confused beeping*" - - def throw_internal_error(msg \\ "*screaming*") do - raise "intentional internal error: #{msg}" - end - def text_chunk(msg, len, max_pieces, premade_regex \\ nil) when is_bitstring(msg) and is_integer(len) and is_integer(max_pieces) and (is_nil(premade_regex) or is_struct(premade_regex, Regex)) do - r = premade_regex || Regex.compile!("(.{1,#{len}})", "us") + r = premade_regex || text_chunk_regex(len) Regex.scan(r, msg, trim: true, capture: :all_but_first) |> Enum.take(max_pieces) @@ -217,25 +220,7 @@ defmodule Stampede do ) end - @spec! author_is_privileged( - %{server_id: any()}, - %{author_id: any()} - ) :: boolean() - def author_is_privileged(cfg, msg) do - Service.apply_service_function(cfg, :author_is_privileged, [cfg.server_id, msg.author_id]) - end - def reload_service(cfg) do Service.apply_service_function(cfg, :reload_configs, []) end - - @spec! is_vip_in_this_context(map(), server_id(), user_id()) :: boolean() - def is_vip_in_this_context(vips, nil, author_id), - do: author_id in Map.values(vips) - - def is_vip_in_this_context(vips, server_id, author_id) do - Enum.any?(vips, fn {this_server, this_author} -> - author_id == this_author and this_server == server_id - end) - end end diff --git a/lib/stampede/cfg_table.ex b/lib/stampede/cfg_table.ex index 15496f2..8475ad6 100644 --- a/lib/stampede/cfg_table.ex +++ b/lib/stampede/cfg_table.ex @@ -166,7 +166,7 @@ defmodule Stampede.CfgTable do def insert_cfg(cfg) do Logger.info("adding #{cfg.service |> inspect()} server #{cfg.server_id |> inspect()}") - schema = apply(cfg.service, :site_config_schema, []) + schema = Service.apply_service_function(cfg, :site_config_schema, []) _ = SiteConfig.revalidate!(cfg, schema) try_with_table(fn table -> diff --git a/lib/stampede/interact.ex b/lib/stampede/interact.ex index b115be5..2c3b747 100644 --- a/lib/stampede/interact.ex +++ b/lib/stampede/interact.ex @@ -21,7 +21,7 @@ defmodule Stampede.Interact.IntTable do msg_id: S.msg_id(), msg: %Msg{}, response: %Response{}, - traceback: String.t(), + traceback: TxtBlock.t(), channel_lock: S.channel_lock_action() }) end @@ -170,7 +170,7 @@ defmodule Stampede.Interact do msg_id: int.msg.id, msg: int.msg, response: int.response, - traceback: int.traceback |> IO.iodata_to_binary(), + traceback: int.traceback, channel_lock: int.channel_lock ) |> IntTable.validate!() diff --git a/lib/stampede/interaction.ex b/lib/stampede/interaction.ex index f2ef86c..ff3b672 100644 --- a/lib/stampede/interaction.ex +++ b/lib/stampede/interaction.ex @@ -8,7 +8,7 @@ defmodule Stampede.Interaction do plugin: _ :: any(), msg: _ :: Msg, response: _ :: Response, - traceback: [] :: iodata() | String.t(), + traceback: [] :: TxtBlock.t(), channel_lock: false :: S.channel_lock_action() ) diff --git a/lib/stampede/msg.ex b/lib/stampede/msg.ex index 25cc315..d4c2e28 100644 --- a/lib/stampede/msg.ex +++ b/lib/stampede/msg.ex @@ -9,8 +9,17 @@ defmodule Stampede.Msg do channel_id: _ :: S.channel_id(), author_id: _ :: S.user_id(), server_id: _ :: S.server_id(), - referenced_msg_id: nil :: S.msg_id() + referenced_msg_id: nil :: S.msg_id(), + service: _ :: module() ) - def new(keys), do: struct!(__MODULE__, keys) + defmacro new(keys) do + quote do + struct!( + unquote(__MODULE__), + unquote(keys) + |> Keyword.put_new(:service, __MODULE__) + ) + end + end end diff --git a/lib/stampede/response.ex b/lib/stampede/response.ex index e07ec2c..90b96bd 100644 --- a/lib/stampede/response.ex +++ b/lib/stampede/response.ex @@ -36,6 +36,16 @@ defmodule Stampede.Response do struct!( unquote(__MODULE__), Keyword.put_new(unquote(keys), :origin_plug, __MODULE__) + |> Keyword.update!(:text, fn + str when is_binary(str) -> + str + + nil -> + nil + + iodata when is_list(iodata) -> + iodata |> IO.iodata_to_binary() + end) ) end end diff --git a/lib/txt_block.ex b/lib/txt_block.ex new file mode 100644 index 0000000..3974a92 --- /dev/null +++ b/lib/txt_block.ex @@ -0,0 +1,97 @@ +defmodule TxtBlock do + @doc """ + Storage for text to be formatted differently according to context, i.e. posting to different services. str_list-friendly. + + Section types with their markdown equivalents: + - quote_block (greater-than signs '>') + - source_block (triple backticks) + - source (single backticks) + - list, dotted (list starting with '-') + - list, numbered (list starting with numbers) + """ + + use TypeCheck + alias Stampede, as: S + + @type! block :: {type(), lazy(t())} + @type! type :: + :quote_block + | :source_block + | :source + | {:indent, pos_integer() | String.t()} + | {:list, :dotted | :numbered} + @type! t :: [] | nonempty_list(lazy(t())) | String.t() | lazy(block) + + @spec! to_str_list(t(), module()) :: S.str_list() + def to_str_list(txt, _service_name) + when is_binary(txt), + do: txt + + def to_str_list({type, blk}, service_name) do + to_str_list(blk, service_name) + |> Service.txt_format(type, service_name) + end + + def to_str_list(blk, service_name) when is_list(blk) do + # TODO: check performance of flattened vs non-flattened lists + List.foldl(blk, [], fn + [], acc -> + acc + + item, acc -> + to_str_list(item, service_name) + |> case do + [] -> + acc + + other -> + acc ++ List.wrap(other) + end + end) + |> case do + [] -> + [] + + [singleton] -> + singleton + + other -> + other + end + end + + @spec! plain_indent_io(S.str_list(), String.t() | non_neg_integer()) :: S.str_list() + def plain_indent_io(str, n) when is_integer(n), + do: str |> plain_indent_io(String.duplicate(" ", n)) + + def plain_indent_io(str, prefix) when is_binary(prefix) do + IO.iodata_to_binary(str) + |> String.split("\n", trim: true) + |> Enum.flat_map(&[prefix, &1, "\n"]) + end +end + +defmodule TxtBlock.Debugging do + use TypeCheck + + @spec! all_formats_example() :: TxtBlock.t() + def all_formats_example() do + [ + "Testing formats.\n\n", + "Quoted\n", + {:quote_block, "Quoted line 1\nQuoted line 2\n"}, + "\n", + {:source_block, "source(1)\nsource(2)\n"}, + "\n", + ["Inline source quote ", {:source, "foobar"}, "\n"], + "\n", + {{:indent, "><> "}, ["school\n", "of\n", "fishies\n"]}, + "\n", + "Dotted list\n", + {{:list, :dotted}, ["Item 1", "Item 2", "Item 3"]}, + "\n", + "Numbered list\n", + {{:list, :numbered}, ["Item 1", "Item 2", "Item 3"]} + ] + end +end diff --git a/lib/txt_block/md.ex b/lib/txt_block/md.ex new file mode 100644 index 0000000..8e55ae3 --- /dev/null +++ b/lib/txt_block/md.ex @@ -0,0 +1,83 @@ +defmodule TxtBlock.Md do + use TypeCheck + alias Stampede, as: S + + @spec! format(TxtBlock.t(), TxtBlock.type()) :: S.str_list() + def format(input, type) + + def format(txt, {:indent, n}), + do: TxtBlock.plain_indent_io(txt, n) + + def format(txt, :quote_block) do + TypeCheck.conforms!(txt, S.str_list()) + + TxtBlock.plain_indent_io(txt, "> ") + end + + def format(txt, :source_block) do + TypeCheck.conforms!(txt, S.str_list()) + + [ + "```\n", + txt, + "```\n" + ] + end + + def format(txt, :source) do + TypeCheck.conforms!(txt, S.str_list()) + + ["`", txt, "`"] + end + + def format(items, {:list, :dotted}) when is_list(items) do + Enum.flat_map(items, fn blk -> + ["- ", blk, "\n"] + end) + end + + def format(items, {:list, :numbered}) when is_list(items) do + Enum.map_reduce(items, 0, fn blk, i -> + j = i + 1 + + { + [j |> Integer.to_string(), ". ", blk, "\n"], + j + } + end) + |> elem(0) + end + + defmodule Debugging do + def all_formats_processed() do + """ + Testing formats. + + Quoted + > Quoted line 1 + > Quoted line 2 + + ``` + source(1) + source(2) + ``` + + Inline source quote `foobar` + + ><> school + ><> of + ><> fishies + + Dotted list + - Item 1 + - Item 2 + - Item 3 + + Numbered list + 1. Item 1 + 2. Item 2 + 3. Item 3 + """ + end + end +end diff --git a/mix.exs b/mix.exs index 7867c32..f957c01 100644 --- a/mix.exs +++ b/mix.exs @@ -22,6 +22,7 @@ defmodule Stampede.MixProject do ] end + @doc "Dynamically configure app dependencies for given services" 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 @@ -60,24 +61,44 @@ defmodule Stampede.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:dialyxir, "~> 1.4", runtime: false}, - {:credo, "~> 1.7", runtime: false}, - {:ex_doc, "~> 0.31", only: :dev, runtime: false}, + # Checking + {:ex_check, "~> 0.16.0", only: [:dev], runtime: false}, + {:credo, ">= 0.0.0", only: [:dev], runtime: false}, + {:dialyxir, ">= 0.0.0", only: [:dev], runtime: false}, + {:doctor, ">= 0.0.0", only: [:dev], runtime: false}, + {:ex_doc, ">= 0.0.0", only: [:dev], runtime: false}, + {:gettext, ">= 0.0.0", only: [:dev], runtime: false}, + {:sobelow, ">= 0.0.0", only: [:dev], runtime: false}, + {:mix_audit, ">= 0.0.0", only: [:dev], runtime: false}, + + # RUNTIME TYPE CHECKING + # https://hexdocs.pm/type_check/readme.html + {:type_check, "~> 0.13.5"}, + # for type checking streams + {:stream_data, "~> 0.5.0"}, + + # Benchmarking + {:benchee, "~> 1.1", runtime: false, only: :dev}, + + # SERVICES # {:nostrum, "~> 0.8.0", runtime: false}, {:nostrum, github: "Kraigie/nostrum", runtime: false}, + + # For catching Erlang errors and sending to services {:logger_backends, "~> 1.0"}, - {:type_check, "~> 0.13.5"}, - # https://hexdocs.pm/type_check/readme.html - {:stream_data, "~> 0.5.0"}, - # for type checking streams + + # For site configs {:fast_yaml, "~> 1.0"}, - {:gen_stage, "~> 1.2"}, - # https://hexdocs.pm/gen_stage/GenStage.html - {:nimble_options, "~> 1.0"}, + # NimbleOptions generates docs with its definitions. Use for site configs. # https://hexdocs.pm/nimble_options/NimbleOptions.html + {:nimble_options, "~> 1.0"}, + + # JSON logging {:logstash_logger_formatter, "~> 1.1"}, - {:benchee, "~> 1.1", runtime: false, only: :dev}, + + # Persistant storage, particularly interaction logging {:memento, "~> 0.3.2"} + ## 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 e6fae56..b53748b 100644 --- a/mix.lock +++ b/mix.lock @@ -1,24 +1,29 @@ %{ - "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castle": {:hex, :castle, "0.3.0", "47b1a550b2348a6d7e60e43ded1df19dca601ed21ef6f267c3dbb1b3a301fbf5", [:mix], [{:forecastle, "~> 0.1.0", [hex: :forecastle, repo: "hexpm", optional: false]}], "hexpm", "dbdc1c171520c4591101938a3d342dec70d36b7f5b102a5c138098581e35fcef"}, - "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, + "certifi": {:hex, :certifi, "2.13.0", "e52be248590050b2dd33b0bb274b56678f9068e67805dca8aa8b1ccdb016bbf6", [:rebar3], [], "hexpm", "8f3d9533a0f06070afdfd5d596b32e21c6580667a492891851b0e2737bc507a1"}, "chacha20": {:hex, :chacha20, "1.0.4", "0359d8f9a32269271044c1b471d5cf69660c362a7c61a98f73a05ef0b5d9eb9e", [:mix], [], "hexpm", "2027f5d321ae9903f1f0da7f51b0635ad6b8819bc7fe397837930a2011bc2349"}, "confispex": {:hex, :confispex, "1.1.0", "f74a5aa9ef5701d0c0f522fe573c983b25aa2fac5cd1ca4bcbbc5968d444b2c9", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: false]}], "hexpm", "ada453219777fa92afd658f2968fb9ecf83c06c926843b10218ea2f676d33120"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"}, "curve25519": {:hex, :curve25519, "1.0.5", "f801179424e4012049fcfcfcda74ac04f65d0ffceeb80e7ef1d3352deb09f5bb", [:mix], [], "hexpm", "0fba3ad55bf1154d4d5fc3ae5fb91b912b77b13f0def6ccb3a5d58168ff4192d"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, + "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"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"}, "equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, + "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, + "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"}, + "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "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, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "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"}, "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "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"}, @@ -26,18 +31,20 @@ "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.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"}, + "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, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"}, "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.2", "6cd5c5e2edbc9298629c85347b39fb3210656e541153826efd0b2a63767f3395", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "68d2f06f96b9c445a23434c9d5f09682866a5b4e90f631829db1c64f140e795b"}, "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"}, - "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "norm": {:hex, :norm, "0.13.0", "2c562113f3205e3f195ee288d3bd1ab903743e7e9f3282562c56c61c4d95dec4", [:mix], [{:stream_data, "~> 0.5", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "447cc96dd2d0e19dcb37c84b5fc0d6842aad69386e846af048046f95561d46d7"}, - "nostrum": {:git, "https://github.com/Kraigie/nostrum.git", "1ec397fda41d4dd345aaeba471b88c8ccded920f", []}, + "nostrum": {:git, "https://github.com/Kraigie/nostrum.git", "d2daf4941927bc4452a4e79acbef4a574ce32f57", []}, "p1_utils": {:hex, :p1_utils, "1.0.25", "2d39b5015a567bbd2cc7033eeb93a7c60d8c84efe1ef69a3473faa07fa268187", [:rebar3], [], "hexpm", "9219214428f2c6e5d3187ff8eb9a8783695c2427420be9a259840e07ada32847"}, "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"}, "salsa20": {:hex, :salsa20, "1.0.4", "404cbea1fa8e68a41bcc834c0a2571ac175580fec01cc38cc70c0fb9ffc87e9b", [:mix], [], "hexpm", "745ddcd8cfa563ddb0fd61e7ce48d5146279a2cf7834e1da8441b369fdc58ac6"}, + "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, diff --git a/test/stampede_stateless_test.exs b/test/stampede_stateless_test.exs index 2f96263..65c6f29 100644 --- a/test/stampede_stateless_test.exs +++ b/test/stampede_stateless_test.exs @@ -2,6 +2,7 @@ defmodule StampedeStatelessTest do use ExUnit.Case, async: true import ExUnit.CaptureLog alias Stampede, as: S + require S.Msg alias Service.Dummy, as: D doctest Stampede @@ -70,12 +71,7 @@ defmodule StampedeStatelessTest do server_id: :none ) - try do - Plugin.Test.process_msg(dummy_cfg, msg) - catch - _t, e -> - assert is_struct(e, SillyError) - end + assert_raise SillyError, fn -> Plugin.Test.process_msg(dummy_cfg, msg) end msg = S.Msg.new( @@ -133,20 +129,20 @@ defmodule StampedeStatelessTest do test "vip check" do vips = %{some_server: :admin} - assert S.is_vip_in_this_context( + assert S.vip_in_this_context?( vips, :some_server, :admin ) - assert S.is_vip_in_this_context( + assert S.vip_in_this_context?( vips, nil, :admin ) assert false == - S.is_vip_in_this_context( + S.vip_in_this_context?( vips, :some_server, :non_admin @@ -186,4 +182,112 @@ defmodule StampedeStatelessTest do assert result == %{foo: MapSet.new([:bar, :baz])} end end + + describe "text formatting" do + test "flattens lists" do + input = [[[], []], [["f"], ["o", "o"]], ["b", ["a"], "r"]] + wanted = ["f", "o", "o", "b", "a", "r"] + + assert wanted == TxtBlock.to_str_list(input, Service.Dummy) + assert "lol" == TxtBlock.to_str_list([[[[[], [[[["lol"], []]]]]]]], Service.Dummy) + end + + test "source block" do + correct = + """ + ``` + foo + ``` + """ + + one = + TxtBlock.to_str_list( + {:source_block, "foo\n"}, + Service.Dummy + ) + |> IO.iodata_to_binary() + + two = + TxtBlock.to_str_list( + {:source_block, [["f"], [], "o", [["o"], "\n"]]}, + Service.Dummy + ) + |> IO.iodata_to_binary() + + assert one == correct + assert two == correct + end + + test "source ticks" do + correct = "`foo`" + + one = + TxtBlock.to_str_list( + {:source, "foo"}, + Service.Dummy + ) + |> IO.iodata_to_binary() + + two = + TxtBlock.to_str_list( + {:source, [["f"], [], "o", [["o"]]]}, + Service.Dummy + ) + |> IO.iodata_to_binary() + + assert one == correct + assert two == correct + end + + test "quote block" do + correct = "> foo\n> bar\n" + + one = + TxtBlock.to_str_list( + {:quote_block, "foo\nbar"}, + Service.Dummy + ) + |> IO.iodata_to_binary() + + two = + TxtBlock.to_str_list( + {:quote_block, [["f"], [], "o", [["o"]], ["\n", "bar"]]}, + Service.Dummy + ) + |> IO.iodata_to_binary() + + assert one == correct + assert two == correct + end + + test "indent block" do + correct = " foo\n bar\n" + + one = + TxtBlock.to_str_list( + {{:indent, " "}, "foo\nbar"}, + Service.Dummy + ) + |> IO.iodata_to_binary() + + two = + TxtBlock.to_str_list( + {{:indent, 2}, [["f"], [], "o", [["o"]], ["\n", "bar"]]}, + Service.Dummy + ) + |> IO.iodata_to_binary() + + assert one == correct + assert two == correct + end + + test "Markdown" do + processed = + TxtBlock.Debugging.all_formats_example() + |> TxtBlock.to_str_list(Service.Dummy) + |> IO.iodata_to_binary() + + assert processed == TxtBlock.Md.Debugging.all_formats_processed() + end + end end diff --git a/test/stampede_test.exs b/test/stampede_test.exs index fe344ea..5cc3f90 100644 --- a/test/stampede_test.exs +++ b/test/stampede_test.exs @@ -82,7 +82,11 @@ defmodule StampedeTest do end test "plugin raising", s do - {result, log} = with_log(fn -> D.send_msg(s.id, :t1, :u1, "!raise") end) + {result, log} = + with_log(fn -> + D.send_msg(s.id, :t1, :u1, "!raise") + end) + assert match?(%{text: @confused_response}, result) assert String.contains?(log, "SillyError"), "SillyError not thrown" @@ -154,20 +158,22 @@ defmodule StampedeTest do end test "many messages", s do - dummy_messages = + expected = 0..9 |> Enum.map(fn x -> {:t1, :u1, "#{x}"} end) - |> Enum.reduce({[], 0}, fn {a, u, m}, lst -> + |> Enum.reduce([], fn {a, u, m}, lst -> D.send_msg(s.id, a, u, m) - [{:_, {u, m, nil}} | lst] + [{u, m, nil} | lst] end) + |> Enum.reverse() - assert match?( - dummy_messages, - D.channel_history(s.id, :t1) - ) + published = + D.channel_history(s.id, :t1) + |> Enum.map(&elem(&1, 1)) + + assert expected == published end end