Initial commit
commit
83bb66d94c
|
@ -0,0 +1,2 @@
|
||||||
|
bots/*/bot
|
||||||
|
bots/*/.config
|
|
@ -0,0 +1,74 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "help",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlCharaHelp",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "learn",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlGrammarLearn",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "unlearn",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlGrammarUnlearn",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "forget",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlGrammarUnlearn",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "show",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlGrammarShow",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ping",
|
||||||
|
"grammar": "ping.json",
|
||||||
|
"handler": "",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pool",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlPoolLook",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "pooladd",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlPoolAdd",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "poolls",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlPoolList",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "poolrm",
|
||||||
|
"grammar": "",
|
||||||
|
"handler": "hdlPoolRemove",
|
||||||
|
"allowOverwrite": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "8ball",
|
||||||
|
"grammar": "8ball.json",
|
||||||
|
"handler": "",
|
||||||
|
"allowOverwrite": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "burger",
|
||||||
|
"grammar": "burger.json",
|
||||||
|
"handler": "",
|
||||||
|
"allowOverwrite": true
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"debug": false,
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"host": "ircserver.tld",
|
||||||
|
"port": 6697,
|
||||||
|
"user": "botUser",
|
||||||
|
"pass": "botPassword",
|
||||||
|
"nick": "botNick",
|
||||||
|
"channels": ["#channel1", "#channel2"],
|
||||||
|
"codePrefix": "!",
|
||||||
|
"admins": [
|
||||||
|
{
|
||||||
|
"user": "adminUser",
|
||||||
|
"pass": "adminPass"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"origin": [
|
||||||
|
"it is certain",
|
||||||
|
"it is decidedly so",
|
||||||
|
"without a doubt",
|
||||||
|
"yes — definitely",
|
||||||
|
"you may rely on it",
|
||||||
|
"as I see it, yes",
|
||||||
|
"most likely",
|
||||||
|
"outlook good",
|
||||||
|
"yes",
|
||||||
|
"signs point to yes",
|
||||||
|
"reply hazy try again",
|
||||||
|
"ask again later",
|
||||||
|
"better not tell you now",
|
||||||
|
"cannot predict now",
|
||||||
|
"concentrate and ask again",
|
||||||
|
"don't count on it",
|
||||||
|
"my reply is no",
|
||||||
|
"my sources say no",
|
||||||
|
"outlook not so good",
|
||||||
|
"very doubtful"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"patty": [
|
||||||
|
"coconut",
|
||||||
|
"nutty",
|
||||||
|
"possible",
|
||||||
|
"soy",
|
||||||
|
"tofu",
|
||||||
|
"veggie delight"
|
||||||
|
],
|
||||||
|
"veggie": [
|
||||||
|
"kimchi",
|
||||||
|
"jalapeño peppers",
|
||||||
|
"lettuce",
|
||||||
|
"onions",
|
||||||
|
"pickles",
|
||||||
|
"tomatoes"
|
||||||
|
],
|
||||||
|
"cheese": [
|
||||||
|
"vegan mozzarella cheese"
|
||||||
|
],
|
||||||
|
"sauce": [
|
||||||
|
"barbecue sauce",
|
||||||
|
"curry sauce",
|
||||||
|
"Dijon mustard",
|
||||||
|
"ketchup"
|
||||||
|
],
|
||||||
|
"burger": [
|
||||||
|
"#patty# burger with #veggie#, #veggie# and #cheese# in #sauce#",
|
||||||
|
"#patty# burger with #veggie#, #veggie# and #veggie# in #sauce#"
|
||||||
|
],
|
||||||
|
"side": [
|
||||||
|
"avocado salad",
|
||||||
|
"BBQ baked beans",
|
||||||
|
"BBQ potato chips",
|
||||||
|
"caesar salad",
|
||||||
|
"coleslaw",
|
||||||
|
"corn on the cob",
|
||||||
|
"corn salad",
|
||||||
|
"french fries",
|
||||||
|
"french potato salad",
|
||||||
|
"fried pickles",
|
||||||
|
"fried shishito peppers",
|
||||||
|
"a fruit bowl",
|
||||||
|
"vegan mac and cheese",
|
||||||
|
"onion rings",
|
||||||
|
"potato wedges",
|
||||||
|
"roasted tomatoes",
|
||||||
|
"sweet potato fries",
|
||||||
|
"a three-bean salad",
|
||||||
|
"tempura green beans",
|
||||||
|
"zucchini chips"
|
||||||
|
],
|
||||||
|
"origin": [
|
||||||
|
"Here, have a #burger#!",
|
||||||
|
"Here, have a #burger#, served with #side#!"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{ "origin": ["pong!"] }
|
|
@ -0,0 +1,12 @@
|
||||||
|
import ../../cirrus
|
||||||
|
import ../../modules/[grammar, javapool]
|
||||||
|
import chara
|
||||||
|
|
||||||
|
|
||||||
|
# Initialise cirrus
|
||||||
|
var
|
||||||
|
config = loadIrcConfig()
|
||||||
|
states = initServers(config.servers)
|
||||||
|
|
||||||
|
# Initialise grammar
|
||||||
|
listenGrammar(states)
|
|
@ -0,0 +1,18 @@
|
||||||
|
import std/[strutils, tables]
|
||||||
|
import ../../cirrus
|
||||||
|
from ../../modules/grammar import getGrammarNames
|
||||||
|
|
||||||
|
|
||||||
|
proc hdlCharaHelp*(st: ServerState, msg: Message): void =
|
||||||
|
var
|
||||||
|
names = getGrammarNames(st.codePrefix)
|
||||||
|
help = "Hi, I'm " & st.nick & ", a bot with random features. " &
|
||||||
|
"You can find my source code at https://git.tilde.town/mio/cirrus. " &
|
||||||
|
"I'll respond to these phrases: " & join(names, ", ")
|
||||||
|
message(st.sock, msg.replyTo, help)
|
||||||
|
|
||||||
|
|
||||||
|
when defined(nimHasUsed):
|
||||||
|
{.used.}
|
||||||
|
|
||||||
|
handlers["hdlCharaHelp"] = hdlCharaHelp
|
|
@ -0,0 +1,6 @@
|
||||||
|
#!/sbin/openrc-run
|
||||||
|
|
||||||
|
name="javapool"
|
||||||
|
command="$HOME/bin/cirrus/bots/javapool/bot"
|
||||||
|
pidfile="/var/run/javapool.pid"
|
||||||
|
start_stop_daemon_args="--background --make-pidfile"
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=javapool — an IRC bot
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=%h/bin/cirrus/bots/javapool
|
||||||
|
ExecStart=./bot
|
||||||
|
Restart=always
|
||||||
|
RestartSec=300
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
|
@ -0,0 +1,340 @@
|
||||||
|
import std/[base64, json, jsonutils, net, os, sequtils, strutils, tables]
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
Admin* = tuple
|
||||||
|
user: string
|
||||||
|
pass: string
|
||||||
|
|
||||||
|
Server* = tuple
|
||||||
|
host: string
|
||||||
|
port: int
|
||||||
|
user: string
|
||||||
|
pass: string
|
||||||
|
nick: string
|
||||||
|
channels: seq[string]
|
||||||
|
codePrefix: string
|
||||||
|
admins: seq[Admin]
|
||||||
|
|
||||||
|
ServerState* = tuple
|
||||||
|
host: string
|
||||||
|
sock: Socket
|
||||||
|
isAuth: bool
|
||||||
|
channels: seq[string]
|
||||||
|
codePrefix: string
|
||||||
|
nick: string
|
||||||
|
admins: seq[Admin]
|
||||||
|
|
||||||
|
State* = seq[ServerState]
|
||||||
|
|
||||||
|
Message* = tuple
|
||||||
|
code: string
|
||||||
|
codeParams: seq[string]
|
||||||
|
sender: string
|
||||||
|
recipient: string
|
||||||
|
replyTo: string
|
||||||
|
body: string
|
||||||
|
|
||||||
|
IrcConfig* = tuple
|
||||||
|
debug: bool
|
||||||
|
servers: seq[Server]
|
||||||
|
|
||||||
|
SvcCode* = tuple
|
||||||
|
code: string
|
||||||
|
handler: string
|
||||||
|
|
||||||
|
SvcCodes* = seq[SvcCode]
|
||||||
|
|
||||||
|
|
||||||
|
# Look for .config directory in the bot executable's directory
|
||||||
|
# or in $HOME/.config/cirrus
|
||||||
|
var ircConfigDir*: string
|
||||||
|
if dirExists(unixToNativePath(getAppDir() & "/.config")):
|
||||||
|
ircConfigDir = getAppDir() & "/.config/"
|
||||||
|
else:
|
||||||
|
ircConfigDir = getConfigDir() & "cirrus/"
|
||||||
|
|
||||||
|
var
|
||||||
|
ircConfigFile* = ircConfigDir & "config.json"
|
||||||
|
ircCodeFile* = ircConfigDir & "codes.json"
|
||||||
|
ircDebug* = false
|
||||||
|
serviceCodes*: SvcCodes
|
||||||
|
handlers* = initTable[string, proc(st: ServerState, msg: Message): void]()
|
||||||
|
|
||||||
|
|
||||||
|
# Echo string to stdout.
|
||||||
|
proc debug*(tag: string, str: string): void =
|
||||||
|
if ircDebug:
|
||||||
|
echo join(["[debug]", "[", tag, "] ", str])
|
||||||
|
|
||||||
|
|
||||||
|
# Load JSON config file and return a config tuple of keys and values.
|
||||||
|
proc loadIrcConfig*(file = ircConfigFile): IrcConfig =
|
||||||
|
if fileExists(unixToNativePath(file)):
|
||||||
|
try:
|
||||||
|
var
|
||||||
|
nodes = parseFile(unixToNativePath(file))
|
||||||
|
conf = to(nodes, IrcConfig)
|
||||||
|
if conf.debug.type() is bool:
|
||||||
|
ircDebug = conf.debug
|
||||||
|
return conf
|
||||||
|
except IOError:
|
||||||
|
debug("loadIrcConfig", "error: could not read config file " & file)
|
||||||
|
except JsonParsingError:
|
||||||
|
debug("loadIrcConfig", "error: invalid JSON in config file " & file)
|
||||||
|
else:
|
||||||
|
debug("loadIrcConfig", "error: cannot find config file " & file)
|
||||||
|
|
||||||
|
|
||||||
|
# Connect to a server at the given host and port. Return a Socket object if
|
||||||
|
# successful, or output an error otherwise.
|
||||||
|
proc connectServer*(host: string, port: int, useSSL = true): Socket =
|
||||||
|
var sock = newSocket()
|
||||||
|
if useSSL:
|
||||||
|
var ctx = newContext(verifyMode = CVerifyNone)
|
||||||
|
wrapSocket(ctx, sock)
|
||||||
|
try:
|
||||||
|
connect(sock, host, Port(port))
|
||||||
|
except OSError:
|
||||||
|
debug("connectServer", "error: cannot connect to the server " & host &
|
||||||
|
"/" & intToStr(port))
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
# Wrap net's send() with carriage return and debug echo.
|
||||||
|
proc sendServer*(sock: Socket, str: string): void =
|
||||||
|
send(sock, str & "\r\n")
|
||||||
|
debug("sendServer", str)
|
||||||
|
|
||||||
|
|
||||||
|
# Wrap net's recvLine() with debug echo.
|
||||||
|
proc recvServer*(sock: Socket, debug = true): string =
|
||||||
|
var str = recvLine(sock)
|
||||||
|
debug("recvServer", str)
|
||||||
|
return str
|
||||||
|
|
||||||
|
|
||||||
|
# Send an IRC-formatted message to the server for a channel or user.
|
||||||
|
proc message*(sock: Socket, recipient: string, str: string): void =
|
||||||
|
sendServer(sock, join(["PRIVMSG ", recipient, " :", str]))
|
||||||
|
|
||||||
|
|
||||||
|
# Disconnect from a server with an optional quit message.
|
||||||
|
proc disconnectServer*(sock: Socket, quitMsg = "Bye."): void =
|
||||||
|
sendServer(sock, "QUIT :" & quitMsg)
|
||||||
|
close(sock)
|
||||||
|
|
||||||
|
|
||||||
|
# Authenticate to a server. Authentication modes: sasl-plain (default),
|
||||||
|
# nickserv.
|
||||||
|
proc authServer*(sock: Socket, user: string, pass: string, nick: string,
|
||||||
|
mode = "sasl-plain"): bool =
|
||||||
|
var resp: string
|
||||||
|
case mode
|
||||||
|
of "sasl-plain":
|
||||||
|
sendServer(sock, "CAP LS 302")
|
||||||
|
resp = recvServer(sock)
|
||||||
|
if find(resp, "sasl") != -1:
|
||||||
|
sendServer(sock, join(["USER", user, "0 *", user], " "))
|
||||||
|
sendServer(sock, "NICK " & nick)
|
||||||
|
sendServer(sock, "CAP REQ sasl")
|
||||||
|
resp = recvServer(sock)
|
||||||
|
if find(resp, "ACK") != 1:
|
||||||
|
sendServer(sock, "AUTHENTICATE PLAIN")
|
||||||
|
resp = recvServer(sock)
|
||||||
|
if find(resp, "AUTHENTICATE +") != 1:
|
||||||
|
# Format of the string to encode: "user\0user\0password"
|
||||||
|
sendServer(sock, "AUTHENTICATE " & encode(join([user, "\0", user, "\0",
|
||||||
|
pass])))
|
||||||
|
resp = recvServer(sock)
|
||||||
|
if find(resp, "successful") != 1:
|
||||||
|
sendServer(sock, "CAP END")
|
||||||
|
sendServer(sock, "NICK " & nick)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
# PASS + Nickserv
|
||||||
|
else:
|
||||||
|
sendServer(sock, join(["PASS ", user, ":", pass]))
|
||||||
|
sendServer(sock, join(["USER", user, "0 *", user], " "))
|
||||||
|
sendServer(sock, "NICK " & nick)
|
||||||
|
sendServer(sock, join(["NickServ IDENTIFY", user, pass], " "))
|
||||||
|
resp = recvServer(sock)
|
||||||
|
if find(resp, "logged in") != -1 or find(resp, "identified") != -1:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
# Respond to server pings to keep the socket connected.
|
||||||
|
proc sendPong*(sock: Socket, str: string): void =
|
||||||
|
if find(str, "PING") == 0:
|
||||||
|
sendServer(sock, "PONG " & split(str, " ")[1])
|
||||||
|
|
||||||
|
|
||||||
|
# Join a list of channels on a server.
|
||||||
|
proc joinChannels*(sock: Socket, chans: seq): void =
|
||||||
|
if chans.len() == 1:
|
||||||
|
sendServer(sock, "JOIN " & chans[0])
|
||||||
|
elif chans.len() > 1:
|
||||||
|
sendServer(sock, "JOIN " & join(chans, ","))
|
||||||
|
else:
|
||||||
|
debug("joinChannels", "warning: no channels joined, none found")
|
||||||
|
|
||||||
|
|
||||||
|
# Given a code prefix and message string from a server, parse the message
|
||||||
|
# string and return a message tuple of keys and values. The code prefix is set
|
||||||
|
# for a given server instance in the config file.
|
||||||
|
#
|
||||||
|
# Initial received string format:
|
||||||
|
# :[sender-nick]!~[sender-user]@domain.tld PRIVMSG [channel|recipient] :[body]
|
||||||
|
proc parseMessage*(prefix: string, str: string): Message =
|
||||||
|
var
|
||||||
|
msg: Message
|
||||||
|
strLen: int
|
||||||
|
if find(str, "PRIVMSG") != -1 and find(str, "!") != -1:
|
||||||
|
# code
|
||||||
|
strLen = split(str)[3].len() - 1
|
||||||
|
var cPLen = prefix.len()
|
||||||
|
msg.code = split(str)[3][(1 + cPLen)..strLen]
|
||||||
|
# codeParams
|
||||||
|
if split(split(str, ":")[2]).len() > 1:
|
||||||
|
strLen = split(str).len() - 1
|
||||||
|
msg.codeParams = split(str)[4..strLen]
|
||||||
|
else:
|
||||||
|
msg.codeParams = @[]
|
||||||
|
# sender
|
||||||
|
strLen = split(split(str, "!")[1], "@")[0].len() - 1
|
||||||
|
msg.sender = split(split(str, "!")[1], "@")[0][1..strLen]
|
||||||
|
# recipient and replyTo
|
||||||
|
msg.recipient = split(split(str, "PRIVMSG ")[1])[0]
|
||||||
|
msg.replyTo = msg.sender
|
||||||
|
if find(msg.recipient, "#") == 0:
|
||||||
|
msg.replyTo = msg.recipient
|
||||||
|
# body
|
||||||
|
strLen = split(str).len() - 1
|
||||||
|
var bodyLen = join(split(str)[3..strLen], " ").len() - 1
|
||||||
|
msg.body = join(split(str)[3..strLen], " ")[1..bodyLen]
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
# Load JSON service code file to global variable `serviceCodes`.
|
||||||
|
proc loadCodes*(file = ircCodeFile): void =
|
||||||
|
if fileExists(unixToNativePath(file)):
|
||||||
|
try:
|
||||||
|
var nodes = parseFile(unixToNativePath(file))
|
||||||
|
serviceCodes = to(nodes, SvcCodes)
|
||||||
|
except IOError:
|
||||||
|
debug("loadCodes", "error: could not JSON file " & file)
|
||||||
|
# Show error if the code file is missing fields
|
||||||
|
except KeyError:
|
||||||
|
debug("loadCodes", "error: invalid " & file)
|
||||||
|
# Warn if a code has no handler linked to it
|
||||||
|
for c in serviceCodes:
|
||||||
|
if c.handler == "":
|
||||||
|
debug("loadCodes", "warning: code " & c.code & " not added, no " &
|
||||||
|
"handler found")
|
||||||
|
else:
|
||||||
|
debug("loadCodes", "error: cannot find service codes file")
|
||||||
|
|
||||||
|
|
||||||
|
# Write service codes to a JSON file.
|
||||||
|
proc saveCodes*(codes: SvcCodes, file = ircCodeFile): void =
|
||||||
|
writeFile(unixToNativePath(file), pretty(toJson(codes)))
|
||||||
|
|
||||||
|
|
||||||
|
# Return a list of service code names given the code prefix.
|
||||||
|
proc getCodeNames*(prefix: string, codes = serviceCodes): seq[string] =
|
||||||
|
return map(serviceCodes, proc(c: SvcCode): string = prefix & c.code)
|
||||||
|
|
||||||
|
|
||||||
|
# Check whether a user and pass matches an admin credentials. Return true
|
||||||
|
# if there is a match or false otherwise.
|
||||||
|
proc isAdmin*(admins: seq[Admin], user: string, pass: string): bool =
|
||||||
|
for a in admins:
|
||||||
|
if user == a.user and pass == a.pass:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
# Send a handler response to the server.
|
||||||
|
proc runHandler*(prc: string, st: ServerState, msg: Message, hdl = handlers):
|
||||||
|
void =
|
||||||
|
if hasKey(hdl, prc):
|
||||||
|
hdl[prc](st, msg)
|
||||||
|
else:
|
||||||
|
debug("runHandler", "warning: no corresponding handler found for " & prc)
|
||||||
|
|
||||||
|
|
||||||
|
# Given a sequence of servers, connect and authenticate to each server, and
|
||||||
|
# join pre-configured channels. Return a sequence of server states.
|
||||||
|
proc initServers*(servers: seq[Server]): State =
|
||||||
|
var states: State
|
||||||
|
for svr in servers:
|
||||||
|
var
|
||||||
|
sock = connectServer(svr.host, svr.port)
|
||||||
|
isAuth = authServer(sock, svr.user, svr.pass, svr.nick)
|
||||||
|
add(states, (host: svr.host, sock: sock, isAuth: isAuth, channels: @[],
|
||||||
|
codePrefix: svr.codePrefix, nick: svr.nick, admins: svr.admins))
|
||||||
|
if isAuth:
|
||||||
|
joinChannels(sock, svr.channels)
|
||||||
|
states[states.len() - 1].channels = svr.channels
|
||||||
|
return states
|
||||||
|
|
||||||
|
|
||||||
|
# Parse message strings received and pass them to handlers.
|
||||||
|
proc listenCodes*(st: ServerState, str: string): void =
|
||||||
|
var msg = parseMessage(st.codePrefix, str)
|
||||||
|
for c in serviceCodes:
|
||||||
|
if msg.code == c.code:
|
||||||
|
runHandler(c.handler, st, msg)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# Initialise a loop and add listeners to respond to service codes.
|
||||||
|
proc listen*(states: seq[ServerState]): void =
|
||||||
|
while true:
|
||||||
|
var
|
||||||
|
statesBuf = states
|
||||||
|
count = 0
|
||||||
|
for st in statesBuf:
|
||||||
|
try:
|
||||||
|
var recv = recvServer(st.sock)
|
||||||
|
sendPong(st.sock, recv)
|
||||||
|
listenCodes(st, recv)
|
||||||
|
except AssertionDefect:
|
||||||
|
# Remove server from the states sequence
|
||||||
|
delete(statesBuf, count)
|
||||||
|
# Exit if there was only one server connection
|
||||||
|
if states.len() == 1:
|
||||||
|
quit()
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
|
# Pre-configured handlers
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Show a greeting message with a list of service codes.
|
||||||
|
proc hdlIrcHelp*(st: ServerState, msg: Message): void =
|
||||||
|
var
|
||||||
|
names = getCodeNames(st.codePrefix)
|
||||||
|
help = "Hello, I'm a bot. I respond to the following codes: " &
|
||||||
|
join(names, ", ")
|
||||||
|
message(st.sock, msg.replyTo, help)
|
||||||
|
|
||||||
|
|
||||||
|
# Disconnect from the current server.
|
||||||
|
proc hdlIrcQuit*(st: ServerState, msg: Message): void =
|
||||||
|
if msg.codeParams.len() == 1:
|
||||||
|
if isAdmin(st.admins, msg.sender, msg.codeParams[0]):
|
||||||
|
disconnectServer(st.sock)
|
||||||
|
else:
|
||||||
|
message(st.sock, msg.replyTo, "Unauthorised user or password.")
|
||||||
|
else:
|
||||||
|
message(st.sock, msg.replyTo, "Unauthorised user or password.")
|
||||||
|
|
||||||
|
|
||||||
|
# Init
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
handlers["hdlIrcHelp"] = hdlIrcHelp
|
||||||
|
handlers["hdlIrcQuit"] = hdlIrcQuit
|
|
@ -0,0 +1,432 @@
|
||||||
|
import std/[httpclient, json, jsonutils, net, os, random, sequtils, strutils,
|
||||||
|
tables]
|
||||||
|
import ../cirrus
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
GrammarCode* = tuple
|
||||||
|
code: string
|
||||||
|
grammar: string
|
||||||
|
handler: string
|
||||||
|
allowOverwrite: bool
|
||||||
|
|
||||||
|
GrammarCodes* = seq[GrammarCode]
|
||||||
|
|
||||||
|
Grammar* = OrderedTable[string, JsonNode]
|
||||||
|
|
||||||
|
Grammars* = seq[tuple[code: string, grammar: Grammar]]
|
||||||
|
|
||||||
|
|
||||||
|
var
|
||||||
|
grammarDir = ircConfigDir & "grammar/"
|
||||||
|
codeFile = ircConfigDir & "codes.json"
|
||||||
|
grammars*: Grammars
|
||||||
|
grammarCodes*: GrammarCodes
|
||||||
|
reservedCodes* = @["help", "forget", "learn", "unlearn"]
|
||||||
|
pasteUrls* = @["https://ttm.sh"]
|
||||||
|
|
||||||
|
|
||||||
|
# Set the grammar and code file location.
|
||||||
|
proc setGrammarDir*(configDir = ircConfigDir, gramDir = grammarDir,
|
||||||
|
codes = codeFile): void =
|
||||||
|
ircConfigDir = configDir
|
||||||
|
grammarDir = gramDir
|
||||||
|
codeFile = codes
|
||||||
|
|
||||||
|
|
||||||
|
# Download a url and save to a file path. Return true if successful or false
|
||||||
|
# otherwise.
|
||||||
|
proc fetchUrl*(url: string, file: string): bool =
|
||||||
|
var
|
||||||
|
cl = newHttpClient(userAgent = defUserAgent,
|
||||||
|
sslContext = newContext(verifyMode = CVerifyPeer))
|
||||||
|
data: string
|
||||||
|
try:
|
||||||
|
data = cl.getContent(url)
|
||||||
|
except OSError:
|
||||||
|
debug("fetchUrl", "error: cannot fetch url " & url)
|
||||||
|
except ValueError:
|
||||||
|
debug("fetchUrl", "error: no uri scheme found for " & url)
|
||||||
|
if data != "":
|
||||||
|
try:
|
||||||
|
writeFile(unixToNativePath(file), data)
|
||||||
|
return true
|
||||||
|
except IOError:
|
||||||
|
debug("fetchUrl", "error: could not write to file " & file)
|
||||||
|
return false
|
||||||
|
else:
|
||||||
|
debug("fetchUrl", "error: file saved from " & url & " has no contents")
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
# Upload a grammar JSON file to a pastebin and return the paste url.
|
||||||
|
proc postGrammar*(file: string): string =
|
||||||
|
var
|
||||||
|
cl = newHttpClient(userAgent = defUserAgent,
|
||||||
|
sslContext = newContext(verifyMode = CVerifyPeer))
|
||||||
|
data = newMultiPartData()
|
||||||
|
res: string
|
||||||
|
cl.headers = newHttpHeaders({"Content-Type": "application/json"})
|
||||||
|
randomize()
|
||||||
|
var host = sample(pasteUrls)
|
||||||
|
try:
|
||||||
|
data.addFiles({"file": file})
|
||||||
|
res = cl.postContent(host, multipart = data)
|
||||||
|
except OSError:
|
||||||
|
debug("postGrammar", "error: cannot post file " & file)
|
||||||
|
if res != "":
|
||||||
|
return strip(res)
|
||||||
|
else:
|
||||||
|
debug("postGrammar", "error: no response received from " & host)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# Load and return the grammar given a grammar JSON file path.
|
||||||
|
proc loadGrammar*(file: string): Grammar =
|
||||||
|
var gram: Grammar
|
||||||
|
try:
|
||||||
|
gram = getFields(parseFile(unixToNativePath(file)))
|
||||||
|
except IOError:
|
||||||
|
debug("loadGrammar", "error: could not read JSON file " & file)
|
||||||
|
except JsonParsingError:
|
||||||
|
debug("loadGrammar", "error: invalid JSON in file " & file)
|
||||||
|
return gram
|
||||||
|
|
||||||
|
|
||||||
|
# Check whether a code is writable. Return true if writable or false otherwise.
|
||||||
|
proc checkWritable*(name: string, codes = grammarCodes): bool =
|
||||||
|
# Check if the code is a reserved keyword
|
||||||
|
for c in reservedCodes:
|
||||||
|
if name == c:
|
||||||
|
return false
|
||||||
|
# Check if the code allows overwriting
|
||||||
|
for c in codes:
|
||||||
|
if name == c.code and c.allowOverwrite == false:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
# Return the aliases of a given code name as a sequence and their indices, or
|
||||||
|
# the code name given an alias and its index. Return an empty sequence if none
|
||||||
|
# are found. Aliases are code names sharing the same grammar file or handler
|
||||||
|
# name.
|
||||||
|
proc lookupAliases*(name: string, ty = "aliases", codes = grammarCodes):
|
||||||
|
seq[(string, int)] =
|
||||||
|
var
|
||||||
|
grammar: string
|
||||||
|
handler: string
|
||||||
|
res: seq[(string, int)]
|
||||||
|
count = 0
|
||||||
|
tyCount = 0
|
||||||
|
for c in codes:
|
||||||
|
if name == c.code:
|
||||||
|
grammar = c.grammar
|
||||||
|
handler = c.handler
|
||||||
|
break
|
||||||
|
count += 1
|
||||||
|
case ty
|
||||||
|
of "aliases":
|
||||||
|
for c in codes:
|
||||||
|
if grammar != "" and grammar == c.grammar and c.code != name:
|
||||||
|
add(res, (c.code, tyCount))
|
||||||
|
elif handler != "" and handler == c.handler and c.code != name:
|
||||||
|
add(res, (c.code, tyCount))
|
||||||
|
tyCount += 1
|
||||||
|
else:
|
||||||
|
if grammar != "":
|
||||||
|
res = @[(split(grammar, ".")[0], count)]
|
||||||
|
else:
|
||||||
|
for c in codes:
|
||||||
|
# Take the first result found
|
||||||
|
if handler != "" and handler == c.handler and c.code != name:
|
||||||
|
add(res, (c.code, tyCount))
|
||||||
|
break
|
||||||
|
tyCount += 1
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# Return a list of service code names given the code prefix.
|
||||||
|
proc getGrammarNames*(prefix: string, codes = grammarCodes): seq[string] =
|
||||||
|
return map(grammarCodes, proc(c: GrammarCode): string = prefix & c.code)
|
||||||
|
|
||||||
|
|
||||||
|
# Given a service code name and grammars, return the associated grammar and the
|
||||||
|
# index of the grammar. If no grammar is found for the code, return an empty
|
||||||
|
# grammar and -1.
|
||||||
|
proc lookupGrammar*(code: string, grams = grammars): (Grammar, int) =
|
||||||
|
var
|
||||||
|
gram: Grammar
|
||||||
|
found = false
|
||||||
|
count = 0
|
||||||
|
for g in grams:
|
||||||
|
if g.code == code:
|
||||||
|
gram = g.grammar
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
count += 1
|
||||||
|
if found:
|
||||||
|
return (gram, count)
|
||||||
|
else:
|
||||||
|
return (gram, -1)
|
||||||
|
|
||||||
|
|
||||||
|
# Return a random origin string from a grammar. If the grammar origin array is
|
||||||
|
# empty, return an empty string.
|
||||||
|
proc getOrigin*(gram: Grammar): string =
|
||||||
|
if getOrDefault(gram, "origin") != nil:
|
||||||
|
var
|
||||||
|
line = getStr(sample(getElems(getOrDefault(gram, "origin"))))
|
||||||
|
spliced = false
|
||||||
|
by = "#"
|
||||||
|
check: seq[bool]
|
||||||
|
splice: string
|
||||||
|
word: string
|
||||||
|
exp: string
|
||||||
|
sub: seq[string]
|
||||||
|
# Convert splices to random selections from their respective blocks
|
||||||
|
while not spliced:
|
||||||
|
check = @[]
|
||||||
|
for w in split(line):
|
||||||
|
# Splices need to be at least 3 characters long, e.g. "#s#"
|
||||||
|
if w.len() >= 3:
|
||||||
|
# Allow for punctuation after a splice, e.g. "#block#,"
|
||||||
|
if rfind(w, by) == w.len() - 2:
|
||||||
|
splice = w[0..(w.len() - 2)]
|
||||||
|
word = w[1..(w.len() - 3)]
|
||||||
|
else:
|
||||||
|
splice = w[0..(w.len() - 1)]
|
||||||
|
word = w[1..(w.len() - 2)]
|
||||||
|
if find(w, by) == 0 and (rfind(w, by) == (w.len() - 1) or
|
||||||
|
rfind(w, by) == (w.len() - 2)):
|
||||||
|
if getOrDefault(gram, word) != nil:
|
||||||
|
randomize()
|
||||||
|
exp = getStr(sample(getElems(getOrDefault(gram, word))))
|
||||||
|
sub = split(line, splice, 1)
|
||||||
|
line = join([sub[0], exp, sub[1]])
|
||||||
|
# Confirm all splices have been expanded
|
||||||
|
for w in split(line):
|
||||||
|
if (find(w, by) != 0 and (rfind(w, by) != 0 or rfind(w, by) != 1)) or
|
||||||
|
w.len() <= 2:
|
||||||
|
add(check, true)
|
||||||
|
else:
|
||||||
|
add(check, false)
|
||||||
|
if find(check, false) == -1:
|
||||||
|
spliced = true
|
||||||
|
return line
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# Load JSON service code file to global variable `grammarCodes`.
|
||||||
|
proc loadGrammarCodes*(file = codeFile): void =
|
||||||
|
if fileExists(unixToNativePath(file)):
|
||||||
|
var
|
||||||
|
nodes: JsonNode
|
||||||
|
grammar: OrderedTable[string, JsonNode]
|
||||||
|
try:
|
||||||
|
nodes = parseFile(unixToNativePath(file))
|
||||||
|
except IOError:
|
||||||
|
debug("loadGrammar", "error: could not read file " & file)
|
||||||
|
try:
|
||||||
|
grammarCodes = to(nodes, GrammarCodes)
|
||||||
|
# Show error if the code index file is missing fields
|
||||||
|
except KeyError:
|
||||||
|
debug("loadGrammarCodes", "error: invalid code file " & file)
|
||||||
|
|
||||||
|
# Fetch the grammar for each code and add it to the grammars tuple. A code
|
||||||
|
# can have either grammar or handler, not both. If both are defined, the
|
||||||
|
# grammar will be selected (see `listenCodes()`).
|
||||||
|
for c in grammarCodes:
|
||||||
|
if fileExists(unixToNativePath(grammarDir & c.grammar)):
|
||||||
|
grammar = loadGrammar(grammarDir & c.grammar)
|
||||||
|
add(grammars, (code: c.code, grammar: grammar))
|
||||||
|
for a in lookupAliases(c.code):
|
||||||
|
add(grammars, (code: a[0], grammar: grammar))
|
||||||
|
else:
|
||||||
|
debug("loadGrammarCodes", "warning: code " & c.code &
|
||||||
|
" not added, no grammar file or handler found")
|
||||||
|
else:
|
||||||
|
debug("loadGrammarCodes", "error: cannot find service codes file")
|
||||||
|
|
||||||
|
|
||||||
|
# Write service codes to a JSON file.
|
||||||
|
proc saveGrammarCodes*(codes: GrammarCodes, file = codeFile): void =
|
||||||
|
try:
|
||||||
|
writeFile(unixToNativePath(file), pretty(toJson(codes)))
|
||||||
|
except IOError:
|
||||||
|
debug("saveGrammarCodes", "error: could not write to file " & file)
|
||||||
|
|
||||||
|
|
||||||
|
# Add new grammar.
|
||||||
|
proc addGrammar*(sock: Socket, msg: Message): void =
|
||||||
|
if msg.codeParams.len() >= 2:
|
||||||
|
var nameSplit = split(msg.codeParams[0], ",")
|
||||||
|
if not checkWritable(nameSplit[0]):
|
||||||
|
message(sock, msg.replyTo, "Sorry, " & nameSplit[0] &
|
||||||
|
" is not writable. Please use another code.")
|
||||||
|
return
|
||||||
|
|
||||||
|
var
|
||||||
|
aliases: seq[string]
|
||||||
|
file = grammarDir & nameSplit[0] & ".json"
|
||||||
|
urlFetched = fetchUrl(msg.codeParams[1], file)
|
||||||
|
grammar = loadGrammar(file)
|
||||||
|
origin = getOrigin(grammar)
|
||||||
|
if nameSplit.len() > 1:
|
||||||
|
var aliasesLen = nameSplit.len() - 1
|
||||||
|
aliases = nameSplit[1..aliasesLen]
|
||||||
|
|
||||||
|
# Add to the global codes and grammar tuples and save to the codes file
|
||||||
|
if origin != "" and urlFetched:
|
||||||
|
add(grammarCodes, (code: nameSplit[0], grammar: nameSplit[0] & ".json",
|
||||||
|
handler: "", allowOverwrite: true))
|
||||||
|
add(grammars, (code: nameSplit[0], grammar: grammar))
|
||||||
|
# Add aliases
|
||||||
|
for a in aliases:
|
||||||
|
if lookupGrammar(a)[1] == -1:
|
||||||
|
add(grammarCodes, (code: a, grammar: nameSplit[0] & ".json",
|
||||||
|
handler: "", allowOverwrite: true))
|
||||||
|
add(grammars, (code: a, grammar: grammar))
|
||||||
|
saveGrammarCodes(grammarCodes)
|
||||||
|
message(sock, msg.replyTo, "Learned.")
|
||||||
|
else:
|
||||||
|
message(sock, msg.replyTo, "Sorry, I didn't get that. " &
|
||||||
|
"The grammar file may be empty or invalid.")
|
||||||
|
# Remove the grammar file if the grammar is invalid
|
||||||
|
removeFile(unixToNativePath(file))
|
||||||
|
else:
|
||||||
|
message(sock, msg.replyTo, "Sorry, I didn't get that. " &
|
||||||
|
"Please provide a code name and JSON url.")
|
||||||
|
|
||||||
|
|
||||||
|
# Remove grammar. Only codes that are writable can be removed.
|
||||||
|
proc removeGrammar*(sock: Socket, msg: Message): void =
|
||||||
|
if msg.codeParams.len() >= 1:
|
||||||
|
# Alias entries will follow the writable status of the code name entry
|
||||||
|
# since the aliases depend on the same grammar file, which can be removed
|
||||||
|
# if the code name entry is writable.
|
||||||
|
if lookupAliases(msg.codeParams[0], "code").len() == 0:
|
||||||
|
message(sock, msg.replyTo, "Sorry, I don't remember learning that.")
|
||||||
|
return
|
||||||
|
var name = lookupAliases(msg.codeParams[0], "code")[0][0]
|
||||||
|
if not checkWritable(name) and name != msg.codeParams[0]:
|
||||||
|
message(sock, msg.replyTo, join(["Sorry, ", msg.codeParams[0],
|
||||||
|
" (alias of ", name, ") cannot be removed", "because ", name,
|
||||||
|
" is not writable."]))
|
||||||
|
return
|
||||||
|
# Remove from the global codes and grammars tuples, save to the codes file
|
||||||
|
# and remove the grammar json
|
||||||
|
var aliases = lookupAliases(name)
|
||||||
|
for a in aliases:
|
||||||
|
delete(grammars, lookupGrammar(a[0])[1])
|
||||||
|
delete(grammarCodes, a[1])
|
||||||
|
delete(grammars, lookupGrammar(name)[1])
|
||||||
|
delete(grammarCodes, lookupAliases(name, "code")[0][1])
|
||||||
|
saveGrammarCodes(grammarCodes)
|
||||||
|
removeFile(unixToNativePath(grammarDir & name & ".json"))
|
||||||
|
message(sock, msg.replyTo, msg.codeParams[0] & "? What's that?")
|
||||||
|
else:
|
||||||
|
message(sock, msg.replyTo, "Sorry, I didn't get that. " &
|
||||||
|
"Please provide a code.")
|
||||||
|
|
||||||
|
|
||||||
|
# Show the paste url of a grammar code.
|
||||||
|
proc showGrammar*(sock: Socket, msg: Message): void =
|
||||||
|
var reply: string
|
||||||
|
if msg.codeParams.len() > 0:
|
||||||
|
var
|
||||||
|
names = map(grammarCodes, proc(c: GrammarCode): string = c.code)
|
||||||
|
files = map(grammarCodes, proc(c: GrammarCode): string = c.grammar)
|
||||||
|
url: string
|
||||||
|
file: string
|
||||||
|
for p in msg.codeParams:
|
||||||
|
if find(names, p) != -1:
|
||||||
|
file = files[find(names, p)]
|
||||||
|
if file != "":
|
||||||
|
url = postGrammar(grammarDir & file)
|
||||||
|
if url != "":
|
||||||
|
message(sock, msg.replyTo, url)
|
||||||
|
else:
|
||||||
|
reply = "No grammar linked to " & p
|
||||||
|
message(sock, msg.replyTo, reply)
|
||||||
|
else:
|
||||||
|
reply = "No grammar found for " & p
|
||||||
|
message(sock, msg.replyTo, reply)
|
||||||
|
else:
|
||||||
|
reply = "Please provide a code."
|
||||||
|
message(sock, msg.replyTo, reply)
|
||||||
|
|
||||||
|
|
||||||
|
# Send a grammar response to the server.
|
||||||
|
proc runGrammar*(gram: Grammar, sock: Socket, msg: Message): void =
|
||||||
|
var origin = getOrigin(gram)
|
||||||
|
if origin != "":
|
||||||
|
message(sock, msg.replyTo, origin)
|
||||||
|
else:
|
||||||
|
debug("runGrammar", "warning: no origin found in grammar")
|
||||||
|
|
||||||
|
|
||||||
|
# Parse message strings received and pass them to handlers.
|
||||||
|
proc listenGrammarCodes*(st: ServerState, str: string): void =
|
||||||
|
var msg = parseMessage(st.codePrefix, str)
|
||||||
|
for c in grammarCodes:
|
||||||
|
if msg.code == c.code:
|
||||||
|
if lookupGrammar(c.code)[1] != -1:
|
||||||
|
runGrammar(lookupGrammar(c.code)[0], st.sock, msg)
|
||||||
|
else:
|
||||||
|
runHandler(c.handler, st, msg)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# Initialise a loop and add listeners to respond to service codes.
|
||||||
|
proc listenGrammar*(states: seq[ServerState]): void =
|
||||||
|
while true:
|
||||||
|
var
|
||||||
|
statesBuf = states
|
||||||
|
count = 0
|
||||||
|
for st in statesBuf:
|
||||||
|
try:
|
||||||
|
var recv = recvServer(st.sock)
|
||||||
|
sendPong(st.sock, recv)
|
||||||
|
listenGrammarCodes(st, recv)
|
||||||
|
except AssertionDefect:
|
||||||
|
# Remove server from the states sequence
|
||||||
|
delete(statesBuf, count)
|
||||||
|
# Exit if there was only one server connection
|
||||||
|
if states.len() == 1:
|
||||||
|
quit()
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
|
# Handlers
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc hdlGrammarHelp*(st: ServerState, msg: Message): void =
|
||||||
|
var
|
||||||
|
names = getGrammarNames(st.codePrefix)
|
||||||
|
help = "Hello, I'm a tracery-inspired grammar bot. " &
|
||||||
|
"I respond to the following service codes: " & join(names, ", ")
|
||||||
|
message(st.sock, msg.replyTo, help)
|
||||||
|
|
||||||
|
|
||||||
|
proc hdlGrammarLearn*(st: ServerState, msg: Message): void =
|
||||||
|
addGrammar(st.sock, msg)
|
||||||
|
|
||||||
|
|
||||||
|
proc hdlGrammarShow*(st: ServerState, msg: Message): void =
|
||||||
|
showGrammar(st.sock, msg)
|
||||||
|
|
||||||
|
|
||||||
|
proc hdlGrammarUnlearn*(st: ServerState, msg: Message): void =
|
||||||
|
removeGrammar(st.sock, msg)
|
||||||
|
|
||||||
|
|
||||||
|
# Init
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
setGrammarDir()
|
||||||
|
loadGrammarCodes()
|
||||||
|
handlers["hdlGrammarHelp"] = hdlGrammarHelp
|
||||||
|
handlers["hdlGrammarLearn"] = hdlGrammarLearn
|
||||||
|
handlers["hdlGrammarShow"] = hdlGrammarShow
|
||||||
|
handlers["hdlGrammarUnlearn"] = hdlGrammarUnlearn
|
|
@ -0,0 +1,123 @@
|
||||||
|
import std/[json, jsonutils, os, sequtils, strutils, tables]
|
||||||
|
import ../cirrus
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
PoolItem = tuple
|
||||||
|
name: string
|
||||||
|
desc: string
|
||||||
|
|
||||||
|
JavaPool = seq[PoolItem]
|
||||||
|
|
||||||
|
var
|
||||||
|
javaPool: JavaPool
|
||||||
|
poolFile* = ircConfigDir & "javapool.json"
|
||||||
|
|
||||||
|
|
||||||
|
# Save pool to pool config file.
|
||||||
|
proc savePool(file = poolFile, pool = javaPool): void =
|
||||||
|
try:
|
||||||
|
writeFile(unixToNativePath(file), pretty(toJson(pool)))
|
||||||
|
except IOError:
|
||||||
|
debug("savePool", "error: could not write to file " & file)
|
||||||
|
|
||||||
|
|
||||||
|
# Load pool config file.
|
||||||
|
proc loadPool(file = poolFile): JavaPool =
|
||||||
|
# If config file doesn't exist, create one
|
||||||
|
if not fileExists(unixToNativePath(file)):
|
||||||
|
savePool(file, @[])
|
||||||
|
try:
|
||||||
|
var
|
||||||
|
nodes = parseFile(unixToNativePath(file))
|
||||||
|
conf = to(nodes, JavaPool)
|
||||||
|
return conf
|
||||||
|
except IOError:
|
||||||
|
debug("loadPool", "error: could not read JSON file " & file)
|
||||||
|
except JsonParsingError:
|
||||||
|
debug("loadPool", "error: invalid JSON in file " & file)
|
||||||
|
|
||||||
|
|
||||||
|
# Given an item name, return its description and its index if the item is in
|
||||||
|
# the pool, or empty string and -1 otherwise.
|
||||||
|
proc lookupPoolItem(name: string, pool = javaPool): (string, int) =
|
||||||
|
var count = 0
|
||||||
|
for i in pool:
|
||||||
|
if name == i.name:
|
||||||
|
return (desc: i.desc, index: count)
|
||||||
|
count += 1
|
||||||
|
return (desc: "", index: -1)
|
||||||
|
|
||||||
|
|
||||||
|
# Handlers
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Show a list of pool items.
|
||||||
|
proc hdlPoolList*(st: ServerState, msg: Message): void =
|
||||||
|
var reply: string
|
||||||
|
if javaPool.len() == 0:
|
||||||
|
reply = "There are no items in the pool yet."
|
||||||
|
else:
|
||||||
|
reply = "There are " & intToStr(javaPool.len()) & " items in the pool: " &
|
||||||
|
join(map(javaPool, proc(i: PoolItem): string = i.name), ", ")
|
||||||
|
message(st.sock, msg.replyTo, reply)
|
||||||
|
|
||||||
|
|
||||||
|
# Show an item's description if it exists in the pool, or list the pool items
|
||||||
|
# if no item name was given in the message.
|
||||||
|
proc hdlPoolLook*(st: ServerState, msg: Message): void =
|
||||||
|
if msg.codeParams.len() > 0:
|
||||||
|
var reply: string
|
||||||
|
for p in msg.codeParams:
|
||||||
|
if lookupPoolItem(p)[1] != -1:
|
||||||
|
reply = join(["The ", p, " is ", lookupPoolItem(p)[0], "."])
|
||||||
|
else:
|
||||||
|
reply = "There is no " & p & " in the pool."
|
||||||
|
message(st.sock, msg.replyTo, reply)
|
||||||
|
else:
|
||||||
|
hdlPoolList(st, msg)
|
||||||
|
|
||||||
|
|
||||||
|
# Add an item to the pool.
|
||||||
|
proc hdlPoolAdd*(st: ServerState, msg: Message): void =
|
||||||
|
var reply: string
|
||||||
|
if msg.codeParams.len() >= 2:
|
||||||
|
var desc = join(msg.codeParams[1..(msg.codeParams.len() - 1)], " ")
|
||||||
|
add(javaPool, (name: msg.codeParams[0], desc: desc))
|
||||||
|
savePool()
|
||||||
|
reply = "Item " & msg.codeParams[0] & " added to the pool."
|
||||||
|
message(st.sock, msg.replyTo, reply)
|
||||||
|
else:
|
||||||
|
reply = "Usage: " & st.codePrefix & msg.code & " [name] [description]"
|
||||||
|
message(st.sock, msg.replyTo, reply)
|
||||||
|
|
||||||
|
|
||||||
|
# Remove an item from the pool.
|
||||||
|
proc hdlPoolRemove*(st: ServerState, msg: Message): void =
|
||||||
|
var reply: string
|
||||||
|
if msg.codeParams.len() >= 1:
|
||||||
|
for p in msg.codeParams:
|
||||||
|
if lookupPoolItem(p)[1] != -1:
|
||||||
|
delete(javaPool, lookupPoolItem(p)[1])
|
||||||
|
reply = "Item " & p & " removed from the pool."
|
||||||
|
message(st.sock, msg.replyTo, reply)
|
||||||
|
else:
|
||||||
|
reply = "There is no " & p & " in the pool."
|
||||||
|
message(st.sock, msg.replyTo, reply)
|
||||||
|
savePool()
|
||||||
|
else:
|
||||||
|
reply = "Usage: " & st.codePrefix & msg.code & " [name]"
|
||||||
|
message(st.sock, msg.replyTo, reply)
|
||||||
|
|
||||||
|
|
||||||
|
# Init
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
when defined(nimHasUsed):
|
||||||
|
{.used.}
|
||||||
|
|
||||||
|
javaPool = loadPool()
|
||||||
|
handlers["hdlPoolList"] = hdlPoolList
|
||||||
|
handlers["hdlPoolLook"] = hdlPoolLook
|
||||||
|
handlers["hdlPoolAdd"] = hdlPoolAdd
|
||||||
|
handlers["hdlPoolRemove"] = hdlPoolRemove
|
Reference in New Issue