Skip to content

Commit

Permalink
add some docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
ProducerMatt committed Mar 18, 2024
1 parent 034dfff commit 3cf82d3
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 35 deletions.
7 changes: 5 additions & 2 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Config

# Extra metadata for the logger to keep
stampede_metadata = [
:stampede_component,
:stampede_msg_id,
Expand All @@ -19,12 +20,12 @@ extra_metadata =
stampede_metadata ++
nostrum_metadata

# Actually start configuring things
config :stampede,
compile_env: Mix.env()

config :logger, :console,
level: :debug,
# extra nostrum metadata
metadata: extra_metadata

config :logger,
Expand All @@ -36,6 +37,7 @@ config :stampede, :logger, [
{:handler, :file_log, :logger_std_h,
%{
config: %{
# Don't mix environment logs
file: ~c"logs/#{Mix.env()}/#{node()}.log",
filesync_repeat_interval: 5000,
file_check: 5000,
Expand All @@ -53,11 +55,12 @@ config :stampede, :logger, [
}}
]

# Discord bot needs these to work
config :nostrum,
gateway_intents: :all

# Don't mix environment databases
config :mnesia,
# Notice the single quotes
dir: ~c".mnesia/#{Mix.env()}/#{node()}"

for config <- "./*.secret.exs" |> Path.expand(__DIR__) |> Path.wildcard() do
Expand Down
22 changes: 14 additions & 8 deletions lib/plugin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ defmodule Plugin do
"""
@type! usage_tuples :: list(String.t() | {String.t(), String.t()})
@callback process_msg(SiteConfig.t(), Msg.t()) :: nil | Response.t()

# TODO: replace with predicate? and handle prefix cleaning seperately
@doc "Given a config and message, indicate if we should respond, and if so what is the relevant part of the message?"
@callback at_module?(SiteConfig.t(), Msg.t()) :: boolean() | {:cleaned, text :: String.t()}
@callback usage() :: usage_tuples()
@callback description() :: String.t()
Expand Down Expand Up @@ -85,10 +88,6 @@ defmodule Plugin do
end)
end

def default_plugin_mfa(plug, [cfg, msg]) do
{plug, :process_msg, [cfg, msg]}
end

@spec! ls(:all | :none | MapSet.t()) :: MapSet.t()
def ls(:none), do: MapSet.new()
def ls(:all), do: ls()
Expand All @@ -97,13 +96,18 @@ defmodule Plugin do
MapSet.intersection(enabled, ls())
end

def default_plugin_mfa(plug, [cfg, msg]) do
{plug, :process_msg, [cfg, msg]}
end

@type! job_result ::
{:job_error, :timeout}
| {:job_error, tuple()}
| {:job_ok, nil}
| {:job_ok, %Response{}}
@type! plugin_job_result :: {atom(), job_result()}

@doc "Attempt some task, safely catch errors, and format the error report for the originating service"
@spec! get_response(S.module_function_args() | atom(), SiteConfig.t(), S.Msg.t()) ::
job_result()
def get_response(plugin, cfg, msg) when is_atom(plugin),
Expand Down Expand Up @@ -164,7 +168,7 @@ defmodule Plugin do
S.quick_task_via(),
__MODULE__,
:get_response,
[default_plugin_mfa(this_plug, [cfg, msg]), cfg, msg]
[this_plug, cfg, msg]
)}
end)

Expand All @@ -187,7 +191,6 @@ defmodule Plugin do
}
end)
|> Enum.map(fn {task, result} ->
# they are reunited :-)
{
task,
case result do
Expand Down Expand Up @@ -219,7 +222,6 @@ defmodule Plugin do
{plug, {:job_error, reason}}
end
end)
|> task_sort()

%{r: chosen_response, tb: traceback} = resolve_responses(task_results)

Expand Down Expand Up @@ -264,6 +266,7 @@ defmodule Plugin do
end
end

@doc "Poll all enabled plugins and choose the most relevant one."
@spec! get_top_response(SiteConfig.t(), Msg.t()) :: nil | Response.t()
def get_top_response(cfg, msg) do
case S.Interact.channel_locked?(msg.channel_id) do
Expand All @@ -289,6 +292,7 @@ defmodule Plugin do
end
end

@doc "Organize plugin results by confidence"
@spec! task_sort(list(plugin_job_result())) :: list(plugin_job_result())
def task_sort(tlist) do
Enum.sort(tlist, fn
Expand Down Expand Up @@ -321,13 +325,15 @@ defmodule Plugin do
end)
end

@doc "Choose best response, creating a traceback along the way."
@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, [])
task_sort(tlist)
|> do_rr(nil, [])
end

def do_rr([], chosen_response, traceback) do
Expand Down
21 changes: 18 additions & 3 deletions lib/service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,48 @@ defmodule Service do
use TypeCheck
alias Stampede, as: S

@doc "return description of valid site config options"
@callback site_config_schema() :: NimbleOptions.t()
@doc "Put the service's internal message representation into a generic Msg"
@callback into_msg(service_message :: any()) :: %Stampede.Msg{}
@doc "Is this service message a DM?"
@callback dm?(service_message :: any()) :: boolean()
@doc "Is this author considered privileged in this context?"
@callback author_privileged?(server_id :: any(), author_id :: any()) :: boolean()
@doc "Is this user the bot itself?"
@callback bot_id?(user_id :: any()) :: boolean()
@doc "Is this message targeted at the bot in a service-specific way?"
@callback at_bot?(
cfg :: SiteConfig.t(),
message :: S.Msg.t()
) :: boolean()
@doc "Send a message on this service"
@callback send_msg(destination :: any(), text :: binary(), opts :: keyword()) :: any()
@doc "Log a safely caught plugin error. Often called from Plugin.get_top_response()"
@callback log_plugin_error(
cfg :: SiteConfig.t(),
message :: S.Msg.t(),
error_info :: PluginCrashInfo.t()
) :: {:ok, formatted :: TxtBlock.t()}
@doc "Report an uncaught error from the Erlang logger. Could have sensitive info for the bot host."
@callback log_serious_error(
log_msg ::
{level :: Stampede.log_level(), _gl :: any(),
{module :: Logger, message :: any(), _timestamp :: any(), _metadata :: any()}}
) :: :ok
@doc "Site configs have been updated and the service should be updated"
@callback reload_configs() :: :ok | {:error, any()}

@doc "Specifies how this service formats TxtBlocks into messages. Non-recursive"
@callback txt_format(blk :: TxtBlock.t(), type :: TxtBlock.type()) :: S.str_list()
@doc "How this service wants plugin errors to be displayed."
@callback format_plugin_fail(
cfg :: SiteConfig.t(),
msg :: S.Msg.t(),
error_info :: PluginCrashInfo.t()
) :: TxtBlock.t()

@doc "Called by Stampede.Application supervisor"
@callback start_link(Keyword.t()) :: :ignore | {:error, any} | {:ok, pid}

defmacro __using__(_opts \\ []) do
Expand Down Expand Up @@ -59,17 +72,19 @@ defmodule Service do
when is_map(cfg) do
cfg
|> SiteConfig.fetch!(:service)
|> apply_service_function(func_name, args)
|> __MODULE__.apply_service_function(func_name, args)
end

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
# Some common functions make more sense being abbreviated here

# TODO: move this 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])
do: __MODULE__.apply_service_function(cfg_or_service, :txt_format, [blk, type])
end
43 changes: 23 additions & 20 deletions lib/service/dummy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,29 @@ defmodule Service.Dummy do
GenServer.call(__MODULE__, {:author_privileged?, server_id, author_id})
end

@impl Service
def at_bot?(_cfg, msg) do
case msg.referenced_msg_id do
nil ->
false

ref ->
transaction!(fn ->
Memento.Query.read(__MODULE__.Table, ref)
|> case do
nil ->
false

found ->
found.user == @bot_user
end
end)
end
end

@impl Service
def bot_id?(id), do: id == @bot_user

@impl Service
def txt_format(blk, kind),
do: TxtBlock.Md.format(blk, kind)
Expand Down Expand Up @@ -342,26 +365,6 @@ defmodule Service.Dummy do
{:reply, dump, state}
end

@impl Service
def at_bot?(_cfg, msg) do
case msg.referenced_msg_id do
nil ->
false

ref ->
transaction!(fn ->
Memento.Query.read(__MODULE__.Table, ref)
|> case do
nil ->
false

found ->
found.user == @bot_user
end
end)
end
end

def handle_call({:author_privileged?, _server_id, author_id}, _from, state) do
# TODO: make VIPs like Discord
case author_id do
Expand Down
25 changes: 23 additions & 2 deletions lib/site_config.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
defmodule SiteConfig do
@moduledoc """
This module defines how per-site configurations are validated and represented.
A configuration usually starts as a YAML file on-disk. It is then:
- read into an Erlang term
- validated with NimbleOptions (simultaneously handling defaults and type-checking)
- some transformations are done; for example, turning atoms referring to services and plugins into their proper names ("discord" into Elixir.Service.Discord, "why" into Elixir.Plugin.Why).
- turned into a SiteConfig struct (internally a map)
- Given to Stampede.CfgTable which handles storage of the configs and keeping services up-to-date.
schema_base() defines a basic site config schema, which is extended by Services for their needs.
"""
use TypeCheck
use TypeCheck.Defstruct
alias Stampede, as: S
Expand All @@ -11,6 +23,7 @@ defmodule SiteConfig do
@type! channel_id :: S.channel_id()
@type! schema :: keyword() | struct()
@type! site_name :: atom()
@typedoc "A nested collection of configs, organized by service, then server_id"
@type! cfg_list :: map(service(), map(server_id(), SiteConfig.t()))
@type! t :: map(atom(), any())

Expand Down Expand Up @@ -76,6 +89,7 @@ defmodule SiteConfig do

def fetch!(cfg, key) when is_map_key(cfg, key), do: Map.fetch!(cfg, key)

@doc "Verify that explicitly listed plugins actually exist"
def real_plugins(:all), do: {:ok, :all}
def real_plugins(:none), do: {:ok, :none}

Expand Down Expand Up @@ -129,6 +143,7 @@ defmodule SiteConfig do
|> Map.new()
end

@doc "Turn plug_name into Elixir.Plugin.PlugName"
def concat_plugs(kwlist, _schema) do
if is_list(Keyword.get(kwlist, :plugs)) do
Keyword.update!(kwlist, :plugs, fn plugs ->
Expand All @@ -137,7 +152,10 @@ defmodule SiteConfig do
:all

ll when is_list(ll) ->
Enum.map(ll, &Module.safe_concat(Plugin, &1))
Enum.map(ll, fn name ->
camel_name = name |> to_string() |> Macro.camelize()
Module.safe_concat(Plugin, camel_name)
end)
|> MapSet.new()
end
end)
Expand All @@ -146,6 +164,7 @@ defmodule SiteConfig do
end
end

@doc "If prefix describes a Regex, compile it"
def make_regex(kwlist, _schema) do
if Keyword.has_key?(kwlist, :prefix) do
Keyword.update!(kwlist, :prefix, fn prefix ->
Expand All @@ -160,6 +179,7 @@ defmodule SiteConfig do
end
end

@doc "For the given keys, make a function that will replace the enumerables at those keys with MapSets"
@spec! make_mapsets(list(atom())) :: (keyword(), any() -> keyword())
def make_mapsets(keys) do
fn kwlist, _schema ->
Expand Down Expand Up @@ -192,6 +212,7 @@ defmodule SiteConfig do
|> load_from_string()
end

@doc "Load all YML files in a directory and return a map of configs"
@spec! load_all(String.t()) :: cfg_list()
def load_all(dir) do
target_dir = dir
Expand Down Expand Up @@ -225,7 +246,7 @@ defmodule SiteConfig do

@spec! make_configs_for_dm_handling(cfg_list()) :: cfg_list()
@doc """
Create a config with key {:dm, service} which all DMs for a service are handled under.
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.
Collects all VIPs for that service and puts them in the DM config.
Expand Down

0 comments on commit 3cf82d3

Please sign in to comment.