big improvements. no more code reloading, use ZNC instead. regex table. store state to disk.
This commit is contained in:
parent
e057eed3ac
commit
8374c1ad1d
|
@ -144,3 +144,5 @@ config.py
|
|||
# ~ files
|
||||
*~
|
||||
|
||||
# botstate
|
||||
botstate.dict
|
||||
|
|
14
README.md
14
README.md
|
@ -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
68
bot.py
|
@ -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
18
irc.py
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"))
|
Loading…
Reference in New Issue