big improvements. no more code reloading, use ZNC instead. regex table. store state to disk.

This commit is contained in:
river 2021-05-29 23:31:39 +01:00
parent e057eed3ac
commit 8374c1ad1d
6 changed files with 98 additions and 47 deletions

2
.gitignore vendored
View File

@ -144,3 +144,5 @@ config.py
# ~ files
*~
# botstate
botstate.dict

View File

@ -7,7 +7,8 @@ My IRC bot in python
* `king-leonidas`: The main script.
* `config.py`: The settings.
* `irc.py`: Generic code to connect to and maintain an IRC connection.
* `bot.py`: Stateless IRC bot code that processes lines.
* `bot.py`: IRC bot code that processes lines.
* `regexmultimatch.py`: A useful regex primitive
## Usage
@ -18,23 +19,16 @@ python -m venv venv
source venv/bin/activate
```
also set up ZNC, this allows you to restart the bot without the connection dropping.
### Launch
```
./king-leonidas
```
### Reload bot.py
```
pkill -HUP king-leonidas
```
## To do
* join after ident, or SASL
* automatically restart on crash
* handle should be split into functions that deal with channel messages, privmsgs, etc.
* bot state saved to disk?
* provide a logfile to write to
* would like to be able to trigger a reload using a Control-something signal in the bot itself, rather than pkill in a separate terminal.

68
bot.py
View File

@ -1,26 +1,54 @@
import re
import random
print("[ + ] Loaded bot.py")
def handle(state, line, f):
print("[IRC] %s" % line)
class Bot():
def __init__(self, state):
print("[ + ] Bot() init ")
self.state = state
self.f = None
self.tbl = [
('^PING :(?P<token>.*)$', self.ping),
('^[^ ]+ INVITE [^ ]+ :(?P<channel>[^ ]+)$', self.invite),
('^:(?P<nick>.+)!(.+)@(.+) JOIN (?P<channel>.+)$', self.join),
('^:(?P<nick>.+)!(.+)@(.+) PRIVMSG [@]?(?P<channel>[^ ]+) :(?P<message>.*)$', self.privmsg),
]
def connect(self, config):
print("PASS %s" % (config.serverpass), file=self.f)
print("USER %s %s %s %s" % (config.username, config.username, config.username, config.username), file=self.f)
print("NICK %s" % (config.username), file=self.f)
print("PRIVMSG NickServ :Identify %s\n" % (config.password), file=self.f)
for channel in config.channels:
print("JOIN %s" % channel, file=self.f)
# ping/pong
m = re.search('^PING :(.*)$', line)
if m:
print(("PONG %s" % m.group(1)), file=f)
return
def ping(self, m):
token = m.group('token')
print("[PONG] %s" % token)
print(("PONG %s" % token), file=self.f)
def invite(self, m):
print(("JOIN %s" % m.group('channel')))
print(("JOIN %s" % m.group('channel')), file=self.f)
def join(self, m):
if m.group('channel') == '#SPARTA':
print('KICK %s %s :%s' % (m.group('channel'), m.group('nick'), "THIS! IS! SPARTA!"), file=self.f)
return
# kick anyone who joins
m = re.search('^:(.+)!(.+)@(.+) JOIN (.+)$', line)
if m:
print('KICK %s %s :%s' % (m.group(4), m.group(1), "THIS! IS! SPARTA!"), file=f)
return
# increment a counter
m = re.search('^[^ ]+ PRIVMSG ([^ ]+) :\+\+$', line)
if m:
if 'ctr' not in state:
state['ctr'] = 0
print('PRIVMSG %s :%s' % (m.group(1), state['ctr']), file=f)
state['ctr'] += 1
def privmsg(self, m):
if m.group('channel') == '#anon':
print('PRIVMSG %s :%s' % (m.group('channel'), m.group('message')), file=self.f)
return
if m.group('channel') == '#prize':
message = random.choice(['snake eyes.', '$100'])
print('PRIVMSG MemoServ :SEND %s %s' % (m.group('nick'), message), file=self.f)
return
# else
if 'ctr' not in self.state:
self.state['ctr'] = 0
print('PRIVMSG %s :%s' % (m.group('channel'), self.state['ctr']), file=self.f)
self.state['ctr'] += 1

18
irc.py
View File

@ -4,13 +4,13 @@ import ssl
import time
import importlib
import bot
from bot import Bot
from regexmultimatch import multimatch
class IRC():
def __init__(self, config):
self.config = config
self.trigger_reload = False
self.state = {}
self.bot = Bot({})
def go(self):
s0 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -18,17 +18,11 @@ class IRC():
s.connect((self.config.server, int(self.config.port)))
f = s.makefile(mode='rw', buffering=1, encoding='utf-8', newline='\r\n')
print("USER %s %s %s %s" % (self.config.username, self.config.username, self.config.username, self.config.username), file=f)
print("NICK %s" % (self.config.username), file=f)
print("PRIVMSG NickServ :Identify %s\n" % (self.config.password), file=f)
print("JOIN %s" % (self.config.channel), file=f)
self.bot.f = f
self.bot.connect(self.config)
for line in f:
if self.trigger_reload:
module = importlib.import_module('bot')
importlib.reload(module)
self.trigger_reload = False
try:
bot.handle(self.state, line.rstrip(), f)
multimatch(self.bot.tbl, line.rstrip())
except:
print("Unexpected error:", sys.exc_info())

View File

@ -1,15 +1,32 @@
#!/usr/bin/env python
import signal
import sys
import ast
from config import Config
from irc import IRC
bot = IRC(Config)
irc = IRC(Config)
def sighup_handler(signo, frame):
bot.trigger_reload = True
def write_state(state):
with open('botstate.dict', 'w') as target:
target.write(str(state))
signal.signal(signal.SIGHUP, sighup_handler)
def read_state():
try:
with open('botstate.dict', 'r') as f:
print('[ + ] Reading bot state from disk.')
s = f.read()
return ast.literal_eval(s)
except:
return {}
bot.go()
def signal_handler(sig, frame):
print('[ + ] Writing bot state to disk.')
write_state(irc.bot.state)
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
irc.bot.state = read_state()
irc.go()

16
regexmultimatch.py Normal file
View File

@ -0,0 +1,16 @@
import re
def multimatch(table, line):
for regex, callback in table:
m = re.match(regex, line)
if m:
return callback(m)
if __name__ == "__main__":
def f1(m):
return "f1"
def f2(m):
return "f2"
tbl = [("foo", f1), ("bar", f2)]
print(multimatch(tbl, "foo"))
print(multimatch(tbl, "bar"))