This commit is contained in:
Luke Rogers 2011-11-20 22:23:31 +13:00
commit 37588421f3
100 changed files with 22673 additions and 0 deletions

53
core/config.py Normal file
View file

@ -0,0 +1,53 @@
import inspect
import json
import os
def save(conf):
json.dump(conf, open('config', 'w'), sort_keys=True, indent=2)
if not os.path.exists('config'):
open('config', 'w').write(inspect.cleandoc(
r'''
{
"connections":
{
"local irc":
{
"server": "localhost",
"nick": "skybot",
"channels": ["#test"]
}
},
"disabled_plugins": [],
"disabled_commands": [],
"acls": {},
"api_keys": {},
"censored_strings":
[
"DCC SEND",
"1nj3ct",
"thewrestlinggame",
"startkeylogger",
"hybux",
"\\0",
"\\x01",
"!coz",
"!tell /x"
],
"admins": []
}''') + '\n')
def config():
# reload config from file if file has changed
config_mtime = os.stat('config').st_mtime
if bot._config_mtime != config_mtime:
try:
bot.config = json.load(open('config'))
bot._config_mtime = config_mtime
except ValueError, e:
print 'ERROR: malformed config!', e
bot._config_mtime = 0

25
core/db.py Normal file
View file

@ -0,0 +1,25 @@
import os
import sqlite3
import thread
threaddbs={}
def get_db_connection(conn, name=''):
"returns an sqlite3 connection to a persistent database"
if not name:
name = '%s.%s.db' % (conn.nick, conn.server)
threadid = thread.get_ident()
if name in threaddbs and threadid in threaddbs[name]:
return threaddbs[name][threadid]
filename = os.path.join(bot.persist_dir, name)
db = sqlite3.connect(filename, timeout=10)
if name in threaddbs:
threaddbs[name][threadid] = db
else:
threaddbs[name] = {threadid: db}
return db
bot.get_db_connection = get_db_connection

250
core/irc.py Normal file
View file

@ -0,0 +1,250 @@
import re
import socket
import time
import thread
import Queue
from ssl import wrap_socket, CERT_NONE, CERT_REQUIRED, SSLError
def decode(txt):
for codec in ('utf-8', 'iso-8859-1', 'shift_jis', 'cp1252'):
try:
return txt.decode(codec)
except UnicodeDecodeError:
continue
return txt.decode('utf-8', 'ignore')
def censor(text):
replacement = '[censored]'
if 'censored_strings' in bot.config:
words = map(re.escape, bot.config['censored_strings'])
regex = re.compile('(%s)' % "|".join(words))
text = regex.sub(replacement, text)
return text
class crlf_tcp(object):
"Handles tcp connections that consist of utf-8 lines ending with crlf"
def __init__(self, host, port, timeout=300):
self.ibuffer = ""
self.obuffer = ""
self.oqueue = Queue.Queue() # lines to be sent out
self.iqueue = Queue.Queue() # lines that were received
self.socket = self.create_socket()
self.host = host
self.port = port
self.timeout = timeout
def create_socket(self):
return socket.socket(socket.AF_INET, socket.TCP_NODELAY)
def run(self):
self.socket.connect((self.host, self.port))
thread.start_new_thread(self.recv_loop, ())
thread.start_new_thread(self.send_loop, ())
def recv_from_socket(self, nbytes):
return self.socket.recv(nbytes)
def get_timeout_exception_type(self):
return socket.timeout
def handle_receive_exception(self, error, last_timestamp):
if time.time() - last_timestamp > self.timeout:
self.iqueue.put(StopIteration)
self.socket.close()
return True
return False
def recv_loop(self):
last_timestamp = time.time()
while True:
try:
data = self.recv_from_socket(4096)
self.ibuffer += data
if data:
last_timestamp = time.time()
else:
if time.time() - last_timestamp > self.timeout:
self.iqueue.put(StopIteration)
self.socket.close()
return
time.sleep(1)
except (self.get_timeout_exception_type(), socket.error) as e:
if self.handle_receive_exception(e, last_timestamp):
return
continue
while '\r\n' in self.ibuffer:
line, self.ibuffer = self.ibuffer.split('\r\n', 1)
self.iqueue.put(decode(line))
def send_loop(self):
while True:
line = self.oqueue.get().splitlines()[0][:500]
print ">>> %r" % line
self.obuffer += line.encode('utf-8', 'replace') + '\r\n'
while self.obuffer:
sent = self.socket.send(self.obuffer)
self.obuffer = self.obuffer[sent:]
class crlf_ssl_tcp(crlf_tcp):
"Handles ssl tcp connetions that consist of utf-8 lines ending with crlf"
def __init__(self, host, port, ignore_cert_errors, timeout=300):
self.ignore_cert_errors = ignore_cert_errors
crlf_tcp.__init__(self, host, port, timeout)
def create_socket(self):
return wrap_socket(crlf_tcp.create_socket(self), server_side=False,
cert_reqs=CERT_NONE if self.ignore_cert_errors else
CERT_REQUIRED)
def recv_from_socket(self, nbytes):
return self.socket.read(nbytes)
def get_timeout_exception_type(self):
return SSLError
def handle_receive_exception(self, error, last_timestamp):
# this is terrible
if not "timed out" in error.args[0]:
raise
return crlf_tcp.handle_receive_exception(self, error, last_timestamp)
irc_prefix_rem = re.compile(r'(.*?) (.*?) (.*)').match
irc_noprefix_rem = re.compile(r'()(.*?) (.*)').match
irc_netmask_rem = re.compile(r':?([^!@]*)!?([^@]*)@?(.*)').match
irc_param_ref = re.compile(r'(?:^|(?<= ))(:.*|[^ ]+)').findall
class IRC(object):
"handles the IRC protocol"
#see the docs/ folder for more information on the protocol
def __init__(self, server, nick, port=6667, channels=[], conf={}):
self.channels = channels
self.conf = conf
self.server = server
self.port = port
self.nick = nick
self.out = Queue.Queue() # responses from the server are placed here
# format: [rawline, prefix, command, params,
# nick, user, host, paramlist, msg]
self.connect()
thread.start_new_thread(self.parse_loop, ())
def create_connection(self):
return crlf_tcp(self.server, self.port)
def connect(self):
self.conn = self.create_connection()
thread.start_new_thread(self.conn.run, ())
self.set_pass(self.conf.get('server_password'))
self.set_nick(self.nick)
self.cmd("USER",
[conf.get('user', 'skybot'), "3", "*", conf.get('realname',
'Python bot - http://github.com/rmmh/skybot')])
def parse_loop(self):
while True:
msg = self.conn.iqueue.get()
if msg == StopIteration:
self.connect()
continue
if msg.startswith(":"): # has a prefix
prefix, command, params = irc_prefix_rem(msg).groups()
else:
prefix, command, params = irc_noprefix_rem(msg).groups()
nick, user, host = irc_netmask_rem(prefix).groups()
paramlist = irc_param_ref(params)
lastparam = ""
if paramlist:
if paramlist[-1].startswith(':'):
paramlist[-1] = paramlist[-1][1:]
lastparam = paramlist[-1]
self.out.put([msg, prefix, command, params, nick, user, host,
paramlist, lastparam])
if command == "PING":
self.cmd("PONG", paramlist)
def set_pass(self, password):
if password:
self.cmd("PASS", [password])
def set_nick(self, nick):
self.cmd("NICK", [nick])
def join(self, channel):
self.cmd("JOIN", [channel])
def msg(self, target, text):
self.cmd("PRIVMSG", [target, text])
def cmd(self, command, params=None):
if params:
params[-1] = ':' + params[-1]
self.send(command + ' ' + ' '.join(map(censor, params)))
else:
self.send(command)
def send(self, str):
self.conn.oqueue.put(str)
class FakeIRC(IRC):
def __init__(self, server, nick, port=6667, channels=[], conf={}, fn=""):
self.channels = channels
self.conf = conf
self.server = server
self.port = port
self.nick = nick
self.out = Queue.Queue() # responses from the server are placed here
self.f = open(fn, 'rb')
thread.start_new_thread(self.parse_loop, ())
def parse_loop(self):
while True:
msg = decode(self.f.readline()[9:])
if msg == '':
print "!!!!DONE READING FILE!!!!"
return
if msg.startswith(":"): # has a prefix
prefix, command, params = irc_prefix_rem(msg).groups()
else:
prefix, command, params = irc_noprefix_rem(msg).groups()
nick, user, host = irc_netmask_rem(prefix).groups()
paramlist = irc_param_ref(params)
lastparam = ""
if paramlist:
if paramlist[-1].startswith(':'):
paramlist[-1] = paramlist[-1][1:]
lastparam = paramlist[-1]
self.out.put([msg, prefix, command, params, nick, user, host,
paramlist, lastparam])
if command == "PING":
self.cmd("PONG", [params])
def cmd(self, command, params=None):
pass
class SSLIRC(IRC):
def __init__(self, server, nick, port=6667, channels=[], conf={},
ignore_certificate_errors=True):
self.ignore_cert_errors = ignore_certificate_errors
IRC.__init__(self, server, nick, port, channels, conf)
def create_connection(self):
return crlf_ssl_tcp(self.server, self.port, self.ignore_cert_errors)

187
core/main.py Normal file
View file

@ -0,0 +1,187 @@
import thread
import traceback
thread.stack_size(1024 * 512) # reduce vm size
class Input(dict):
def __init__(self, conn, raw, prefix, command, params,
nick, user, host, paraml, msg):
chan = paraml[0].lower()
if chan == conn.nick.lower(): # is a PM
chan = nick
def say(msg):
conn.msg(chan, msg)
def reply(msg):
if chan == nick: # PMs don't need prefixes
conn.msg(chan, msg)
else:
conn.msg(chan, '(' + nick + ') ' + msg)
def pm(msg):
conn.msg(nick, msg)
def set_nick(nick):
conn.set_nick(nick)
def me(msg):
conn.msg(chan, "\x01%s %s\x01" % ("ACTION", msg))
def notice(msg):
conn.cmd('NOTICE', [nick, msg])
dict.__init__(self, conn=conn, raw=raw, prefix=prefix, command=command,
params=params, nick=nick, user=user, host=host,
paraml=paraml, msg=msg, server=conn.server, chan=chan,
notice=notice, say=say, reply=reply, pm=pm, bot=bot,
me=me, set_nick=set_nick, lastparam=paraml[-1])
# make dict keys accessible as attributes
def __getattr__(self, key):
return self[key]
def __setattr__(self, key, value):
self[key] = value
def run(func, input):
args = func._args
if 'inp' not in input:
input.inp = input.paraml
if args:
if 'db' in args and 'db' not in input:
input.db = get_db_connection(input.conn)
if 'input' in args:
input.input = input
if 0 in args:
out = func(input.inp, **input)
else:
kw = dict((key, input[key]) for key in args if key in input)
out = func(input.inp, **kw)
else:
out = func(input.inp)
if out is not None:
input.reply(unicode(out))
def do_sieve(sieve, bot, input, func, type, args):
try:
return sieve(bot, input, func, type, args)
except Exception:
print 'sieve error',
traceback.print_exc()
return None
class Handler(object):
'''Runs plugins in their own threads (ensures order)'''
def __init__(self, func):
self.func = func
self.input_queue = Queue.Queue()
thread.start_new_thread(self.start, ())
def start(self):
uses_db = 'db' in self.func._args
db_conns = {}
while True:
input = self.input_queue.get()
if input == StopIteration:
break
if uses_db:
db = db_conns.get(input.conn)
if db is None:
db = bot.get_db_connection(input.conn)
db_conns[input.conn] = db
input.db = db
run(self.func, input)
def stop(self):
self.input_queue.put(StopIteration)
def put(self, value):
self.input_queue.put(value)
def dispatch(input, kind, func, args, autohelp=False):
for sieve, in bot.plugs['sieve']:
input = do_sieve(sieve, bot, input, func, kind, args)
if input == None:
return
if autohelp and args.get('autohelp', True) and not input.inp \
and func.__doc__ is not None:
input.notice(func.__doc__)
return
if func._thread:
bot.threads[func].put(input)
else:
thread.start_new_thread(run, (func, input))
def match_command(command):
commands = list(bot.commands)
# do some fuzzy matching
prefix = filter(lambda x: x.startswith(command), commands)
if len(prefix) == 1:
return prefix[0]
elif prefix and command not in prefix:
return prefix
return command
def main(conn, out):
inp = Input(conn, *out)
# EVENTS
for func, args in bot.events[inp.command] + bot.events['*']:
dispatch(Input(conn, *out), "event", func, args)
if inp.command == 'PRIVMSG':
# COMMANDS
if inp.chan == inp.nick: # private message, no command prefix
prefix = r'^(?:[.]?|'
else:
prefix = r'^(?:[.]|'
command_re = prefix + inp.conn.nick
command_re += r'[:]+\s+)(\w+)(?:$|\s+)(.*)'
m = re.match(command_re, inp.lastparam)
if m:
trigger = m.group(1).lower()
command = match_command(trigger)
if isinstance(command, list): # multiple potential matches
input = Input(conn, *out)
input.notice("Did you mean %s or %s?" %
(', '.join(command[:-1]), command[-1]))
elif command in bot.commands:
input = Input(conn, *out)
input.trigger = trigger
input.inp_unstripped = m.group(2)
input.inp = input.inp_unstripped.strip()
func, args = bot.commands[command]
dispatch(input, "command", func, args, autohelp=True)
# REGEXES
for func, args in bot.plugs['regex']:
m = args['re'].search(inp.lastparam)
if m:
input = Input(conn, *out)
input.inp = m
dispatch(input, "regex", func, args)

161
core/reload.py Normal file
View file

@ -0,0 +1,161 @@
import collections
import glob
import os
import re
import sys
import traceback
if 'mtimes' not in globals():
mtimes = {}
if 'lastfiles' not in globals():
lastfiles = set()
def make_signature(f):
return f.func_code.co_filename, f.func_name, f.func_code.co_firstlineno
def format_plug(plug, kind='', lpad=0, width=40):
out = ' ' * lpad + '%s:%s:%s' % make_signature(plug[0])
if kind == 'command':
out += ' ' * (50 - len(out)) + plug[1]['name']
if kind == 'event':
out += ' ' * (50 - len(out)) + ', '.join(plug[1]['events'])
if kind == 'regex':
out += ' ' * (50 - len(out)) + plug[1]['regex']
return out
def reload(init=False):
changed = False
if init:
bot.plugs = collections.defaultdict(list)
bot.threads = {}
core_fileset = set(glob.glob(os.path.join("core", "*.py")))
for filename in core_fileset:
mtime = os.stat(filename).st_mtime
if mtime != mtimes.get(filename):
mtimes[filename] = mtime
changed = True
try:
eval(compile(open(filename, 'U').read(), filename, 'exec'),
globals())
except Exception:
traceback.print_exc()
if init: # stop if there's an error (syntax?) in a core
sys.exit() # script on startup
continue
if filename == os.path.join('core', 'reload.py'):
reload(init=init)
return
fileset = set(glob.glob(os.path.join('plugins', '*.py')))
# remove deleted/moved plugins
for name, data in bot.plugs.iteritems():
bot.plugs[name] = [x for x in data if x[0]._filename in fileset]
for filename in list(mtimes):
if filename not in fileset and filename not in core_fileset:
mtimes.pop(filename)
for func, handler in list(bot.threads.iteritems()):
if func._filename not in fileset:
handler.stop()
del bot.threads[func]
# compile new plugins
for filename in fileset:
mtime = os.stat(filename).st_mtime
if mtime != mtimes.get(filename):
mtimes[filename] = mtime
changed = True
try:
code = compile(open(filename, 'U').read(), filename, 'exec')
namespace = {}
eval(code, namespace)
except Exception:
traceback.print_exc()
continue
# remove plugins already loaded from this filename
for name, data in bot.plugs.iteritems():
bot.plugs[name] = [x for x in data
if x[0]._filename != filename]
for func, handler in list(bot.threads.iteritems()):
if func._filename == filename:
handler.stop()
del bot.threads[func]
for obj in namespace.itervalues():
if hasattr(obj, '_hook'): # check for magic
if obj._thread:
bot.threads[obj] = Handler(obj)
for type, data in obj._hook:
bot.plugs[type] += [data]
if not init:
print '### new plugin (type: %s) loaded:' % \
type, format_plug(data)
if changed:
bot.commands = {}
for plug in bot.plugs['command']:
name = plug[1]['name'].lower()
if not re.match(r'^\w+$', name):
print '### ERROR: invalid command name "%s" (%s)' % (name,
format_plug(plug))
continue
if name in bot.commands:
print "### ERROR: command '%s' already registered (%s, %s)" % \
(name, format_plug(bot.commands[name]),
format_plug(plug))
continue
bot.commands[name] = plug
bot.events = collections.defaultdict(list)
for func, args in bot.plugs['event']:
for event in args['events']:
bot.events[event].append((func, args))
if init:
print ' plugin listing:'
if bot.commands:
# hack to make commands with multiple aliases
# print nicely
print ' command:'
commands = collections.defaultdict(list)
for name, (func, args) in bot.commands.iteritems():
commands[make_signature(func)].append(name)
for sig, names in sorted(commands.iteritems()):
names.sort(key=lambda x: (-len(x), x)) # long names first
out = ' ' * 6 + '%s:%s:%s' % sig
out += ' ' * (50 - len(out)) + ', '.join(names)
print out
for kind, plugs in sorted(bot.plugs.iteritems()):
if kind == 'command':
continue
print ' %s:' % kind
for plug in plugs:
print format_plug(plug, kind=kind, lpad=6)
print