Initial commit

main
mio 2023-01-20 18:31:02 +00:00
commit 83bb66d94c
14 changed files with 1128 additions and 0 deletions

2
.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
bots/*/bot
bots/*/.config

View File

@ -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
}
]

View File

@ -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"
}
]
}
]
}

View File

@ -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"
]
}

View File

@ -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#!"
]
}

View File

@ -0,0 +1 @@
{ "origin": ["pong!"] }

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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

340
cirrus.nim 100644
View File

@ -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

432
modules/grammar.nim 100644
View File

@ -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

View File

@ -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

8
readme.md 100644
View File

@ -0,0 +1,8 @@
# Readme
Cirrus is a IRC bot script.
## License
BSD-3