~ecs/awkbot

awk irc bot
_http(): don't pass content-length separately
go fmt && go get -u
allow getting/setting arbitrary http headers

refs

awkbot
browse  log 

clone

read-only
https://git.sr.ht/~ecs/awkbot
read/write
git@git.sr.ht:~ecs/awkbot

You can also use your local clone with git send-email.

#awkbot

#who

mostly me (ember), evan had the original idea and wrote parts of the awk side

#what

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)

#where

a private irc channel

#when

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

#why

it seemed like a good idea at the time

#how

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