Python3 is now an additonal target for the bot, and compatibility issues are bugs.
--- /dev/null
+Modular {Python2,Python3} IRC bot
+=================================
+
+Getting started:
+- `cp bot.config.example bot.config`
+- `vim bot.config`
+- Create a MySQL database, i.e. `CREATE DATABASE foo; GRANT ALL ON foo.* TO ...`
+- `mysql <dump.sql`
+- `./run.sh`
+
+Install croncheck.sh in your crontab, if desired.
+`* * * * * /path/to/erebus/croncheck.sh`
+To suppress croncheck.sh from restarting the bot without removing from crontab, `touch dontstart`
+
+Output will be placed in `logfile`, which is rotated to `oldlogs/`. (I strongly recommend `rm oldlogs/*` as a weekly crontab entry.)
+
+The bot targets both Python 2 and 3. However, it is generally only actively tested on Python 2.
+If it's not working on Python 3 (or an included module isn't working on Python 3), please raise a bug.
+
+Some modules require additional supporting materials, which can be found in `modules/contrib/`.
+
+*****
+Module API
+==========
+The module API has largely remained backwards-compatible and likely will remain so into the future. However, it is still currently unstable, primarily because it's only tested with the included modules. If you find a change was introduced which breaks something you relied on, please raise a bug.
+
+There is currently no documentation as to... well, anything. A good starter template for a new module is `modules/eval.py`. `modules/control.py` uses a significant subset of the API features available. `modules/foo.py` is intended as a demonstration module, and documents some of the major features.
MAXLEN = 400 # arbitrary max length of a command generated by Bot.msg functions
-class MyTimer(threading._Timer):
+if sys.version_info.major < 3:
+ timerbase = threading._Timer
+else:
+ timerbase = threading.Timer
+class MyTimer(timerbase):
def __init__(self, *args, **kwargs):
- threading._Timer.__init__(self, *args, **kwargs)
+ timerbase.__init__(self, *args, **kwargs)
self.daemon = True
class BotConnection(object):
def __init__(self, parent, bind, server, port):
self.parent = parent
- self.buffer = ''
+ self.buffer = bytearray(8192)
self.socket = None
self.bind = bind
def send(self, line):
if self.parent.parent.cfg.getboolean('debug', 'io'):
self.parent.log('O', line)
-# print "%09.3f %s [O] %s" % (time.time() % 100000, self.parent.nick, line)
self.bytessent += len(line)
self._write(line)
def _write(self, line):
- self.socket.sendall(line+"\r\n")
+ self.socket.sendall(line.encode('utf-8', 'backslashreplace')+b"\r\n")
def read(self):
self.buffer += self.socket.recv(8192)
lines = []
- while "\r\n" in self.buffer:
- pieces = self.buffer.split("\r\n", 1)
-# self.parent.log('I', pieces[0]) # replaced by statement in Bot.parse()
-# print "%09.3f %s [I] %s" % (time.time() % 100000, self.parent.nick, pieces[0])
- lines.append(pieces[0])
+ while b"\r\n" in self.buffer:
+ pieces = self.buffer.split(b"\r\n", 1)
+ lines.append(pieces[0].decode('utf-8', 'backslashreplace'))
self.buffer = pieces[1]
return lines
# Erebus IRC bot - Author: John Runyon
# "Config" class (reading/providing access to bot.config)
-import ConfigParser
+from __future__ import print_function
+import sys
+
+if sys.version_info.major < 3:
+ import ConfigParser
+else:
+ import configparser as ConfigParser
class Config(object):
def __init__(self, filename, writeout=True):
for s in cfg.config.sections():
for k, v in cfg.items(s):
- print "[%r][%r] = %r" % (s, k, v)
-# for k, v in cfg.items():
-# print 'erebus.'+k, '=', v
+ print("[%r][%r] = %r" % (s, k, v))
# Erebus IRC bot - Author: John Runyon
# module loading/unloading/tracking code
+from __future__ import print_function
+
import sys, time
import modlib
+if sys.version_info.major >= 3:
+ from importlib import reload
+
modules = {}
dependents = {}
#dependents[modname] = [list of modules which depend on modname]
def load(parent, modname, dependent=False):
#wrapper to call _load and print return
if dependent:
- print "(Loading dependency %s..." % (modname),
+ print("(Loading dependency %s..." % (modname), end=' ')
else:
- print "%09.3f [MOD] [?] Loading %s..." % (time.time() % 100000, modname),
+ print("%09.3f [MOD] [?] Loading %s..." % (time.time() % 100000, modname), end=' ')
modstatus = _load(parent, modname, dependent)
if not modstatus:
if dependent:
- print "failed: %s)" % (modstatus),
+ print("failed: %s)" % (modstatus), end=' ')
else:
- print "failed: %s." % (modstatus)
+ print("failed: %s." % (modstatus))
elif modstatus == True:
if dependent:
- print "OK)",
+ print("OK)", end=' ')
else:
- print "OK."
+ print("OK.")
else:
if dependent:
- print "OK: %s)" % (modstatus),
+ print("OK: %s)" % (modstatus), end=' ')
else:
- print "OK: %s." % (modstatus)
+ print("OK: %s." % (modstatus))
return modstatus
def _load(parent, modname, dependent=False):
successstatus = []
if not isloaded(modname):
try:
- mod = __import__('modules.'+modname, globals(), locals(), ['*'], -1)
+ mod = __import__('modules.'+modname, globals(), locals(), ['*'], 0)
# ^ fromlist doesn't actually do anything(?) but it means we don't have to worry about this returning the top-level "modules" object
reload(mod) #in case it's been previously loaded.
except Exception as e:
# Erebus IRC bot - Author: John Runyon
# main startup code
+from __future__ import print_function
+
import os, sys, select, MySQLdb, MySQLdb.cursors, time, random, gc
import bot, config, ctlmod
return select.select(self.fdlist, [], [])[0]
def connectall(self):
- for bot in self.bots.itervalues():
+ for bot in self.bots.values():
if bot.conn.state == 0:
bot.connect()
return ctlmod.modules[name]
def log(self, source, level, message):
- print "%09.3f %s [%s] %s" % (time.time() % 100000, source, level, message)
+ print("%09.3f %s [%s] %s" % (time.time() % 100000, source, level, message))
def getuserbyauth(self, auth):
- return [u for u in self.users.itervalues() if u.auth == auth.lower()]
+ return [u for u in self.users.values() if u.auth == auth.lower()]
#bind functions
def hook(self, word, handler):
# module helper functions, see modules/modtest.py for usage
# This file is released into the public domain; see http://unlicense.org/
+import sys
+
+if sys.version_info.major < 3:
+ stringbase = basestring
+else:
+ stringbase = str
+
class error(object):
def __init__(self, desc):
self.errormsg = desc
# non-empty string (or anything else True-y): specified success
#"specified" values will be printed. unspecified values will result in "OK" or "failed"
self.parent = parent
- for cmd, func in self.hooks.iteritems():
+ for cmd, func in self.hooks.items():
self.parent.hook(cmd, func)
self.parent.hook("%s.%s" % (self.name, cmd), func)
- for num, func in self.numhooks.iteritems():
+ for num, func in self.numhooks.items():
self.parent.hooknum(num, func)
- for chan, func in self.chanhooks.iteritems():
+ for chan, func in self.chanhooks.items():
self.parent.hookchan(chan, func)
for func, args, kwargs in self.helps:
pass
return True
def modstop(self, parent):
- for cmd, func in self.hooks.iteritems():
+ for cmd, func in self.hooks.items():
parent.unhook(cmd, func)
parent.unhook("%s.%s" % (self.name, cmd), func)
- for num, func in self.numhooks.iteritems():
+ for num, func in self.numhooks.items():
parent.unhooknum(num, func)
- for chan, func in self.chanhooks.iteritems():
+ for chan, func in self.chanhooks.items():
parent.unhookchan(chan, func)
for func, args, kwargs in self.helps:
cmd = _cmd #...and restore it
if cmd is None:
cmd = func.__name__ # default to function name
- if isinstance(cmd, basestring):
+ if isinstance(cmd, stringbase):
cmd = (cmd,)
func.needchan = needchan
@lib.help(None, "stops the bot")
def die(bot, user, chan, realtarget, *args):
quitmsg = ' '.join(args)
- for botitem in bot.parent.bots.itervalues():
+ for botitem in bot.parent.bots.values():
bot.conn.send("QUIT :Restarting. %s" % (quitmsg))
sys.exit(0)
os._exit(0)
@lib.argsEQ(0)
def modlist(bot, user, chan, realtarget, *args):
mods = ctlmod.modules
- for modname, mod in mods.iteritems():
+ for modname, mod in mods.items():
bot.msg(user, "- %s (%s) [%s]" % ((modname, mod.__file__, ', '.join(ctlmod.dependents[modname]))))
bot.msg(user, "Done.")
if chan is not None: replyto = chan
else: replyto = user
- try: exec ' '.join(args)
+ try: exec(' '.join(args))
except Exception: bot.msg(replyto, "Error: %s %s" % (sys.exc_info()[0], sys.exc_info()[1]))
else: bot.msg(replyto, "Done.")
filename = filepath
fo = open(filename, 'w')
lines = []
- for func in helps.itervalues():
+ for func in helps.values():
if module is not None and func.module != module:
continue
lines += _mkhelp(level, func)
def help_nolag(bot, user, chan, realtarget, *args):
if len(args) == 0: # list commands
lines = []
- for func in helps.itervalues():
+ for func in helps.values():
lines += _mkhelp(user, func)
for line in sorted(lines):
bot.slowmsg(user, str(line))
elif args[0].startswith("@"):
lines = []
mod = args[0][1:].lower()
- for func in helps.itervalues():
+ for func in helps.values():
if func.module == mod:
lines += _mkhelp(user, func)
for line in sorted(lines):
msg = pieces[3][1:]
mo = re_findsub.match(msg)
if mo:
- print lastline[chan]
- print mo.groupdict()
if mo.group('global') is not None:
count = 0 # unlimited
else:
count = 1 # only first
try:
newline = re.sub(mo.group('search'), mo.group('replace'), lastline[chan].msg, count)
- except Exception as e: print e; return # ignore it if it doesn't work
- print newline
+ except Exception as e: return # ignore it if it doesn't work
if newline != lastline[chan].msg:
if lastline[chan].sender == fromnick:
bot.msg(chan, "<%s> %s" % (lastline[chan].sender, newline))
# trivia module
# This file is released into the public domain; see http://unlicense.org/
+from __future__ import print_function
+
# module info
modinfo = {
'author': 'Erebus Team',
return lib.modstop(*args, **kwargs)
# module code
-import json, random, threading, re, time, datetime, os
+import json, random, threading, re, time, datetime, os, sys
+
+if sys.version_info.major < 3:
+ timerbase = threading._Timer
+else:
+ timerbase = threading.Timer
+
try:
import twitter
def country(num, default="??"):
return lib.mod('userinfo')._get(person(num), 'country', default=default).upper()
-class MyTimer(threading._Timer):
+class MyTimer(timerbase):
def __init__(self, *args, **kwargs):
- threading._Timer.__init__(self, *args, **kwargs)
+ timerbase.__init__(self, *args, **kwargs)
self.daemon = True
class TriviaState(object):
try:
state.steptimer.cancel()
except Exception as e:
- print "!!! steptimer.cancel(): %s %r" % (e,e)
+ print("!!! steptimer.cancel(): %s %r" % (e,e))
state.steptimer = None
try:
state.nextquestiontimer.cancel()
except Exception as e:
- print "!!! nextquestiontimer.cancel(): %s %r" % (e,e)
+ print("!!! nextquestiontimer.cancel(): %s %r" % (e,e))
state.nextquestiontimer = None
return True
state.pointvote = None
bot.msg(state.db['chan'], "Vote has been cancelled!")
except Exception as e:
- print e
+ print(e)
bot.msg(user, "Failed to set target.")
@lib.hook(needchan=False)
modstop = lib.modstop
# module code
-import re, urllib2, urlparse, json, HTMLParser
-from BeautifulSoup import BeautifulSoup
+import sys
+if sys.version_info.major < 3:
+ import urllib2
+ import urlparse
+ import HTMLParser
+ from BeautifulSoup import BeautifulSoup
+else:
+ import urllib.request as urllib2
+ import urllib.parse as urlparse
+ import html.parser as HTMLParser
+ from bs4 import BeautifulSoup
+
+import re, json
html_parser = HTMLParser.HTMLParser()
modstop = lib.modstop
# module code
-import json, urllib, time, rfc822
+import json, urllib, time
+from email.utils import parsedate
def location(person, default=None): return lib.mod('userinfo')._get(person, 'location', default=None)
return "That search term is ambiguous. Please be more specific."
current = weather['current_observation']
- measuredat = list(rfc822.parsedate(current['observation_time_rfc822']))
+ measuredat = list(parsedate(current['observation_time_rfc822']))
measuredat[6] = _dayofweek(current['observation_time_rfc822'][0:3])
measuredatTZ = current['local_tz_short']
loc = current['observation_location']
import sys
-sys.setdefaultencoding('utf-8')
+if sys.version_info.major < 3:
+ sys.setdefaultencoding('utf-8')