Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial import

  • Loading branch information...
commit df02500c5d155eda8473507ef94cdec79aeb28e7 0 parents
@richo richo authored
17 TODO
@@ -0,0 +1,17 @@
+This code will become the new trunk soon.
+
+I see no reason to maintain the legacy crap.
+
+TODO
+----
+
+Flesh out the API to let it do everything
+
+More verbose admin commands
+
+Handler for adding modules to other channels
+
+Handler for telling the bot what to do
+
+BUG: python2.6 won't create an args module which breaks the auto restart code
+
53 auth.py
@@ -0,0 +1,53 @@
+import hashlib
+import pickle
+import atexit
+import logging
+from lib import *
+
+AUTH_FILE_NAME="authen.db"
+
+class Authenticator(object):
+ """\
+An object that statefully keeps track of all authentication
+It is managed by AuthModule which tracks the server messages
+ """
+ def __init__(self, auth_hash='', valid_host=''):
+ self.auth_hash = auth_hash
+ self.valid_host = valid_host
+ self.authenticated = LowerList()
+ self.load()
+ atexit.register(self.save)
+
+ def try_auth(self, msg, password):
+ if msg.nick in self.authenticated:
+ return True
+ # Max kludge per user password
+ if hashlib.md5(password).hexdigest() == self.auth_hash:
+ self.authenticated.append(msg.nick)
+ return True
+ return False
+
+ def revoke_auth(self, nick):
+ count = 0
+ try:
+ while True:
+ self.authenticated.remove(nick)
+ count+=1
+ except ValueError:
+ return count > 0
+
+ def authed(self, msg):
+ return msg.nick in self.authenticated
+
+ def save(self):
+ pickle.dump(self.auth_data, open(AUTH_FILE_NAME, 'w'))
+ def load(self):
+ try:
+ fh = open(AUTH_FILE_NAME, 'r')
+ self.auth_data = pickle.load(fh)
+ except IOError:
+ self.auth_data = {}
+
+ # TODO
+ # Authentication decorator
+
70 config.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+"""A module wrapper around the config files for pyBawt"""
+__all__ = []
+
+# This is tremendous hax, just a piece of scaffold to avoid breaking the old
+# API
+# Also, bail the fuck out if the config is incomplete
+
+import logging
+import os
+
+class InvalidConfig(Exception):
+ pass
+class NoConfigFile(Exception):
+ pass
+
+
+CONFIG_FILE = 'pyBawt.conf'
+logging.info("Commencing config read from %s" % (os.path.join(os.getcwd(), CONFIG_FILE)))
+config = {}
+boolvalues = { 'true' : True,
+ '1' : True,
+ 'yes' : True,
+ 'false': False,
+ '0' : False,
+ 'no' : False }
+def to_bool(val):
+ return boolvalues[val.lower()]
+def to_list(val):
+ return list(map(lambda x: x.strip(), val.split(',')))
+def passthru(val):
+ return val
+# A tuple of tuples with a key and an transformer
+keys = (('host', passthru),
+ ('ssl' , to_bool),
+ ('nick', passthru),
+ ('port', int),
+ ('auth_host', passthru),
+ ('auth_hash', passthru),
+ ('channels' , to_list),
+ ('nickserv_nick', passthru),
+ ('nickserv_pass', passthru)
+ )
+
+with open(CONFIG_FILE) as fh:
+ for line in fh:
+ line = line.strip()
+ if line.startswith("#"):
+ # Ignore comments
+ continue
+ if not line:
+ continue
+ try:
+ key, value = map(lambda x: x.strip(),line.split('=', 1))
+ config[key] = value
+ except ValueError:
+ raise InvalidConfig, "Invalid key value pair at (FIXME Lineno)"
+if not config:
+ logging.error("Couldn't open config file")
+ raise NoConfigFile
+ # FIXME distinguish between not there and inaccessible
+for name, transformer in keys:
+ __all__.append(name)
+ try:
+ # This is fucked XXX FIXME
+ #__dict__[name] = transformer(config[name])
+ exec('%s = %s' % (name, repr(transformer(config[name]))))
+
+ except ValueError: #Probably port didn't translate
+ raise InvalidConfig
579 ircSocket.py
@@ -0,0 +1,579 @@
+import threading
+import socket
+import sys
+import time
+import ssl
+import re
+# Rebuild module tree
+import regen_modules
+regen_modules.rebuild_bModules()
+import bModules
+import config
+import signal
+import auth
+import logging
+
+class ModuleError(Exception):
+ pass
+
+class IrcDisconnected(Exception):
+ pass
+
+class IrcTerminated(Exception):
+ pass
+class FlushQueue(Exception):
+ """ Flush the event queue, don't wait for IO"""
+ pass
+class ModulesDidntLoadDueToSyntax(Exception):
+ def __nonzero__(self):
+ # This allows us to retain the logical "if status" test.
+ return False
+
+def should_reconnect():
+ """This hook lies in here because it'll give the rest of the structure a fairly central place
+ to pull strings from to arrange whether or not to do shit"""
+ return True
+
+# These calls need to be abstracted further, if I can do that then I can convince this module
+# to update itself on the fly
+
+Modules = {}
+
+def mangle(name):
+ if len(name) < 8:
+ name += "_"
+ else:
+ name = name[:6] + "00"
+ return name
+
+def _load_modules():
+ global Modules
+ global bModules
+ # This hook generates our bModules instance.
+ regen_modules.rebuild_bModules()
+
+ # Test for syntax errors...
+ try:
+ bModules = reload(bModules)
+ except SyntaxError as e:
+ tb = sys.exc_info()[2]
+ exc_type, exc_value, exc_tb = sys.exc_info()
+ raise ModulesDidntLoadDueToSyntax(exc_type, exc_value, exc_tb)
+ Modules = bModules.modules
+_load_modules()
+
+#RE_NICK_MATCH = re.compile(r":([A-Za-z0-9_-^`]+)!([A-Za-z0-9_-]+)@([A-Za-z0-9_\.-])")
+RE_NICK_MATCH = re.compile(r":([A-Za-z0-9\[\]\^\\~`_-]+)!([~A-Za-z0-9]+)@([A-Za-z0-9_\.-]+)")
+RE_INFO_MATCH = re.compile(r":([A-Za-z0-9\[\]\^\\~`_-]+)!([~A-Za-z0-9]+)@([A-Za-z0-9_\.-]+)")
+
+class irc_data(object):
+ def __init__(self, data):
+ self.data = data
+
+ def __eq__(self, cmp):
+ if type(cmp) == int:
+ return self.data == cmp
+ elif type(cmp) == str:
+ return str(self.data) == cmp
+ else:
+ return self.data == cmp
+IRC_MOTD_START = irc_data(375)
+IRC_MOTD_DATA = irc_data(372)
+IRC_NICK_IN_USE = irc_data(433)
+IRC_NICK_NOT_AVAILABLE = irc_data(432)
+IRC_TOPIC = irc_data(332)
+
+
+class Message(object):
+ def __init__(self, msg):
+ self.msg = msg
+ self.data_segment = None
+ self.address_segment = None
+ self.nick = None
+ self.name = None
+ self.host = None
+ self.numeric = False
+ self._debug = False
+ self.source, self.event, self.data = msg.split(" ", 2)
+ self.event = self.event.upper()
+ self.replyto = None
+ self.origin = None
+ try:
+ int(self.event)
+ self.numeric = True
+ if ":" in self.data:
+ self.address_segment, self.data_segment = [i.strip() for i in self.data.split(":", 1)]
+ except ValueError:
+ # For the most part, we can safely only look at stuff that's non-numeric
+ # Implies that self.event wasn't a number
+ if ":" in self.data:
+ self.address_segment, self.data_segment = [i.strip() for i in self.data.split(":", 1)]
+ else:
+ logging.fixme("No address segment: %s" % self.data)
+ self.data_segment = self.data
+ self.address_segment = "<NoAddress>"
+ # We go a bit further in attempting to gather info...
+ #:richo!richo@staffers.psych0tik.net
+ m = RE_NICK_MATCH.search(self.source)
+ if m:
+ self.nick = m.group(1)
+ self.name = m.group(2)
+ self.host = m.group(3)
+ # Hax to make this slightly more logical
+ if self.event == "JOIN":
+ self.address_segment = self.data_segment
+ # Hanlder hax
+ if self.event == "MODE":
+ self.address_segment = self.data_segment.split(" ", 1)[0]
+ if self.is_private():
+ self.replyto = self.nick
+ self.origin = 'privmsg'
+ else:
+ self.replyto = self.address_segment
+ self.origin = self.address_segment
+
+ def parse_modes(self):
+ if self.event != "MODE":
+ return None
+ channel, modes, nicks = self.data_segment.split(" ", 2)
+ return (channel, modes, nicks)
+ def is_private(self):
+ # This needs to be more global:
+ try:
+ if not self.address_segment[0] in ["!", "&", "#"]:
+ return True
+ except TypeError:
+ pass
+ return False
+
+ def __str__(self):
+ return self.msg
+ def dump(self):
+ return " ".join(["Data Segment : %s" % (self.data_segment),
+ "Address Segment : %s" % (self.address_segment),
+ "Source : %s" % (self.source),
+ "Event : %s" % (self.event),
+ "Data : %s" % (self.data),
+ "Nick : %s" % (self.nick),
+ "Name : %s" % (self.name),
+ "Host : %s" % (self.host),
+ "--"])
+
+class chatnet(object):
+ def __init__(self, host, port=6667, use_ssl=False):
+ self.auth_host = ''
+ self.auth_hash = ''
+ self.nick = ""
+ self._debug = False
+
+ self.auth_host = config.auth_host
+ self.auth_hash = config.auth_hash
+ self.authenticator = auth.Authenticator(auth_hash = self.auth_hash, valid_host = self.auth_host)
+
+ self.ready_signal = IRC_MOTD_START
+ self.ready = False
+ self.data = ""
+ self._queue = []
+ self.queue = []
+ self.host = host
+ self.port = port
+ self.use_ssl = use_ssl
+ self.channels = {'privmsg': Channel("privmsg", self)}
+ self.sock = SockConnect(self.host, self.port, self.use_ssl)
+ self.msg = self.privmsg
+ self.chore_queue = []
+ self.event_handlers = {
+ 'JOIN': self.handle_join
+ }
+
+ def recv_wait(self):
+#This method pulls data back from the server and queues it for processing.
+#So far it's considerations are:
+#Don't do anything particularly clever with the last item, it may be incomplete
+#Handle PING/PONG instantly.
+#XXX TODO
+ buf = self.sock.recv(1024)
+ if not buf:
+ raise IrcDisconnected
+ self.data += buf
+ self._queue += self.data.split("\r\n")
+ if self.data.endswith("\r\n"):
+ self.data = ""
+ else:
+ self.data = self._queue.pop()
+ # continue with execution
+
+ def _handle(self, msg):
+# XXX This desperately needs fleshing out
+# if addressed to channel
+# -> locate channel object and add to queue
+# -- If not joined to channel, add channel object and populate queue
+# -> if in doubt, dump to main queue
+# -> Debug condition, self.debug everything-
+
+# The _handle method also need
+
+ if msg.upper().startswith("PING"):
+ self.write(msg.upper().replace("PING", "PONG"))
+ return
+ message = Message(msg)
+ # Have code for catching identify here.
+ # IF ident successful
+ # -> Set self.nick
+ # IF ident failed
+ # -> Mangle nick and try again. ## NEEDS NICK TO DO THIS. Set nick in identify
+ if message.event == IRC_TOPIC:
+ self.handle_topic(message)
+ if message.event == IRC_NICK_IN_USE:
+ self.retry_identify()
+ if message.event == IRC_NICK_NOT_AVAILABLE:
+ self.retry_identify()
+
+ if message.event == self.ready_signal:
+ self.ready = True
+
+ # Start handling.
+ # TODO, do whatever hax need to be done to populate the channel's topic element.
+ # Perfectly valid to have the channel itself put in a request for the topic in initialisation
+
+ if message.event in self.event_handlers:
+ self.event_handlers[message.event](message)
+
+ try:
+ chan = self.channels[message.address_segment]
+ chan.add_msg(message)
+ except:
+ # This creates channels for EVERYONE that privmsg's us.
+ # I'm not sure this is right, should privmsg's from nonchannels just go into an arbitrary queue?
+ # Ignore stuff for channels we don't know about.
+ self.channels['privmsg'].add_msg(message)
+ def handle_join(self, msg):
+ self.add_channel(msg.data_segment)
+
+ def handle_topic(self, msg):
+ #Source : :natalya.psych0tik.net
+ #Event : 332
+ #Data : pyBawt_ #rawptest :OBVIOUS TOPIC STRING
+ nick, chan = msg.address_segment.split(" ")[0:2]
+ topic = msg.data_segment
+ if chan in self.channels:
+ self.channels[chan].topic = topic
+
+ def add_channel(self, channel):
+ if channel not in self.channels:
+ self.channels[channel] = Channel(channel, self)
+
+ def dump_queue(self):
+ while self._queue:
+ msg = self._queue.pop(0)
+ if not msg:
+ continue
+ self._handle(msg)
+ for i in self.channels.values():
+ i.do_chores()
+ self._do_chores()
+
+ def dump_channel_data(self):
+ out = []
+ for i in self.channels:
+ out.append(i)
+ out.append(repr(self.channels[i].modules))
+ return out
+
+ def debug(self, msg):
+ if self._debug:
+ print msg
+
+ def available_modules(self):
+ out = []
+ for i in dir(bModules):
+ if i.endswith("Module"):
+ out.append(i)
+ return out
+
+ def retry_identify(self):
+ # We have been denied our nick, work out what to do.
+ if self.nickserv_info:
+ # We have nickserv info, so try to boot our ghost, however
+ # We need a valid nick to do this. TODO
+ pass
+ else:
+ self.identify(mangle(self.nick), self.nickserv_info)
+ # This call is precariously close to becoming recursive.
+ # Make sure that identify never has a direct call path here.
+
+ def auth_self(self, to, pas):
+ # privmsg bypasses the chores code, it seems
+ self.add_chore(self.privmsg, (to, "IDENTIFY %s" % pas))
+
+ def identify(self, nick, nickserv_info=None):
+ # TODO Have this do something a bit more intelligent
+ # If setting nick fails (trigger based on the queue, try again)
+ # Use the chore system
+ self.nickserv_info = nickserv_info
+ self.nick = nick
+ # We use _write because if we wait for readyness we'll be waiting a while..
+ self._write("USER %(nick)s * 8 :%(nick)s" % {"nick": nick})
+ self._write("NICK %(nick)s" % {"nick": nick})
+ # If we have ended up here and we have nickserv_info, one of two things has happened.
+ # a) our nick is taken and we need to change nick, so we can talk to nickserv, then arrange a call to this function
+ # to get our nick back and ID
+ # b) We have our nick (either because it was free or because we have ghosted our old nick)
+ # Either way, that should be handled out of _handle for an appropriate signal, potentially off the MOTD signal
+ # That tells us that we're ready
+ if self.nickserv_info:
+ self.ns_identify
+
+ def notice(self, to, msg):
+ # Should this be in the queue?
+ self.write("NOTICE %(to)s :%(msg)s" % {"to": to, "msg": msg})
+
+ def privmsg(self, to, msg):
+ self._write("PRIVMSG %(to)s :%(msg)s" % {"to": to, "msg": msg})
+
+ def action(self, to, msg):
+ self._write("PRIVMSG %(to)s :\x01ACTION %(msg)s\x01" % {"to": to, "msg": msg})
+
+ def kick(self, chan, nick, reason=''):
+ self.write("KICK %(chan)s %(nick)s :%(reason)s" % (
+ { 'chan': chan,
+ 'nick': nick,
+ 'reason': reason}))
+
+ def write(self, msg):
+ self.add_chore(self._write, [msg])
+
+ def _write(self, msg):
+ self.sock.send(msg + "\n")
+
+ def join(self, chan, key=""):
+ self.add_chore(self._join, [chan, key])
+
+ def part(self, chan, reason=""):
+ self.add_chore(self._part, [chan, reason])
+
+
+ def _part(self, chan, reason):
+ self._write("PART %(chan)s %(reason)s" % {'chan': chan, 'reason': reason})
+ # XXX Let a handler do this when we get notification from the server
+ # it should tell us our state, not let us dictate
+ if chan in self.channels:
+ del self.channels[chan]
+
+ def quit(self, quitmsg=""):
+ # Flush our queues before we leave.
+ self.write("QUIT :%s" % (quitmsg))
+ self._do_chores()
+ SockClose(self.sock)
+
+ def reload_modules(self):
+ """Reloads modules, returns true on success or a traceback object if shit hits the fan"""
+ try:
+ _load_modules()
+ except ModulesDidntLoadDueToSyntax as tb:
+ # TODO - this can't be right. Works, but looks all fucked up
+ # I'm positive that the native exception handling caters for this
+ return tb # Exception instance, contains exc_info
+ else:
+ for i in self.channels.values():
+ i.reload_modules()
+ return True
+ def reg_handler(self, signum, handler):
+ # Create a signal handler which inserts a call to handler into the chore stack
+ def _(*args):
+ # This is the function which will be called
+ self.add_chore(handler, [])
+ raise FlushQueue
+ signal.signal(signum, _)
+
+ def add_chore(self, method, args):
+ self.chore_queue.append((method, args))
+
+ def add_module(self, chan, module):
+ return self.channels[chan].add_module(module)
+
+ def del_module(self, chan, module):
+ try:
+ self.channels[chan].del_module(module)
+ except ModuleError:
+ print "Couldn't remove %s from %s" % (module, chan)
+ raise
+
+ def _join(self, chan, key=""):
+ if chan not in self.channels:
+ self.write("JOIN %(chan)s %(key)s" % {"chan": chan,
+ "key" : key})
+
+ # self.channels[chan] = channel()
+ # The recv parser will handle adding it, once we're actually joined.
+
+ def _do_chores(self):
+ ret = False
+ if not self.ready:
+ return
+ while self.chore_queue:
+ ret = True
+ chore = self.chore_queue.pop(0)
+ chore[0](* chore[1])
+ return ret
+
+class Nick(object):
+ # Dummy holder for nick properties
+ op = False
+ voice = False
+
+class Channel(object):
+ """Placeholder until I actually have something of use"""
+ def __init__(self, name, parent):
+ self.name = name.lower()
+ self.parent = parent
+ self.queue = []
+ self.mode = []
+ self.cmode = []
+ self.standing = []
+ self.modules = []
+ self.init_modules()
+ self.topic = ""
+ #self.get_topic()
+ self.modes = {
+ 'op': False,
+ 'halfop': False,
+ 'voice': False,
+ 'owner': False,
+ 'admin': False
+ }
+
+ def privmsg(self, msg):
+ """Hook for passing messages back when I have a channel, but not a parent"""
+ self.parent.privmsg(self.name, msg)
+ def mode_plus_b(self):
+ self.mode(self.nick, "+B")
+
+ def init_modules(self):
+ try:
+ for i in Modules[self.name]:
+ self.modules.append(i(self.parent, self))
+ except KeyError:
+ # No modules for this channel..
+ pass
+ def get_topic(self):
+ """Fire off a request for topic. This will be interpreted elsewhere"""
+ self.parent.write("TOPIC %(name)s" % {'name': self.name})
+
+ def set_topic(self, topic):
+ """Set the topic for this channel"""
+ self.parent.write("TOPIC %(name)s :%(topic)s" % {'name': self.name, 'topic': topic})
+ # TODO - Need to retrieve channel modes
+ # no +t means we don't need ops
+ return self.modes['op']
+
+ def reload_modules(self):
+ _load_modules()
+ self.modules = []
+ self.init_modules()
+
+ def add_module(self, module):
+ # Really haxy test for modules already loaded
+ try:
+ mod = getattr(bModules, module)
+ for i in self.modules:
+ logging.fixme("Scanning %s against %s" % (i, mod))
+ if isinstance(i, mod):
+ raise bModules.ModuleAlreadyLoaded
+ self.modules.append(mod(self.parent, self))
+ return True
+ except AttributeError:
+ return False
+
+ def dump_modules(self):
+ return ", ".join(repr(i) for i in self.modules)
+
+ def del_module(self, module):
+ m = getattr(bModules, module)
+ l = len(self.modules)
+ self.modules[:] = [i for i in self.modules if type(i) != type(m)]
+ if len(self.modules) == l:
+ raise ModuleError
+ return
+
+ def add_msg(self, msg):
+ self.queue.append(msg)
+
+ def handle_mode(self, msg):
+ m_map = {'+': True, '-': False}
+ o_map = {'o': 'op',
+ 'v': 'voice',
+ 'h': 'halfop',
+ 'a': 'admin',
+ 'q': 'owner'}
+ try:
+ chan, modes, nicks = msg.data_segment.split(" ", 2)
+ except:
+ # I don't know why non-channel mode events are winding up here
+ return
+ nicks = nicks.split(" ")
+ action = 'True'
+ for m in modes:
+ try:
+ action = m_map[m]
+ except KeyError:
+ nick = nicks.pop(0)
+ try:
+ if nick == self.parent.nick:
+ self.modes[o_map[m]] = action
+ else:
+ # TODO do something clever when other people's modes are changed.
+ pass
+ except KeyError:
+ # unrecognised mode.
+ print "Unrecognised mode!"
+ msg.dump()
+ def dump_modes(self):
+ return "%s: %s" % (self.name, repr(self.modes))
+
+ def do_chores(self):
+ # We load through a set of pluggable triggers.
+ # I have not done this yet, but I'm gunna....
+ while self.queue:
+ msg = self.queue.pop(0)
+ # Handle mode changes internally to update status
+ if msg.event == "MODE":
+ self.handle_mode(msg)
+ for i in self.modules:
+ if i.want(msg):
+ try:
+ # TODO - implement a signal stop
+ i.handle(msg)
+ except bModules.StopHandling:
+ break
+
+def SockConnect(host, port, use_ssl):
+ addr = (host, port)
+ sock = None
+ for res in socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM):
+ af, socktype, proto, canonname, sa = res
+ try:
+ sock = socket.socket(af, socktype, proto)
+ if use_ssl:
+ sock = ssl.wrap_socket(sock)
+ except socket.error, msg:
+ sock = None
+ continue
+ try:
+ sock.connect(sa)
+ except socket.error, msg:
+ sock.close()
+ sock = None
+ continue
+ break
+ if sock is None:
+ raise RuntimeError, "could not connect socket"
+ #sock.settimeout(1)
+ return sock
+
+def SockClose(sock):
+ try:
+ sock.shutdown(socket.SHUT_RDWR)
+ sock.close()
+ except:
+ pass
17 lib.py
@@ -0,0 +1,17 @@
+class Mapping(dict):
+ def __getitem__(self, item):
+ try:
+ return super(Mapping, self).__getitem__(item)
+ except KeyError:
+ self[item] = []
+ return self[item]
+
+class LowerList(list):
+ @staticmethod
+ def lower(n):
+ try:
+ return n.lower()
+ except AttributeError:
+ return n
+ def __contains__(self, value):
+ return self.lower(value) in map(self.lower, self)
44 logging.py
@@ -0,0 +1,44 @@
+import time
+import sys
+
+LOGFILE='pyBawt.log'
+
+def with_log(func):
+ def _(msg):
+ fh = open(LOGFILE, 'a')
+ func(msg, fh)
+ fh.close()
+ return _
+
+@with_log
+def log(msg, fh):
+ if not msg.endswith("\n"):
+ msg += "\n"
+ fh.write(msg)
+
+def fmt(msg):
+ return "%s | %s" % (time.asctime(), msg)
+
+def error(msg):
+ log("E: %s" % fmt(msg))
+
+def info(msg):
+ log("I: %s" % fmt(msg))
+
+def warn(msg):
+ log("W: %s" % fmt(msg))
+
+def fixme(msg):
+ log("X: %s" % fmt(msg))
+
+def fatal(msg):
+ log("F: %s" % fmt(msg))
+ sys.stderr.write(fmt(msg))
+ exit(1)
+
+class Writer(object):
+ def __init__(self, func):
+ self.func = func
+ def write(self, msg):
+ self.func(msg)
+
5 module_config.py
@@ -0,0 +1,5 @@
+# IZ FINAL
+
+modules = OurModules()
+modules["default"] = [ AddModule, HelpModule, SnackModule, AuthModule, SourceModule ]
+
202 modules.d/000_core.py
@@ -0,0 +1,202 @@
+"""This module represents the core features a developer needs to write modules
+for the pyBawt framework"""
+import re
+import time
+import ourgit
+import os
+import sys
+import logging
+
+VERSION="$Rev: 1252 $".split(" ")[1]
+
+def get_help(mdl):
+ try:
+ obj = globals()[mdl]
+ except KeyError:
+ return "No help for %s" % mdl
+ if hasattr(obj, '_commands'):
+ return ", ".join(obj._commands) + "\n" + str(obj.__doc__)
+ else:
+ return str(obj.__doc__)
+
+# CORE
+#-----
+
+class Restart(Exception):
+ pass
+class StopHandling(Exception):
+ pass
+
+class BawtModule(object):
+ """This is a hax module, it just says hi to people"""
+ matcher_re = "hi %(nick)s"
+ matcher_flags = 0
+ _name = "BawtModule"
+ def __init__(self, parent, channel):
+ self.parent = parent
+ self.channel = channel
+ self.rehash()
+ def __repr__(self):
+ return self._name
+ def __str__(self):
+ return self._name
+ def rehash(self):
+ """Reconstruct matcher regex"""
+ self.matcher = re.compile(self.matcher_re % {'nick': self.parent.nick}, # ADD more as neededh
+ self.matcher_flags)
+ def want(self, msg):
+ # OLD API, compat
+ return self.matcher.search(msg.data_segment)
+
+ def handle(self, msg):
+ self.parent.privmsg(msg.replyto, "%s: Hi!" % (msg.nick))
+
+class BawtM2(object):
+ """I'm a lazy programmer who doesn't write help files"""
+
+# Match everything by default
+ privmsg_re = "."
+ privmsg_flags = 0
+ topic_re = "."
+ topic_flags = 0
+ mode_re = "."
+ mode_flags = 0
+ join_re = "."
+ join_flags = 0
+ kick_re = "."
+ kick_flags = 0
+ part_re = "."
+ part_flags = 0
+ notice_re = "."
+ notice_flags = 0
+ # For !list
+ _name = "BawtM2"
+ # for is_action
+ action_matcher = re.compile("^\x01ACTION.*\x01$")
+ def __init__(self, parent, channel):
+ self.parent = parent
+ self.channel = channel
+ self.re_data = {'nick': self.parent.nick}# ADD more as neededh
+ self.rehash()
+
+ # Default authentication method is a match of the hostmask against the
+ # Message. I fully expect this to be overridden.
+
+ # Or against a static hash
+ # XXX Potentially allocates stale handles
+ self.auth = self.parent.authenticator.authed
+ self.revoke_auth = self.parent.authenticator.revoke_auth
+ self.on_load()
+
+ def __repr__(self):
+ return self._name
+ def __str__(self):
+ return self._name
+ def require(self, prop):
+ # TODO - Flesh these out
+ """XXX Unratified API"""
+ return self.__getattr__("_require_%s" % prop)()
+ def _require_op(self):
+ return self.channel.modes['op']
+ def rehash(self):
+ """Reconstruct matcher regex"""
+ self.matchers = { "JOIN": re.compile(self.join_re % self.re_data, self.join_flags),
+ "KICK": re.compile(self.kick_re % self.re_data, self.kick_flags),
+ "MODE": re.compile(self.mode_re % self.re_data, self.mode_flags),
+ "PRIVMSG": re.compile(self.privmsg_re % self.re_data, self.privmsg_flags),
+ "PART": re.compile(self.part_re % self.re_data, self.part_flags),
+ "TOPIC": re.compile(self.topic_re % self.re_data, self.topic_flags),
+ "NOTICE": re.compile(self.notice_re % self.re_data, self.notice_flags)
+ }
+
+ # API for adding stuff to this on the fly..
+ self.handlers = { "JOIN": self.handle_join,
+ "KICK": self.handle_kick,
+ "MODE": self.handle_mode,
+ "PRIVMSG": self.handle_privmsg,
+ "PART": self.handle_part,
+ "TOPIC": self.handle_topic,
+ "NOTICE": self.handle_notice
+ }
+ def want(self, msg):
+ try:
+ self.m = self.matchers[msg.event].search(msg.data_segment)
+ if self.m:
+ return True
+ else:
+ return False
+ except KeyError:
+ # We've been asked to handle something we're not aware of.. so we
+ # probably don't want it...
+ return False
+
+ # TODO Delegate to overridable from instance
+
+ def handle(self, msg):
+ # We contruct this list at runtime, which makes the objects mutable,
+ # whereas if it were constructed and stored, we would not be able to
+ # alter them on the fly
+ try:
+ self.handlers[msg.event](msg)
+ except KeyError:
+ logging.fixme("Module %s attached a handler to an event it couldn't handle" % (self._name))
+
+ def handle_privmsg(self, msg):
+ self.noop()
+
+ def handle_kick(self, msg):
+ self.noop()
+
+ def handle_join(self, msg):
+ self.noop()
+
+ def handle_mode(self, msg):
+ self.noop()
+
+ def handle_topic(self, msg):
+ self.noop()
+
+ def handle_part(self, msg):
+ self.noop()
+
+ def handle_notice(self, msg):
+ self.noop()
+
+ def noop(self, *args, **kwargs):
+ pass
+
+ def is_action(self, msg):
+ if self.action_matcher.match(msg.data_segment):
+ return True
+ else:
+ return False
+ def on_load(self, *args, **kwargs):
+ pass
+
+def _exit():
+ #template
+ sys.exit()
+
+
+class OurModules(object):
+ data = {'default': []}
+ nick = "dummynick"
+ def __setitem__(self, key, value):
+ if key not in self.data:
+ for i in self.data['default']:
+ value.append(i)
+ self.data[key] = value
+ def __getitem__(self, key):
+ if key in self.data:
+ return self.data[key]
+ else:
+ return self.data['default']
+ def dump(self):
+ dmp = []
+ for i in self.data:
+ dmp.append("%s:" % i)
+ dmp.append("[ %s ]" % (", ".join(repr(j(self, self)) for j in self.data[i])))
+ return dmp
+ def __repr__(self):
+ return "\n".join(dmp)
+
228 modules.d/core_modules.py
@@ -0,0 +1,228 @@
+import re
+import time
+import ourgit
+import os
+import sys
+import atexit
+import traceback
+from lib import *
+import logging
+
+VERSION="$Rev: 1252 $".split(" ")[1]
+
+class ModuleAlreadyLoaded(Exception):
+ pass
+
+# TODO Split this out into core modules, and then import a set of user added
+# ones. BawtModule and the stuff to do reloads is all that needs to be global
+# TODO Work out how the channel infrastructure will work (structured
+# import?)(Circular ref??)
+# Implement something to reference these by name instead of type hax.
+class WriteThing(object):
+ def __init__(self, writable, target):
+ self.w = writable
+ self.target = target
+ def write(self, msg):
+ self.w.privmsg(self.target, msg)
+
+
+class HelpModule(BawtM2):
+ """Allows for reading the docstring of arbitrary modules"""
+ _name = "HelpModule"
+ privmsg_re = "^(!|%(nick)s:\s+)(help) ?([^ ]*)"
+ def handle_privmsg(self, msg):
+ if self.m.group(3):
+ for i in get_help(self.m.group(3)).split("\n"):
+ self.parent.privmsg(msg.replyto, "%s: %s" % (msg.nick, i))
+ else:
+ self.parent.privmsg(msg.replyto, "%s: help [module]" % (msg.nick))
+class SourceModule(BawtM2):
+ """Contains the commands for interacting with pyBawts internal source management and update routines"""
+ _commands = ['reload', 'update', 'version']
+ privmsg_re = "^(!|%(nick)s:\s?)(%(commands)s)" % {'commands': "|".join(_commands),
+ 'nick': '%(nick)s'}
+ _name = "SourceModule"
+ def handle_privmsg(self, msg):
+ if not self.auth(msg):
+ self.parent.privmsg(msg.replyto, "%s: I don't know you." % (msg.nick))
+ return
+ argv = msg.data_segment.split(" ")
+ if self.m.group(2) == "reload":
+ status = self.parent.reload_modules()
+ if status:
+ self.parent.privmsg(msg.replyto, "%s: Reload Complete." % (msg.nick))
+ else:
+ t = WriteThing(self.parent, msg.nick)
+ self.parent.privmsg(msg.replyto, "%s: Reload Failed! (pm for stackdump)" % (msg.nick))
+ # TODO- this isn't actually the error in question. Lol.
+ #traceback.print_tb(status, file=t)
+ traceback.print_exception(*status.args, file=t)
+ elif self.m.group(2) == "update":
+ self.parent.privmsg(msg.replyto, ourgit.update_git())
+ elif self.m.group(2) == "version":
+ self.parent.privmsg(msg.replyto, "%(nick)s: %(version)s on %(branch)s" %
+ { "nick": msg.nick,
+ "version": ourgit.version(),
+ "branch": ourgit.current_branch()})
+ elif self.m.group(2) == "checkout":
+ self.parent.privmsg(msg.replyto, "Not implemented LOL!")
+
+class AdminModule(BawtM2):
+ """Houses nearly everything that needs Authentication"""
+ _commands = ['loaded', 'restart', 'del', 'modlist']
+ privmsg_re = "^(!|%(nick)s:\s?)(%(commands)s) ?" % {'commands': "|".join(_commands),
+ 'nick': '%(nick)s'}
+ _name = "AdminModule"
+ def handle_privmsg(self, msg):
+ if not self.auth(msg):
+ self.parent.privmsg(msg.replyto, "%s: I don't know you." % (msg.nick))
+ return
+ argv = msg.data_segment.split(" ")
+ # TODO - Second argument for channel.
+ # TODO - Hax at this to replace the argv[0] matching based on a regex group to pull the commnad
+
+ if self.m.group(2) == "loaded":
+ self.parent.privmsg(msg.replyto, self.channel.dump_modules())
+ elif self.m.group(2) == "del":
+ self.parent.privmsg(msg.replyto, "Sorry, not implemented")
+ elif self.m.group(2) == "restart":
+ raise Restart
+ elif self.m.group(2) == "modlist":
+ self.parent.privmsg(msg.replyto, "%(nick)s: %(mods)s" % {'nick': msg.nick,
+ 'mods': ', '.join(self.parent.available_modules())})
+ else:
+ self.parent.privmsg(msg.replyto, "I have NFI how you managed this.")
+
+class DebugModule(BawtM2):
+ """Houses nearly everything that needs Authentication"""
+ _commands = ['version', 'nick', 'uname', 'dumpchans', 'argv' ]
+ privmsg_re = "^!(%(commands)s)" % {'commands': "|".join(_commands)}
+ _name = "DebugModule"
+ def handle_privmsg(self, msg):
+ if not self.auth(msg):
+ self.parent.privmsg(msg.replyto, "%s: I don't know you." % (msg.nick))
+ return
+ argv = msg.data_segment.split(" ")
+ # TODO - Second argument for channel.
+ if argv[0] == "!version":
+ self.parent.privmsg(msg.replyto, "Modules: %(modv)s, Global: %(gvs)s" % {
+ 'modv': VERSION,
+ 'gvs': ourgit.version()
+ })
+ elif argv[0] == "!nick":
+ self.parent.privmsg(msg.replyto, "%s: As far as I know, my nick is %s" % (msg.nick, self.parent.nick))
+ elif argv[0] == "!uname":
+ self.parent.privmsg(msg.replyto, " ".join(os.uname()))
+ elif argv[0] == "!dumpchans":
+ for i in self.parent.dump_channel_data():
+ self.parent.privmsg(msg.replyto, i)
+ elif argv[0] == "!argv":
+ self.parent.privmsg(msg.replyto, str(sys.argv))
+ else:
+ self.parent.privmsg(msg.replyto, "I have NFI how you managed this.")
+
+class AddModule(BawtM2):
+ privmsg_re = "^(!|%(nick)s:\s?)(add) ([^ ]*)"
+ _name = "AddModule"
+ def handle_privmsg(self, msg):
+ if not self.auth(msg):
+ self.parent.privmsg(msg.replyto, "%s: I don't know you." % (msg.nick))
+ return
+ mod = self.m.group(3)
+ try:
+ if self.parent.add_module(msg.origin.lower(), mod):
+ logging.info("%s loaded module %s in %s" % (msg.nick, mod, msg.origin.lower()))
+ self.parent.privmsg(msg.replyto, "done.")
+ else:
+ self.parent.privmsg(msg.replyto, "No such module")
+ except ModuleAlreadyLoaded:
+ logging.info("%s attempted to load duplicate module %s in %s" % (msg.nick, mod, msg.origin.lower()))
+ self.parent.privmsg(msg.replyto, "Module already loaded in this context")
+ except IndexError:
+ self.parent.privmsg(msg.replyto, "Module?")
+
+class ChanModule(BawtM2):
+ _commands = ['join', 'part', 'kick']
+ privmsg_re = "^!(%(commands)s)" % {'commands': "|".join(_commands)}
+ _name = "ChanModule"
+ def handle_privmsg(self, msg):
+ if not self.auth(msg):
+ logging.info("%s attempted to %s without auth" % (msg.nick, msg.data_segment))
+ self.parent.privmsg(msg.replyto, "%s: I don't know you." % (msg.nick))
+ return
+ argv = msg.data_segment.split(" ")
+ if argv[0] == '!join':
+ if len(argv) == 1:
+ self.parent.privmsg(msg.replyto, "%s: Join which channel?" % (msg.nick))
+ return
+ self.parent.join(argv[1])
+ elif argv[0] == '!part':
+ if len(argv) == 1:
+ self.parent.part(msg.replyto)
+ else:
+ self.parent.part(argv[1])
+ elif argv[0] == '!kick':
+ if len(argv) == 1:
+ self.parent.privmsg(msg.replyto, "%s: Kick whom?" % (msg.nick))
+ return
+ self.parent.kick(msg.replyto, argv[1], "Zot! Kicked")
+
+
+class DumpModule(BawtM2):
+ _name = "DumpModule"
+ def handle_privmsg(self, msg):
+ msg.dump()
+ def handle_notice(self, msg):
+ msg.dump()
+
+class ChannelMapping(Mapping):
+ def refcount(self, nick, insensitive=True):
+ count = 0
+ if insensitive:
+ nick = nick.lower()
+ for i in self.itervalues():
+ # Not actually respecting insensitiv
+ if nick in map(lambda n: n.lower, i):
+ count += 1
+ return count
+
+class AuthModule(BawtM2):
+ _name = "AuthModule"
+ _commands = ['auth', 'status']
+ privmsg_re = "^!(%(commands)s)" % {'commands': "|".join(_commands)}
+ visible = ChannelMapping()
+ def handle_privmsg(self, msg):
+ # TODO Global is_private
+ argv = msg.data_segment.split(" ")
+ if argv[0] == "!auth":
+ if len(argv) == 1 or not msg.is_private():
+ self.parent.privmsg(msg.replyto,
+ "%s: Usage /msg %s !auth [password]" %
+ (msg.nick, self.parent.nick))
+ return
+ if self.parent.authenticator.try_auth(msg, argv[1]):
+ logging.info("%s authenticated successfully" % msg.nick)
+ self.parent.privmsg(msg.replyto, "This has been a triumph")
+ else:
+ logging.info("%s has failed to authenticated" % msg.nick)
+ self.parent.privmsg(msg.replyto, "You have chosen poorly")
+ return
+ elif argv[0] == "!status":
+ if self.auth(msg.nick):
+ self.parent.privmsg(msg.replyto, "You are identified")
+ else:
+ self.parent.privmsg(msg.replyto, "You are not identified")
+ def handle_nick(self, msg):
+ self.handle_part(msg)
+ def handle_join(self, msg):
+ logging.info("Sighting %s" % msg.nick)
+ self.visible[msg.address_segment].append(msg.nick)
+ def handle_part(self, msg):
+ logging.info("Losing sight of %s" % msg.nick)
+ try:
+ self.visible[msg.address_segment].remove(msg.nick)
+ except ValueError:
+ logging.fixme("%s left %s without having been seen, testing auth anyway" % (msg.nick, msg.address_segment))
+ if self.visible.refcount(msg.nick) == 0:
+ logging.info("Can't see %s; deauthing" % msg.nick)
+ self.parent.authenticator.revoke_auth(msg.nick)
8 modules.d/debug.py
@@ -0,0 +1,8 @@
+class DebugModule(BawtM2):
+ _name = "DebugModule"
+ privmsg_re = "(!|%(nick)s:\s?)repr"
+ def handle_privmsg(self, msg):
+ print repr(msg.data_segment)
+ self.parent.privmsg(msg.replyto, "%(nick)s: %(data)s" % {'nick': msg.nick,
+ 'data': repr(msg.data_segment)})
+
107 modules.d/extras.py
@@ -0,0 +1,107 @@
+
+# EXTRAS
+#-------
+class OrlyModule(BawtM2):
+ "Screams yarly at newbs"
+ privmsg_re = "orly"
+ privmsg_flags = re.I
+ _name = "OrlyModule"
+ def handle_privmsg(self, msg, ORLOG=['', 0, 0]):
+ if not msg.nick:
+ return
+ if ORLOG[0] != msg.nick.lower() or ORLOG[1] + 150 < time.time():
+ ORLOG[0] = msg.nick.lower()
+ ORLOG[1] = time.time()
+ ORLOG[2] = 0
+ self.parent.privmsg(msg.replyto, "%s: yarly" % (msg.nick))
+ return
+ else:
+ if ORLOG[2] == 0:
+ self.parent.privmsg(msg.replyto, "%s: yasrsly" % (msg.nick))
+ ORLOG[2] += 1
+ return
+ elif ORLOG[2] == 1:
+ self.parent.privmsg(msg.replyto, "%s: ya, SRSLY FFS!!" % (msg.nick))
+ ORLOG[2] += 1
+ return
+ else:
+ self.parent.privmsg(msg.replyto,
+ "%s: Yes, really, you goddamn retarded pile of baby sputum." % (msg.nick))
+ self.parent.kick(msg.replyto, msg.nick, "GTFO")
+ return
+
+class SnackModule(BawtM2):
+ ":D"
+ _name = "SnackModule"
+ privmsg_re = "botsnack"
+ privmsg_flags = re.I
+ def handle_privmsg(self, msg):
+ self.parent.privmsg(msg.replyto, ":D")
+
+class PingModule(BawtModule):
+ "Used to ping users. Incomplete"
+ matcher_re = "^!ping"
+ matcher_flags = re.I
+ _name = "PingModule"
+ def handle(self, msg):
+ args = msg.data_segment.split(" ")
+ if len(args) == 1:
+ self.parent.privmsg(msg.replyto, "%s: Who do you want to ping?" % (msg.nick))
+ return
+ else:
+ if self.ping(args[1]):
+ self.parent.privmsg(msg.replyto, "%s: I have pinged: %s" % (msg.nick, args[1]))
+ else:
+ self.parent.privmsg(msg.replyto, "%s: Sorry, I don't know how to ping %s" % (msg.nick, args[1]))
+
+ def ping(self, who):
+ # Fix this later
+ target = who.lower()
+ if target == "richo":
+ return True
+ else:
+ return False
+
+class HarrassModule(BawtM2):
+ "Yells incessantly at someone's bot"
+ _name = "HarrassModule"
+ def handle_privmsg(self, msg):
+ if not msg.nick:
+ return
+ if msg.nick.lower() == "james":
+ self.parent.privmsg(msg.replyto, "james, you're a douchenozzle")
+
+class IllustrationModule(BawtM2):
+ "Proves my point"
+ _name = "HarrassModule"
+ privmsg_re = "^!molested"
+ def handle_privmsg(self, msg):
+ self.parent.privmsg(msg.replyto, "Authored by richo (warl0ck) before you start arguing. Dynamic module loading bitchez0rz.")
+
+class BattleModule(BawtModule):
+ "richo's submission for the battlebots project"
+ matcher_re = "SOLVE"
+ _name = "BattleModule"
+ def handle(self, msg):
+ if msg.nick == "DickServ":
+ if msg.data_segment.startswith("SOLVE"):
+ equation = msg.data_segment.split(":", 1)[1]
+ self.parent.privmsg(msg.nick, "!solve " + str(eval(equation)))
+ self.parent.privmsg(msg.replyto, "!solve " + str(eval(equation)))
+
+class DebugTopicModule(BawtM2):
+ "Testing for dynamic topic handling, checking that I can hook topic changes at the module level"
+ topic_re = "."
+ privmsg_re = "^!topic"
+ _name = "DebugTopicModule"
+ def handle_privmsg(self, msg):
+ # XXX Epic kludge
+ try:
+ self.parent.privmsg(msg.replyto, "As far as I know, the topic is: %s" % (repr(self.channel.topic)))
+ except:
+ self.parent.privmsg(msg.replyto, "Your stupid topic module is broken, newb")
+ def handle_topic(self, msg):
+ self.parent.privmsg(msg.replyto, "%s set topic to: %s" % (msg.nick, msg.data_segment))
+ self.channel.topic = msg.data_segment
+#/EXTRAS
+#-------
15 modules.d/join.py
@@ -0,0 +1,15 @@
+class GreetModule(BawtM2):
+ _name = "GreetModule"
+ def handle_join(self, msg):
+ # We get notified of our own joins..
+ if msg.nick != self.parent.nick:
+ self.parent.privmsg(msg.replyto, "Hi %s" % msg.nick)
+
+class WildModule(BawtM2):
+ _name = "WildModule"
+ def handle_join(self, msg):
+ if self.parent.nick.lower() == "a":
+ self.parent.action(msg.replyto, "wild %s appears!" % msg.nick)
+ def on_load(self, *args, **kwargs):
+ self.parent.identify('A')
+
5 modules.d/mode.py
@@ -0,0 +1,5 @@
+class ModeModule(BawtM2):
+ _name = "ModeModule"
+ privmsg_re = "!mode"
+ def handle_privmsg(self, msg):
+ self.parent.privmsg(msg.replyto, self.channel.dump_modes())
7 modules.d/prod.py
@@ -0,0 +1,7 @@
+class ProdModule(BawtM2):
+ privmsg_re = "prods %(nick)s"
+ privmsg_flags = re.I
+ _name = "ProdModule"
+
+ def handle_privmsg(self, msg):
+ self.parent.action(msg.replyto, "prods %(nick)s right back!" % {'nick': msg.nick})
0  modules.local.d/.gitinclude
No changes.
457 mpd.py
@@ -0,0 +1,457 @@
+# python-mpd: Python MPD client library
+# Copyright (C) 2008-2010 J. Alexander Treuman <jat@spatialrift.net>
+#
+# python-mpd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# python-mpd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with python-mpd. If not, see <http://www.gnu.org/licenses/>.
+
+import socket
+
+
+HELLO_PREFIX = "OK MPD "
+ERROR_PREFIX = "ACK "
+SUCCESS = "OK"
+NEXT = "list_OK"
+
+
+class MPDError(Exception):
+ pass
+
+class ConnectionError(MPDError):
+ pass
+
+class ProtocolError(MPDError):
+ pass
+
+class CommandError(MPDError):
+ pass
+
+class CommandListError(MPDError):
+ pass
+
+class PendingCommandError(MPDError):
+ pass
+
+class IteratingError(MPDError):
+ pass
+
+
+class _NotConnected(object):
+ def __getattr__(self, attr):
+ return self._dummy
+
+ def _dummy(*args):
+ raise ConnectionError("Not connected")
+
+class MPDClient(object):
+ def __init__(self):
+ self.iterate = False
+ self._reset()
+ self._commands = {
+ # Status Commands
+ "clearerror": self._fetch_nothing,
+ "currentsong": self._fetch_object,
+ "idle": self._fetch_list,
+ "noidle": None,
+ "status": self._fetch_object,
+ "stats": self._fetch_object,
+ # Playback Option Commands
+ "consume": self._fetch_nothing,
+ "crossfade": self._fetch_nothing,
+ "mixrampdb": self._fetch_nothing,
+ "mixrampdelay": self._fetch_nothing,
+ "random": self._fetch_nothing,
+ "repeat": self._fetch_nothing,
+ "setvol": self._fetch_nothing,
+ "single": self._fetch_nothing,
+ "replay_gain_mode": self._fetch_nothing,
+ "replay_gain_status": self._fetch_item,
+ "volume": self._fetch_nothing,
+ # Playback Control Commands
+ "next": self._fetch_nothing,
+ "pause": self._fetch_nothing,
+ "play": self._fetch_nothing,
+ "playid": self._fetch_nothing,
+ "previous": self._fetch_nothing,
+ "seek": self._fetch_nothing,
+ "seekid": self._fetch_nothing,
+ "stop": self._fetch_nothing,
+ # Playlist Commands
+ "add": self._fetch_nothing,
+ "addid": self._fetch_item,
+ "clear": self._fetch_nothing,
+ "delete": self._fetch_nothing,
+ "deleteid": self._fetch_nothing,
+ "move": self._fetch_nothing,
+ "moveid": self._fetch_nothing,
+ "playlist": self._fetch_playlist,
+ "playlistfind": self._fetch_songs,
+ "playlistid": self._fetch_songs,
+ "playlistinfo": self._fetch_songs,
+ "playlistsearch": self._fetch_songs,
+ "plchanges": self._fetch_songs,
+ "plchangesposid": self._fetch_changes,
+ "shuffle": self._fetch_nothing,
+ "swap": self._fetch_nothing,
+ "swapid": self._fetch_nothing,
+ # Stored Playlist Commands
+ "listplaylist": self._fetch_list,
+ "listplaylistinfo": self._fetch_songs,
+ "listplaylists": self._fetch_playlists,
+ "load": self._fetch_nothing,
+ "playlistadd": self._fetch_nothing,
+ "playlistclear": self._fetch_nothing,
+ "playlistdelete": self._fetch_nothing,
+ "playlistmove": self._fetch_nothing,
+ "rename": self._fetch_nothing,
+ "rm": self._fetch_nothing,
+ "save": self._fetch_nothing,
+ # Database Commands
+ "count": self._fetch_object,
+ "find": self._fetch_songs,
+ "findadd": self._fetch_nothing,
+ "list": self._fetch_list,
+ "listall": self._fetch_database,
+ "listallinfo": self._fetch_database,
+ "lsinfo": self._fetch_database,
+ "search": self._fetch_songs,
+ "update": self._fetch_item,
+ "rescan": self._fetch_item,
+ # Sticker Commands
+ "sticker get": self._fetch_item,
+ "sticker set": self._fetch_nothing,
+ "sticker delete": self._fetch_nothing,
+ "sticker list": self._fetch_list,
+ "sticker find": self._fetch_songs,
+ # Connection Commands
+ "close": None,
+ "kill": None,
+ "password": self._fetch_nothing,
+ "ping": self._fetch_nothing,
+ # Audio Output Commands
+ "disableoutput": self._fetch_nothing,
+ "enableoutput": self._fetch_nothing,
+ "outputs": self._fetch_outputs,
+ # Reflection Commands
+ "commands": self._fetch_list,
+ "notcommands": self._fetch_list,
+ "tagtypes": self._fetch_list,
+ "urlhandlers": self._fetch_list,
+ "decoders": self._fetch_plugins,
+ }
+
+ def __getattr__(self, attr):
+ if attr.startswith("send_"):
+ command = attr.replace("send_", "", 1)
+ wrapper = self._send
+ elif attr.startswith("fetch_"):
+ command = attr.replace("fetch_", "", 1)
+ wrapper = self._fetch
+ else:
+ command = attr
+ wrapper = self._execute
+ if command not in self._commands:
+ command = command.replace("_", " ")
+ if command not in self._commands:
+ raise AttributeError("'%s' object has no attribute '%s'" %
+ (self.__class__.__name__, attr))
+ return lambda *args: wrapper(command, args)
+
+ def _send(self, command, args):
+ if self._command_list is not None:
+ raise CommandListError("Cannot use send_%s in a command list" %
+ command.replace(" ", "_"))
+ self._write_command(command, args)
+ retval = self._commands[command]
+ if retval is not None:
+ self._pending.append(command)
+
+ def _fetch(self, command, args=None):
+ if self._command_list is not None:
+ raise CommandListError("Cannot use fetch_%s in a command list" %
+ command.replace(" ", "_"))
+ if self._iterating:
+ raise IteratingError("Cannot use fetch_%s while iterating" %
+ command.replace(" ", "_"))
+ if not self._pending:
+ raise PendingCommandError("No pending commands to fetch")
+ if self._pending[0] != command:
+ raise PendingCommandError("'%s' is not the currently "
+ "pending command" % command)
+ del self._pending[0]
+ retval = self._commands[command]
+ if callable(retval):
+ return retval()
+ return retval
+
+ def _execute(self, command, args):
+ if self._iterating:
+ raise IteratingError("Cannot execute '%s' while iterating" %
+ command)
+ if self._pending:
+ raise PendingCommandError("Cannot execute '%s' with "
+ "pending commands" % command)
+ retval = self._commands[command]
+ if self._command_list is not None:
+ if not callable(retval):
+ raise CommandListError("'%s' not allowed in command list" %
+ command)
+ self._write_command(command, args)
+ self._command_list.append(retval)
+ else:
+ self._write_command(command, args)
+ if callable(retval):
+ return retval()
+ return retval
+
+ def _write_line(self, line):
+ self._wfile.write("%s\n" % line)
+ self._wfile.flush()
+
+ def _write_command(self, command, args=[]):
+ parts = [command]
+ for arg in args:
+ parts.append('"%s"' % escape(str(arg)))
+ self._write_line(" ".join(parts))
+
+ def _read_line(self):
+ line = self._rfile.readline()
+ if not line.endswith("\n"):
+ raise ConnectionError("Connection lost while reading line")
+ line = line.rstrip("\n")
+ if line.startswith(ERROR_PREFIX):
+ error = line[len(ERROR_PREFIX):].strip()
+ raise CommandError(error)
+ if self._command_list is not None:
+ if line == NEXT:
+ return
+ if line == SUCCESS:
+ raise ProtocolError("Got unexpected '%s'" % SUCCESS)
+ elif line == SUCCESS:
+ return
+ return line
+
+ def _read_pair(self, separator):
+ line = self._read_line()
+ if line is None:
+ return
+ pair = line.split(separator, 1)
+ if len(pair) < 2:
+ raise ProtocolError("Could not parse pair: '%s'" % line)
+ return pair
+
+ def _read_pairs(self, separator=": "):
+ pair = self._read_pair(separator)
+ while pair:
+ yield pair
+ pair = self._read_pair(separator)
+
+ def _read_list(self):
+ seen = None
+ for key, value in self._read_pairs():
+ if key != seen:
+ if seen is not None:
+ raise ProtocolError("Expected key '%s', got '%s'" %
+ (seen, key))
+ seen = key
+ yield value
+
+ def _read_playlist(self):
+ for key, value in self._read_pairs(":"):
+ yield value
+
+ def _read_objects(self, delimiters=[]):
+ obj = {}
+ for key, value in self._read_pairs():
+ key = key.lower()
+ if obj:
+ if key in delimiters:
+ yield obj
+ obj = {}
+ elif key in obj:
+ if not isinstance(obj[key], list):
+ obj[key] = [obj[key], value]
+ else:
+ obj[key].append(value)
+ continue
+ obj[key] = value
+ if obj:
+ yield obj
+
+ def _read_command_list(self):
+ try:
+ for retval in self._command_list:
+ yield retval()
+ finally:
+ self._command_list = None
+ self._fetch_nothing()
+
+ def _iterator_wrapper(self, iterator):
+ try:
+ for item in iterator:
+ yield item
+ finally:
+ self._iterating = False
+
+ def _wrap_iterator(self, iterator):
+ if not self.iterate:
+ return list(iterator)
+ self._iterating = True
+ return self._iterator_wrapper(iterator)
+
+ def _fetch_nothing(self):
+ line = self._read_line()
+ if line is not None:
+ raise ProtocolError("Got unexpected return value: '%s'" % line)
+
+ def _fetch_item(self):
+ pairs = list(self._read_pairs())
+ if len(pairs) != 1:
+ return
+ return pairs[0][1]
+
+ def _fetch_list(self):
+ return self._wrap_iterator(self._read_list())
+
+ def _fetch_playlist(self):
+ return self._wrap_iterator(self._read_playlist())
+
+ def _fetch_object(self):
+ objs = list(self._read_objects())
+ if not objs:
+ return {}
+ return objs[0]
+
+ def _fetch_objects(self, delimiters):
+ return self._wrap_iterator(self._read_objects(delimiters))
+
+ def _fetch_changes(self):
+ return self._fetch_objects(["cpos"])
+
+ def _fetch_songs(self):
+ return self._fetch_objects(["file"])
+
+ def _fetch_playlists(self):
+ return self._fetch_objects(["playlist"])
+
+ def _fetch_database(self):
+ return self._fetch_objects(["file", "directory", "playlist"])
+
+ def _fetch_outputs(self):
+ return self._fetch_objects(["outputid"])
+
+ def _fetch_plugins(self):
+ return self._fetch_objects(["plugin"])
+
+ def _fetch_command_list(self):
+ return self._wrap_iterator(self._read_command_list())
+
+ def _hello(self):
+ line = self._rfile.readline()
+ if not line.endswith("\n"):
+ raise ConnectionError("Connection lost while reading MPD hello")
+ line = line.rstrip("\n")
+ if not line.startswith(HELLO_PREFIX):
+ raise ProtocolError("Got invalid MPD hello: '%s'" % line)
+ self.mpd_version = line[len(HELLO_PREFIX):].strip()
+
+ def _reset(self):
+ self.mpd_version = None
+ self._iterating = False
+ self._pending = []
+ self._command_list = None
+ self._sock = None
+ self._rfile = _NotConnected()
+ self._wfile = _NotConnected()
+
+ def _connect_unix(self, path):
+ if not hasattr(socket, "AF_UNIX"):
+ raise ConnectionError("Unix domain sockets not supported "
+ "on this platform")
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.connect(path)
+ return sock
+
+ def _connect_tcp(self, host, port):
+ try:
+ flags = socket.AI_ADDRCONFIG
+ except AttributeError:
+ flags = 0
+ err = None
+ for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+ socket.SOCK_STREAM, socket.IPPROTO_TCP,
+ flags):
+ af, socktype, proto, canonname, sa = res
+ sock = None
+ try:
+ sock = socket.socket(af, socktype, proto)
+ sock.connect(sa)
+ return sock
+ except socket.error, err:
+ if sock is not None:
+ sock.close()
+ if err is not None:
+ raise err
+ else:
+ raise ConnectionError("getaddrinfo returns an empty list")
+
+ def connect(self, host, port):
+ if self._sock is not None:
+ raise ConnectionError("Already connected")
+ if host.startswith("/"):
+ self._sock = self._connect_unix(host)
+ else:
+ self._sock = self._connect_tcp(host, port)
+ self._rfile = self._sock.makefile("rb")
+ self._wfile = self._sock.makefile("wb")
+ try:
+ self._hello()
+ except:
+ self.disconnect()
+ raise
+
+ def disconnect(self):
+ self._rfile.close()
+ self._wfile.close()
+ self._sock.close()
+ self._reset()
+
+ def fileno(self):
+ if self._sock is None:
+ raise ConnectionError("Not connected")
+ return self._sock.fileno()
+
+ def command_list_ok_begin(self):
+ if self._command_list is not None:
+ raise CommandListError("Already in command list")
+ if self._iterating:
+ raise IteratingError("Cannot begin command list while iterating")
+ if self._pending:
+ raise PendingCommandError("Cannot begin command list "
+ "with pending commands")
+ self._write_command("command_list_ok_begin")
+ self._command_list = []
+
+ def command_list_end(self):
+ if self._command_list is None:
+ raise CommandListError("Not in command list")
+ if self._iterating:
+ raise IteratingError("Already iterating over a command list")
+ self._write_command("command_list_end")
+ return self._fetch_command_list()
+
+
+def escape(text):
+ return text.replace("\\", "\\\\").replace('"', '\\"')
+
+
+# vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79:
56 ourgit.py
@@ -0,0 +1,56 @@
+import os
+import subprocess as sp
+import logging
+
+for p in ('/usr/bin/git', '/usr/local/bin/git'):
+ if os.path.exists(p):
+ git_binary = p
+
+
+def oneline(cmd):
+ try:
+ p = sp.Popen(cmd.split(" "), stdout=sp.PIPE, close_fds=True, executable=git_binary)
+ p.wait()
+ return p.stdout.readline()
+ except OSError:
+ return "Couldn't find binary (probably)"
+def sp_stdout(cmd):
+ try:
+ p = sp.Popen(cmd.split(" "), stdout=sp.PIPE, close_fds=True)
+ yield p.stdout
+ except OSError:
+ raise StopIteration
+
+def current_branch():
+ with sp_stdout('git branch') as branchout:
+ while True:
+ i = stdout.readline()
+ if i.startswith("*"):
+ return i[1:].strip()
+ return False
+
+def update_git():
+ data = oneline("git pull")
+ logging.info("Updated source to %s" % data)
+ return data
+ # TODO sanity check
+
+
+def checkout_git(branch):
+ return oneline('git checkout %s' % branch)
+
+def update_git_head():
+ return oneline('git reset --hard HEAD')
+
+def version():
+ return oneline('git show --shortstat HEAD')[:16]
+
+def log():
+ return ""
+
+def clean_git():
+ pass
+
+def git(cmd):
+ return oneline("git %s" % cmd)
+
10 pyBawt.conf.sample
@@ -0,0 +1,10 @@
+host = irc.psych0tik.net
+ssl = True
+nick = YourBotNick
+port = 6697
+auth_host = YourHost
+auth_hash = [md5sumhere]
+channels = #testcannel
+nickserv_nick = nickserv
+nickserv_pass = nickservpassword
+
91 pyBawt.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+##
+# pyBawt is (Will be) released under the WTFPL
+# You are hereby licensed to do WHATEVER THE FUCK YOU WANT with it's source
+# code
+##
+
+# Rich Healey '08
+
+import ircSocket
+import time
+import sys
+import os
+import random
+import config
+import bModules
+import logging
+import traceback
+
+logging.info("pyBawt started")
+
+# Have a crack at sweet argparsing
+
+# TODO - hax involving stdout for debugging
+
+try:
+ import argparse
+ parser = argparse.ArgumentParser(description='IRC bot written in python')
+ parser.add_argument("-d", "--debug", dest='debug', action='store_true',
+ default=False,
+ help='include debug data, also crash violently on error')
+ args = parser.parse_args()
+
+ debug = args.debug
+except ImportError:
+# no argparse, probably py2.6
+ debug = False
+ # TODO - something clever here to make args still work
+
+def restart_stub():
+ net.quit("Going down for restart")
+ os.execv(sys.executable, [sys.executable] + sys.argv)
+
+net = ircSocket.chatnet(config.host, port=config.port, use_ssl=config.ssl)
+# Ugly hax, port to argparse if we see any more nicks
+nick = config.nick
+net.identify(nick)
+net.auth_self(config.nickserv_nick, config.nickserv_pass)
+net._debug = True
+
+# Before we hit mainloop, write pidfile
+try:
+ fh = open('/tmp/pyBawt.pid', 'w')
+ fh.write(str(os.getpid()))
+ fh.close()
+except IOError:
+ logging.fatal("Couldn't write pidfile")
+
+try:
+ for i in config.channels:
+ net.join(i)
+ while True:
+ try:
+ net.recv_wait()
+ except ircSocket.FlushQueue:
+ pass
+ net.dump_queue()
+except KeyboardInterrupt:
+ logging.error("Shutting down due to user intervention")
+ net.quit("Killed from terminal")
+except bModules.Restart:
+ # TODO Include the user who did this
+ logging.error("Restarting due to user intervention")
+ restart_stub()
+except ircSocket.IrcDisconnected:
+ if ircSocket.should_reconnect():
+ restart_stub()
+except ircSocket.IrcTerminated:
+ # Catch but don't handle, die gracefully
+ pass
+except Exception:
+ # TODO - Checkout from stable git branch
+ if debug: # Debug hook? Either way it's stupid.
+ logging.error("Shutting down and bailing out")
+ raise
+ else:
+ logging.error("Exception caught, restarting")
+ traceback.print_exception(*sys.exc_info(), file=logging.Writer(logging.error))
+ restart_stub()
+
16 pyBawt.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+cwd=`dirname $0`
+cd $cwd
+
+# Checkout master, shouldn't ever be broken
+while echo; do
+ echo "Starting fetch"
+
+ git fetch
+ git reset --hard origin/master
+ echo "Starting pyBawt"
+ ./pyBawt.py
+ echo "pyBawt died, sleeping"
+ sleep 5
+done
37 regen_modules.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+MODULE_DIRECTORY = 'modules.d'
+LOCAL_MODULES = 'modules.local.d'
+CONFIG = 'module_config.py'
+outfile = 'bModules.py'
+
+import os
+
+def rebuild_bModules():
+ out = open(outfile, 'w')
+ # Module directory
+ modules = [os.path.join(MODULE_DIRECTORY, i) for i in os.listdir(MODULE_DIRECTORY)]
+ if os.path.exists(LOCAL_MODULES):
+ modules.append(os.path.join(LOCAL_MODULES, i) for i in os.listdir(LOCAL_MODULES)
+ modules.sort()
+ modules.append(CONFIG)
+ for i in modules:
+ if not i.endswith(".py"):
+ continue
+ if os.path.isdir(i):
+ continue
+ # We're happy with the file.
+ for line in open(i, 'r').readlines():
+ out.write(line)
+ out.close()
+ return outfile
+
+def save_modules(module_map):
+ pass
+# XXX TODO construct this function to save to module_config.py
+
+if __name__ == '__main__':
+ # Test rig
+ rebuild_bModules()
+
+
+
7 start.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+codedir=`dirname $0`
+
+cd $codedir
+
+screen -dmS pyBawt python2.7 $codedir/pyBawt.py
10 test/radio.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+
+import sys
+sys.path.append("../")
+import mpd
+
+c = mpd.MPDClient()
+c.connect("domino.psych0tik.net", 6600)
+print c.currentsong()
+
27 test/unit/helpers.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+import os
+import sys
+
+sys.path.append(os.getcwd())
+
+def ASSERT(cond, errormsg):
+ if cond:
+ print errormsg
+ print "\t\t\tPASS"
+ else:
+ # COLORS
+ print >>sys.stderr, errormsg
+ print "\t\t\tFAIL"
+ # TODO Add this to something and continue
+ # Maybe even raise a token exceptiont o catch so I can
+ # examine the stack
+ exit()
+def START(test):
+ # epic kludge
+ os.current_test = test
+ print("== %s ==" % test)
+def END():
+ try:
+ print("== %s ==" % os.current_test)
+ except AttributeError:
+ ASSERT(False, "No test in progress")
24 test/unit/test_lib.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+from helpers import *
+
+import lib
+
+START("LowerList")
+# LowerList should behave like a list
+lowerlist = lib.LowerList()
+lowerlist.append("UpPeR CaSe")
+
+# LowerList should be case insensitive
+ASSERT("upper case" in lowerlist, "LowerList should be case insensitive")
+END()
+
+START("Mapping")
+mapping = lib.Mapping()
+mapping["rawr"] = "Thing"
+
+ASSERT("rawr" in mapping, "Regular assignment works")
+
+mapping["doesntexist"].append("rawr")
+
+ASSERT("rawr" in mapping["doesntexist"], "Creating keys by referencing them")
+END()
3  update_radio.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+kill -ALRM `cat /tmp/pyBawt.pid`
25 webutils.py
@@ -0,0 +1,25 @@
+import time
+import os
+
+## CONFIG
+
+# Trailing slash is important.
+webroot = "http://natalya.psych0tik.net/~pyBawt/"
+docroot = "/home/pyBawt/public_html"
+
+class PublicationError(Exception):
+ pass
+
+def publish(content, prefix="", suffix="txt"):
+ name = ""
+ if prefix:
+ name += prefix + "_"
+ name += "%i.%s" % (time.time(), suffix)
+ try:
+ fh = open(os.path.join(docroot, name), 'w')
+ fh.write(content)
+ fh.close()
+ return webroot + name
+
+ except IOError:
+ raise PublicationError
Please sign in to comment.
Something went wrong with that request. Please try again.