]> jfr.im git - erebus.git/commitdiff
add compatibility with Python3. add README.
authorzonidjan <redacted>
Sat, 7 Apr 2018 04:24:21 +0000 (23:24 -0500)
committerzonidjan <redacted>
Sat, 7 Apr 2018 04:24:21 +0000 (23:24 -0500)
Python3 is now an additonal target for the bot, and compatibility issues are bugs.

14 files changed:
README.md [new file with mode: 0644]
bot.py
config.py
ctlmod.py
erebus.py
modlib.py
modules/control.py
modules/eval.py
modules/help.py
modules/subtext.py
modules/trivia.py
modules/urls.py
modules/weather.py
sitecustomize.py

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..b489db3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+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.
diff --git a/bot.py b/bot.py
index 16979727e6194891ce0b02b024415ef8b2c687b7..0e6e9263d7b072ae372156346349c4f21195b092 100644 (file)
--- a/bot.py
+++ b/bot.py
@@ -8,9 +8,13 @@ from collections import deque
 
 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
 
 
@@ -424,7 +428,7 @@ class Bot(object):
 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
@@ -456,22 +460,19 @@ class BotConnection(object):
        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
index 6abad40d280df0145529a5e4c2fefaadd772ba58..4f3e3fb144ceac0b915ed2e96d7b3983e6dd01b5 100644 (file)
--- a/config.py
+++ b/config.py
@@ -1,7 +1,13 @@
 # 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):
@@ -59,6 +65,4 @@ if __name__ == '__main__':
 
        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))
index dbd2a36bdf5185a9935ea8115b85ea035ddc0d29..4d12d214a2e7f261f462f5ec4bcb1be9b00ed2ad 100644 (file)
--- a/ctlmod.py
+++ b/ctlmod.py
@@ -1,9 +1,14 @@
 # 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]
@@ -14,32 +19,32 @@ def modhas(modname, attname): return getattr(modules[modname], attname, None) is
 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:
index 3f26ea29063400686306f3b849ab8e1bb2373bf3..b8552e670749eb87c1009338b454ee67e42c35ce 100644 (file)
--- a/erebus.py
+++ b/erebus.py
@@ -3,6 +3,8 @@
 # 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
 
@@ -220,7 +222,7 @@ class Erebus(object): #singleton to pass around
                        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()
 
@@ -228,10 +230,10 @@ class Erebus(object): #singleton to pass around
                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):
index ff63a07612a9fd0fcf6157bb820275a7d635ace6..8bd6adb63c297500538c08a83c0d4d7233fe2ffc 100644 (file)
--- a/modlib.py
+++ b/modlib.py
@@ -2,6 +2,13 @@
 # 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
@@ -54,12 +61,12 @@ class modlib(object):
                # 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:
@@ -69,12 +76,12 @@ class modlib(object):
                                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:
@@ -107,7 +114,7 @@ class modlib(object):
                        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
index 884b82b8840d6cc5dac8f8f1a618760b39420e89..a34f795fcfe446c7a94c94d5b05238b675b1ecba 100644 (file)
@@ -27,7 +27,7 @@ from collections import deque
 @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)
@@ -71,7 +71,7 @@ def modreload(bot, user, chan, realtarget, *args):
 @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.")
 
index aadd81b97112cc6ead75b83d845736942dabcf98..e6fad75291396ead61bd498aa53332bd25e58613 100644 (file)
@@ -44,7 +44,7 @@ def cmd_exec(bot, user, chan, realtarget, *args):
        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.")
 
index 94c28eee1d1b1cefe8d40b3d58de5048b24aebdb..fc0dfe5392fbbc9de9f6c868233ec1747e8a5e91 100644 (file)
@@ -119,7 +119,7 @@ def _genhelp(bot, user, chan, realtarget, *args):
                        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)
@@ -179,7 +179,7 @@ def showcommands(bot, user, chan, realtarget, *args):
 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))
@@ -187,7 +187,7 @@ def help_nolag(bot, user, chan, realtarget, *args):
        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):
index bef2ac18f822d45f8d3372dba978992105ad9b3f..ca0ab0475154cd32793a9487b66b29f80a77d4b3 100644 (file)
@@ -32,16 +32,13 @@ def privmsg_hook(bot, line):
        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))
index 042b1bb5a49156d786559e99226c699e4ffc5cef..9f7624fef960c252e90287bb100fbcff5666d00e 100644 (file)
@@ -2,6 +2,8 @@
 # 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',
@@ -28,7 +30,13 @@ def modstop(*args, **kwargs):
        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
@@ -58,9 +66,9 @@ def pts(num):
 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):
@@ -520,12 +528,12 @@ def stop():
        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
 
@@ -611,7 +619,7 @@ def settarget(bot, user, chan, realtarget, *args):
                        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)
index 4c4bf6b54799926a80c0cd825c9c79983036ba3d..e25777873835da3f7b051cdbc6143444fb3ce478 100644 (file)
@@ -20,8 +20,19 @@ modstart = lib.modstart
 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()
 
index b1cd03a28922cf2220a0671a844870e6e056481c..27fc9013d7423ad9a1968fb39e005265fc7136f4 100644 (file)
@@ -18,7 +18,8 @@ modstart = lib.modstart
 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)
 
@@ -37,7 +38,7 @@ def _weather(place):
                                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']
index 2417d4da4d89289a0e1151e31d1f98b35cd04fdd..c70d4fa486b29444a5fdc405faad47cbbc006c09 100644 (file)
@@ -1,2 +1,3 @@
 import sys
-sys.setdefaultencoding('utf-8')
+if sys.version_info.major < 3:
+       sys.setdefaultencoding('utf-8')