handle parse errors in awk
go get -u
_http(): more robustly separate headers from body
mostly me (ember), evan had the original idea and wrote parts of the awk side
an awk environment hooked up to irc. i promise it's not as bad of an idea as it sounds
awk snippets (a name attached to a piece of awk code) are stored in postgresql,
along with arbitrary persistent data. the go wrapper runs __onmsg__
with $0
set to the right thing on each message, whose job it is to grab all the rest of
the snippets, concatenate them, and evaluate them. the go environment also
provides a few functions that're necessary for various bots i wanted to
implement
it also runs __ontick__
every 10s, in order to be able to do things that
aren't in response to messages (eg. reminders, or an rss reader)
a private irc channel
i started writing the original bot in april of 2020, evan suggested awk in september of 2021, and i finished the awkification process in december 2022
it seemed like a good idea at the time
bootstrapping an awkbot installation isn't particularly trivial, since the interface for editing snippets is itself written in awk. prepare a database with the provided schema.sql, then import these snippets into it (the first space-separated field is the name, the rest of it is the cmd):
add /^\.add / { sub(/^\.add /, ""); idx = index($0, " "); if (idx == 0) { print "usage: .add <name> <awk script>"; exit }; name = substr($0, 1, idx - 1); cmd = substr($0, idx + 1); f = substr(name, 1, 1); if (f == "/" || f == "{") { print "script name must not start wth / or {"; exit }; parse_excl(cmd, name); add(name, cmd); print "success" }
fn_add function add(name, cmd, args) { args[1] = name; args[2] = cmd; sql("INSERT INTO snippets (name, cmd) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET cmd = $2", args) }
fn_parse_excl function parse_excl(c, name, _, args, r, i, query) { args[1] = name; r = sql("SELECT cmd FROM snippets WHERE name ~ '^fn_' AND name !~ '^__.*__$' AND name != $1 ORDER BY NAME", args, query); for (i = 1; i <= r; i++) { c = c "\n" query[i, "cmd"] }; return _parse(c) }
fn_sql function sql(q, args, r, _, _args, i, j, res, res2, nres) { _args = sql_escape(args); split("", r); nres = split(_sql(q, _args), res, /b/); split(res[1], cols, /a/); for (i in cols) { cols[i] = sql_unescape(cols[i]) }; for (i in res) { split(res[i], res2, /a/); for (j in res2) { r[i - 1, cols[j]] = sql_unescape(res2[j])}}; return nres - 2}
fn_sql_escape function sql_escape(args, _args, arg, i, nargs) { for (i in args) if (i > nargs) nargs = i; for (i = 1; i <= nargs; i++) { arg = args[i]; gsub(/\\/, "\\\\\\\\", arg); gsub(/a/, "\\A", arg); _args = _args arg "a" }; sub(/a$/, "", _args); return _args }
fn_sql_unescape function sql_unescape(s, i, r) { for (i = 1; i <= length(s); i++) { if (substr(s, i, 1) == "\\") { i++; si = substr(s, i, 1); if (si == "\\") { r = r "\\" } else if (si == "A") { r = r "a" } else if (si == "B") { r = r "b" } else { r = r "\\" si } } else r = r substr(s, i, 1) }; return r }
ping /^\.ping$/ { print "pong" }
then add this as well, but manually append each of the snippets whose name starts with "fn_" to it:
__onmsg__ { n = sql("SELECT cmd FROM snippets WHERE name !~ '^__.*__$' and name !~ '^tick_' ORDER BY name", _, query); c = "{ __msg__ = $0; __chan__ = chan() }\n"; for (i = 0; i <= n; i++) c = c query[i, "cmd"] "\n{ $0 = __msg__; setchan(__chan__) }\n"; _eval(c, 1) }
you can check if that's set up correctly by saying ".ping", it should respond with "pong"
once you've done that, you can finish the bootstrap by running the following commands in order:
.add fn_a_bslsh function a_bslsh(s, st) { if (substr(s, st["i"], 1) == "\\") { st["i"]++; if (st["i"] > length(s)) return -1; if (substr(s, st["i"], 1) == "\n") return; }; st["tmp"] = st["tmp"] substr(s, st["i"], 1); }
.add fn_a_dquot function a_dquot(s, st) { if (substr(s, st["i"], 1) == "\"") { for (st["i"]++; substr(s, st["i"], 1) != "\""; st["i"]++) { if (substr(s, st["i"], 1) == "\\") st["i"]++; if (st["i"] > length(s)) return -1; st["tmp"] = st["tmp"] substr(s, st["i"], 1); }; return; }; if (a_bslsh(s, st) == -1) return -1; }
.add fn_a_squot function a_squot(s, st) { if (substr(s, st["i"], 1) == "'") { st["i"]++; match(substr(s, st["i"]), /'/); if (RLENGTH == -1) return -1; st["tmp"] = st["tmp"] substr(s, st["i"], RSTART - 1); st["i"] += RSTART - 1; return; }; if (a_dquot(s, st) == -1) return -1; }
.add fn_shlex function shlex(s, out, _, st) { st["i"] = 0; st["j"] = 0; st["tmp"] = ""; for (st["i"] = 1; st["i"] <= length(s); st["i"]++) { if (substr(s, st["i"], 1) ~ /[ \t\n]/) { if (st["tmp"] != "") out[st["j"]++] = st["tmp"]; st["tmp"] = ""; continue; } if (a_squot(s, st) == -1) return -1; }; if (st["tmp"] != "") out[st["j"]++] = st["tmp"]; return st["j"]; }
.add fn_getfuncs function getfuncs(_, c, r, i, query) { r = sql("SELECT cmd FROM snippets WHERE name ~ '^fn_' AND name !~ '^__.*__$' ORDER BY NAME", _, query); for (i = 1; i <= r; i++) { c = c "\n" query[i, "cmd"] }; return c }
.add fn_parse function parse(c) { return _parse(c getfuncs()) }
.add fn_get function get(key, args, query) { args[1] = key; sql("SELECT value FROM storage WHERE key = $1", args, query); return query[1, "value"] }
.add fn_set function set(key, value, args) { args[1] = key; args[2] = value; sql("INSERT INTO storage (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2", args) }
.add fn_eval function eval(c, passthrough) { return _eval(c getfuncs(), passthrough) }
.add fn_date_lt function date_lt(a, b, args) { if (a == "" || b == "") return 0; args[1] = a; args[2] = b; return sql("SELECT WHERE $1 :: timestamp with time zone < $2 :: timestamp with time zone", args) }
.add addunder /^\.addunder / {sub(/^\.addunder /,"");idx=index($0," ");if(idx==0){print "usage: .addunder <name> <awk script>";exit};name=substr($0,1,idx-1); cmd = substr($0, idx + 1); f = substr(name, 1, 1); if (f == "/" || f == "{") { print "script name must not start wth / or {"; exit }; parse(cmd); set("under." name, cmd); add("__" name "__", cmd getfuncs()); print "success"}
.add lsunder /^\.lsunder$/ { r = sql("SELECT key, value FROM storage WHERE key ~ '^under\\.' ORDER BY key", _, query); for (i = 1; i <= r; i++ ) print ".addunder " substr(query[i, "key"], 7) " " query[i, "value"] }
.add ls /^\.ls($| )/ { n = shlex($0, args); if (n == -1 || n > 3) { print "usage: .ls [<regex> [<regex>]]"; exit }; s = "SELECT name, cmd FROM snippets WHERE name !~ '^__.*__$'"; delete args[0]; if (n > 1) s = s " AND name ~ $1"; if (n > 2)s = s " AND cmd ~ $2"; r = sql(s " ORDER BY name", args, query); for (i = 1; i <= r; i++ ) print ".add " query[i, "name"] " " query[i,"cmd"]}
.add rm /^\.rm / { n = shlex($0, args); if (n != 2) { print "usage: .rm <name>"; exit }; delete args[0]; sql("DELETE FROM snippets WHERE name = $1", args); print "success" }
.addunder __ontick__ { n = sql("SELECT name, cmd FROM snippets WHERE name ~ '^tick_' ORDER BY name", _, query); for (i = 1; i <= n; i++) { t = get("tick." query[i, "name"] ".next"); if (t == "") t = "now"; if (date_lt("now", t)) continue; set("tick." query[i, "name"] ".next", date_add(t, get("tick." query[i, "name"] ".period"))); eval(query[i, "cmd"], 1); }; }
if you have any trouble setting stuff up, or you're curious what other snippets we have in our install, or you just want to say hi, feel free to reach out to me at ecs@d2evs.net or ecs on libera.chat