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