summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--autoexec.py7
-rw-r--r--bouncer.py90
-rw-r--r--cannon.py8
-rw-r--r--irc.py1298
-rw-r--r--ircapp.conf64
-rwxr-xr-xircapp.py125
-rw-r--r--logger.py55
-rw-r--r--modjson.py96
-rwxr-xr-xstartirc.py74
-rw-r--r--wallet.py26
10 files changed, 1264 insertions, 579 deletions
diff --git a/autoexec.py b/autoexec.py
index 501ec2c..0b30894 100644
--- a/autoexec.py
+++ b/autoexec.py
@@ -19,15 +19,14 @@ class Autoexec(object):
self._rejoinchannels = {}
# Saved channels for when a connection is lost
- def onAddonAdd(self, context, label, onconnect=None, onregister=None, autojoin=None, usermodes=None, nsautojoin=None, nsmatch=None, wallet=None, opername=None, opermodes=None, snomasks=None, operexec=None, operjoin=None, autorejoin=True):
+ def onAddonAdd(self, context, label, onconnect=[], onregister=[], autojoin=[], usermodes=None, nsautojoin=[], nsmatch=None, wallet=None, opername=None, opermodes=None, snomasks=None, operexec=None, operjoin=[], autorejoin=True):
labels = [v.label for v in self.networks.values()]
if label in labels:
raise BaseException, "Label already exists"
if context in self.networks.keys():
raise BaseException, "Network already exists"
self.networks[context] = irc.Config(
- self, label=label, onconnect=onconnect, onregister=onregister, autojoin=irc.ChanList(
- autojoin, context=context),
+ self, label=label, onconnect=list(onconnect), onregister=list(onregister), autojoin=irc.ChanList(autojoin, context=context),
usermodes=usermodes, nsautojoin=irc.ChanList(nsautojoin, context=context), nsmatch=nsmatch, wallet=wallet,
opername=opername, opermodes=opermodes, snomasks=snomasks, operexec=operexec, operjoin=irc.ChanList(operjoin, context=context), autorejoin=autorejoin)
self._rejoinchannels[context] = None
@@ -35,7 +34,7 @@ class Autoexec(object):
def onDisconnect(self, context, expected):
conf = self.networks[context]
- if conf.autorejoin and not expected and context.identity:
+ if conf.autorejoin and not expected and context.identity and context.identity.channels:
self._rejoinchannels[context] = irc.ChanList(
context.identity.channels, context=context) # Store a *copy* of the list of channels
diff --git a/bouncer.py b/bouncer.py
index 45d4b6b..8db9dd8 100644
--- a/bouncer.py
+++ b/bouncer.py
@@ -14,6 +14,10 @@ from threading import Thread
from threading import RLock as Lock
import Queue
import chardet
+import modjson
+
+dec = modjson.ModJSONDecoder()
+enc = modjson.ModJSONEncoder(indent=3)
# TODO: Rewrite this *entire* module and make more efficient.
@@ -26,21 +30,14 @@ _listnumerics = dict(b=(367, 368, "channel ban list"),
def BouncerReload(BNC):
+ networks, configs = zip(*BNC.conf.items())
+ json = enc.encode([BNC, configs])
if BNC.isAlive():
BNC.stop()
- if BNC.__version__ == "1.3":
- newBNC = Bouncer(
- addr=BNC.addr, port=BNC.port, secure=BNC.ssl, ipv6=BNC.ipv6,
- certfile=BNC.certfile, keyfile=BNC.keyfile, timeout=BNC.timeout, autoaway=BNC.autoaway)
- for label, (context, passwd, hashtype) in BNC.servers.items():
- context.rmAddon(BNC)
- context.addAddon(
- newBNC, label=label, passwd=passwd, hashtype=hashtype)
- else:
- newBNC = Bouncer(**BNC.__options__)
- for context, conf in BNC.conf.items():
- context.rmAddon(BNC)
- context.addAddon(newBNC, **conf.__dict__)
+ newBNC, newconfs = dec.decode(json)
+ for network, newconf in zip(networks, newconfs):
+ network.rmAddon(BNC)
+ network.addAddon(**newconf)
return newBNC
@@ -56,7 +53,7 @@ class Bouncer (Thread):
self.conf = {}
self.passwd = {}
self.socket = None
- self.ssl = ssl
+ self.secure = secure
self.ipv6 = ipv6
self.certfile = certfile
self.keyfile = keyfile
@@ -119,7 +116,7 @@ class Bouncer (Thread):
Thread.__init__(self)
self.daemon = True
- def onAddonAdd(self, context, label, passwd=None, hashtype="sha512", ignore=None, autoaway=None, translations=None, hidden=None):
+ def onAddonAdd(self, context, label, passwd=None, hashtype="sha512", ignore=None, autoaway=None, translations=[], hidden=[]):
for (context2, conf2) in self.conf.items():
if context == context2:
raise ValueError, "Context already exists in config."
@@ -132,9 +129,8 @@ class Bouncer (Thread):
break
print "Passwords do not match!"
passwd = hashlib.new(hashtype, passwd).hexdigest()
- conf = irc.Config(
- self, label=label, passwd=passwd, hashtype=hashtype, ignore=ignore, autoaway=autoaway,
- translations={} if translations == None else translations, hidden=irc.ChanList(hidden, context=context))
+ conf = irc.Config(self, label=label, passwd=passwd, hashtype=hashtype, ignore=ignore, autoaway=autoaway, translations=[
+ (key if type(key) == irc.Channel else context[key], value) for key, value in translations], hidden=irc.ChanList(hidden, context=context))
self.conf[context] = conf
self._whoexpected[context] = []
if self.debug:
@@ -262,11 +258,11 @@ class Bouncer (Thread):
def onWhoEntry(self, context, origin, channel, user, channame, username, host, serv, nick, flags, hops, realname):
# Called when a WHO list is received.
- conf = self.conf[context]
- if len(self._whoexpected[context]) and self._whoexpected[context][0] in self.clients:
+ if len(self._whoexpected[context]):
client = self._whoexpected[context][0]
- client.send(origin=origin, cmd=352, target=context.identity, params=u"{channame} {username} {host} {serv} {nick} {flags}".format(
- **vars()), extinfo=u"{hops} {realname}".format(**vars()))
+ if client in self.clients:
+ client.send(origin=origin, cmd=352, target=context.identity, params=u"{channame} {username} {host} {serv} {nick} {flags}".format(
+ **vars()), extinfo=u"{hops} {realname}".format(**vars()))
# client.send(":%s 352 %s %s %s %s %s %s %s :%s %s\n"%(origin, context.identity.nick, channame, username, host, serv, nick, flags, hops, realname))
def onWhoEnd(self, context, origin, param, endmsg):
@@ -301,15 +297,10 @@ class Bouncer (Thread):
def onListEntry(self, context, origin, channel, population, extinfo):
# Called when a WHO list is received.
- conf = self.conf[context]
- if channel in conf.translations.keys():
- channame = conf.translations[channel]
- else:
- channame = channel.name
if len(self._listexpected[context]) and self._listexpected[context][0] in self.clients:
client = self._listexpected[context][0]
client.send(origin=origin, cmd=322, target=context.identity,
- params=u"{channame} {population}".format(**vars()), extinfo=extinfo)
+ params=u"{channel.name} {population}".format(**vars()), extinfo=extinfo)
# client.send(":%s 322 %s %s %d :%s\n"%(origin, context.identity.nick, channame, population, extinfo))
def onListEnd(self, context, origin, endmsg):
@@ -424,7 +415,7 @@ class Bouncer (Thread):
self.broadcast(context, origin=user, cmd="JOIN", target=channel, clients=[
client for client in self.clients if channel not in client.hidden])
- def onUnhandled(self, context, line, origin, cmd, target, params, extinfo, targetprefix):
+ def onOther(self, context, line, origin, cmd, target, params, extinfo, targetprefix):
conf = self.conf[context]
self.broadcast(
context, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo,
@@ -482,26 +473,26 @@ class BouncerConnection (Thread):
if type(target) == irc.Channel:
if targetprefix == None:
targetprefix = ""
- if target in self.translations.keys():
- target = targetprefix + self.translations[target]
- else:
- target = targetprefix + target.name
+ # if target in self.translations.keys():
+ # target=targetprefix+self.translations[target]
+ # else:
+ # target=targetprefix+target.name
+ target = targetprefix + target.name
elif type(target) == irc.User:
target = target.nick
if type(cmd) == int:
cmd = "%03d" % cmd
- translated = []
- if params:
- for param in params.split(" "):
- chantypes = self.context.supports.get(
- "CHANTYPES", irc._defaultchantypes)
- if re.match(irc._chanmatch % re.escape(chantypes), param) and self.context[param] in self.translations.keys():
- translated.append(self.translations[self.context[param]])
- else:
- translated.append(param)
- params = " ".join(translated)
+ # translated=[]
+ # if params:
+ # for param in params.split(" "):
+ #chantypes=self.context.supports.get("CHANTYPES", irc._defaultchantypes)
+ # if re.match(irc._chanmatch % re.escape(chantypes), param) and self.context[param] in self.translations.keys():
+ # translated.append(self.translations[self.context[param]])
+ # else:
+ # translated.append(param)
+ #params=" ".join(translated)
if params:
line = u"{cmd} {target} {params}".format(**vars())
@@ -841,7 +832,7 @@ class BouncerConnection (Thread):
self.context.logwrite(
"*** [BouncerConnection] Incoming connection from %s to %s denied: Invalid password." % (self.host, self.context))
self.bouncer.broadcast(
- self.context, origin=self.bouncer.servname, cmd="NOTICE", target=client.context.identity,
+ self.context, origin=self.bouncer.servname, cmd="NOTICE", target=self.context.identity,
extinfo="Incoming connection from %s to %s denied: Invalid password." % (self.host, self.context))
# for client in self.bouncer.clients:
# if client.context!=self.context:
@@ -899,7 +890,8 @@ class BouncerConnection (Thread):
else:
chantypes = self.context.supports.get(
"CHANTYPES", irc._defaultchantypes)
- if cmd.upper() not in ("SETTRANSLATE", "RMTRANSLATE"):
+ # Disable translating for now.
+ if False and cmd.upper() not in ("SETTRANSLATE", "RMTRANSLATE"):
translated = []
for targ in target.split(","):
translatefound = False
@@ -1104,7 +1096,7 @@ class BouncerConnection (Thread):
def cmdSHOW(self, line, target, params, extinfo):
chantypes = self.context.supports.get(
"CHANTYPES", irc._defaultchantypes)
- with self.lock:
+ with self.context.lock, self.lock:
for channame in target.split():
if re.match(irc._chanmatch % re.escape(chantypes), channame):
channel = self.context[channame]
@@ -1128,7 +1120,7 @@ class BouncerConnection (Thread):
def cmdHIDE(self, line, target, params, extinfo):
chantypes = self.context.supports.get(
"CHANTYPES", irc._defaultchantypes)
- with self.lock:
+ with self.context.lock, self.lock:
for channame in target.split():
if re.match(irc._chanmatch % re.escape(chantypes), channame):
channel = self.context[channame]
@@ -1153,7 +1145,7 @@ class BouncerConnection (Thread):
def cmdSETTRANSLATE(self, line, target, params, extinfo):
chantypes = self.context.supports.get(
"CHANTYPES", irc._defaultchantypes)
- with self.lock:
+ with self.context.lock, self.lock:
if re.match(irc._chanmatch % re.escape(chantypes), target) and re.match(irc._chanmatch % re.escape(chantypes), target):
channel = self.context[target]
if self.context.supports.get("CASEMAPPING", "rfc1459") == "ascii":
@@ -1177,7 +1169,7 @@ class BouncerConnection (Thread):
def cmdRMTRANSLATE(self, line, target, params, extinfo):
chantypes = self.context.supports.get(
"CHANTYPES", irc._defaultchantypes)
- with self.lock:
+ with self.context.lock, self.lock:
if re.match(irc._chanmatch % re.escape(chantypes), target):
channel = self.context[target]
if channel not in self.translations.keys():
diff --git a/cannon.py b/cannon.py
index c9acea0..0911245 100644
--- a/cannon.py
+++ b/cannon.py
@@ -9,12 +9,12 @@ class Cannon(object):
def __init__(self):
self.firecount = {}
- def onChanMsg(self, IRC, user, channel, targetprefix, msg):
+ def onChanMsg(self, context, user, channel, targetprefix, msg):
matches = re.findall("^!fire\\s+(.*)$", msg)
if matches:
nickname = matches[0]
if any([nickname.lower() == usr.nick.lower() for usr in channel.users]):
- vic = IRC.user(nickname)
+ vic = context.user(nickname)
if vic in self.firecount.keys():
count = self.firecount[vic] + 1
else:
@@ -37,5 +37,5 @@ class Cannon(object):
"%s: I cannot fire %s out of a cannon, as he or she is not here." %
(user.nick, nickname))
- def onSendChanMsg(self, IRC, origin, channel, targetprefix, msg):
- self.onChanMsg(IRC, IRC.identity, channel, targetprefix, msg)
+ def onSendChanMsg(self, context, origin, channel, targetprefix, msg):
+ self.onChanMsg(context, context.identity, channel, targetprefix, msg)
diff --git a/irc.py b/irc.py
index c641cdb..a69c7f9 100644
--- a/irc.py
+++ b/irc.py
@@ -19,6 +19,9 @@ import inspect
import warnings
import random
+__all__ = ["Connection", "Channel", "ChanList",
+ "User", "UserList", "Config", "timestamp"]
+
def autodecode(s):
try:
@@ -36,11 +39,23 @@ class AddonWarning(Warning):
pass
+class ConnectionWarning(Warning):
+ pass
+
+
+class AddonError(BaseException):
+ pass
+
+
class InvalidName(BaseException):
+
+ """Raised when an invalid string is passed of as a nickname."""
pass
class InvalidPrefix(BaseException):
+
+ """Raised when an string with an invalid prefix is passed of as a channel name."""
pass
@@ -49,6 +64,8 @@ class InvalidCharacter(BaseException):
class ConnectionTimedOut(BaseException):
+
+ """Raised when the connection times out during a blocked Channel.join() or Channel.part() call."""
pass
@@ -57,18 +74,26 @@ class ConnectionClosed(BaseException):
class RequestTimedOut(BaseException):
+
+ """Raised when a timeout is reached during a blocked Channel.join() or Channel.part() call."""
pass
class NotConnected(BaseException):
+
+ """Raised when attempting to send data to a server when not connected."""
pass
class BannedFromChannel(BaseException):
+
+ """Raised in a blocked Channel.join() call when server returns a 474 reply (Banned from Channel)."""
pass
class RedirectedJoin(BaseException):
+
+ """Raised in a blocked Channel.join() call when server returns a 470 reply (Channel redirect)."""
pass
@@ -157,6 +182,7 @@ _prefixmatch = r"\((.*)\)(.*)"
_defaultchanmodes = u"b,k,l,imnpst".split(",")
_defaultprefix = ("ov", "@+")
_defaultchantypes = "&#+!"
+_capmodifiers = "~=-"
_privmodeeventnames = dict(q=("Owner", "Deowner"), a=("Admin", "Deadmin"), o=(
"Op", "Deop"), h=("Halfop", "Dehalfop"), v=("Voice", "Devoice"))
@@ -177,18 +203,50 @@ def timestamp():
class Connection(object):
+ __doc__ = "Manages a connection to an IRC network. Includes support for addons."
__name__ = "pyIRC"
- __version__ = "2.0"
+ __version__ = "2.1"
__author__ = "Brian Sherson"
__date__ = "February 21, 2014"
- def __init__(self, server, nick="ircbot", username="python", realname="Python IRC Library", passwd=None, port=None, ipvers=(socket.AF_INET6, socket.AF_INET), secure=False, autoreconnect=True, timeout=300, retrysleep=5, maxretries=15, protoctl=("UHNAMES", "NAMESX"), quietpingpong=True, pinginterval=60, addons=None, autostart=False):
+ def __init__(
+ self, server, port=None, ipvers=(socket.AF_INET6, socket.AF_INET), secure=False, passwd=None,
+ nick="ircbot", username="python", realname="Python IRC Library",
+ requestcaps=[], starttls=False, protoctl=[],
+ autoreconnect=True, retrysleep=5, maxretries=15,
+ timeout=300, quietpingpong=True, pinginterval=60, addons=[], autostart=False):
+ """__init__(server[, ...])
+
+ Constructor for the Connection class.
+
+ Arguments:
+
+ server: Server name. Can provide host name or IP address.
+ port: Port to use, or automatically selected if port=None.
+ ipvers: Tuple of IP protocols to try.
+ secure: Use SSL.
+ passwd: Password to be sent with PASS during registration process, or None.
+ nick: A nickname, or list of nicknames.
+ username: Username that is requested with USER command during registration process.
+ realname: Desired GECOS.
+ requestcaps: List of capabilities to request on connect.
+ protoctl: Protocols to request when support is detected in 005 response.
+ autoreconnect: Reconnect automatically when disconnected unexpectedly.
+ retrysleep: Number of seconds to wait between connection attempts.
+ maxretries: Number of connection attempts before giving up, or -1 to try indefinitely.
+ timeout: Read timeout.
+ quietpingpong: Suppress logging and events on PING and PONG events.
+ pinginterval: Amount of time of not receiving data from a server aftr which a ping request is to be sent.
+ addons: List of addons that should be initialized with this instance. Items of this list are either instances of addons or dict objects
+ containing keyword arguments to be used to configure addons.
+ autostart: Automatically start connection to IRC server upon initialization.
+ """
if port is None or (type(port) == int and 0 < port < 65536):
self.port = port
else:
raise ValueError, "Invalid value for 'port'"
- if re.match(_nickmatch, nick) if (type(nick) in (str, unicode)) else all([re.match(_nickmatch, n) for n in nick]) if (type(nick) in (list, tuple)) else False:
+ if re.match(_nickmatch, nick) if isinstance(nick, (str, unicode)) else all([re.match(_nickmatch, n) for n in nick]) if isinstance(nick, (list, tuple)) else False:
self.nick = nick
else:
raise ValueError, "Invalid value for 'nick'"
@@ -219,17 +277,17 @@ class Connection(object):
else:
raise ValueError, "Invalid value for 'autoreconnect'"
- if type(maxretries) in (int, long):
+ if isinstance(maxretries, (int, long)):
self.maxretries = maxretries
else:
raise ValueError, "Invalid value for 'maxretries'"
- if type(timeout) in (int, long):
+ if isinstance(timeout, (int, long)):
self.timeout = timeout
else:
raise ValueError, "Invalid value for 'timeout'"
- if type(retrysleep) in (int, long):
+ if isinstance(retrysleep, (int, long, float)) and retrysleep >= 0:
self.retrysleep = retrysleep
else:
raise ValueError, "Invalid value for 'retrysleep'"
@@ -239,6 +297,19 @@ class Connection(object):
else:
raise ValueError, "Invalid value for 'quietpingpong'"
+ if type(starttls) == bool:
+ if starttls and secure:
+ warnings.warn(
+ "Cannot use STARTTLS when secure=True", ConnectionWarning)
+ self.starttls = starttls
+ else:
+ raise ValueError, "Invalid value for 'starttls'"
+
+ if isinstance(requestcaps, (list, tuple)):
+ self.requestcaps = list(requestcaps)
+ else:
+ raise ValueError, "Invalid value for 'requestcaps'"
+
if type(pinginterval) in (int, long):
self.pinginterval = pinginterval
else:
@@ -260,18 +331,26 @@ class Connection(object):
self._recvhandlerthread = None
# Initialize IRC environment variables
- self.users = UserList(context=self)
- self.channels = ChanList(context=self)
+ self.users = UserList(context=self, withdict=True)
+ self.channels = ChanList(context=self, withdict=True)
+
+ # We are going to try something different, to try to make searching quicker.
+ # self.users={}
+ # self.channels={}
+
+ self.servers = ServerList(context=self)
self.addons = []
- self.trusted = [] # To be implemented later
self._init()
- if type(addons) == list:
- for addon in addons:
- if type(addon) == dict:
- self.addAddon(**addon)
+ for conf in addons:
+ try:
+ if type(conf) == dict:
+ self.addAddon(**conf)
else:
- self.addAddon(addon)
+ self.addAddon(conf)
+ except:
+ pass
+
if autostart:
self.connect()
@@ -281,14 +360,11 @@ class Connection(object):
self._connected = False
self._registered = False
self._connection = None
+ self._starttls = False
self.trynick = 0
self.identity = None
- self.motdgreet = None
- self.motd = None
- self.motdend = None
-
self.serv = None
self.welcome = None
self.hostinfo = None
@@ -298,6 +374,22 @@ class Connection(object):
self.supports = {}
self.throttledata = []
self.throttled = False
+ self.enabledcaps = []
+ self.supportedcaps = []
+ self._requestedcaps = []
+ self._caplsrequested = False
+
+ @property
+ def motdgreet(self):
+ return self.identity.server.motdgreet
+
+ @property
+ def motd(self):
+ return self.identity.server.motd
+
+ @property
+ def motdend(self):
+ return self.identity.server.motdend
@property
def connected(self):
@@ -308,17 +400,32 @@ class Connection(object):
return self._registered
def logwrite(self, *lines):
+ """logwrite(*lines)
+
+ Writes one or more line to the log file, signed with a timestamp."""
with self._loglock:
ts = timestamp()
for line in lines:
- try:
- print >>self.log, u"%s %s" % (ts, line)
- except:
- print line
- raise
+ print >>self.log, u"%s %s" % (ts, line)
self.log.flush()
+ def logerror(self, *lines):
+ """logerror(*lines)
+
+ Prints lines and traceback sys.stderr and to the log file."""
+ exc, excmsg, tb = sys.exc_info()
+ lines = lines + tuple(traceback.format_exc().split("\n"))
+
+ # Print to log AND stderr
+ self.logwrite(*[u"!!! {line}".format(**vars()) for line in lines])
+ for line in lines:
+ print >>sys.stderr, line
+
def logopen(self, filename, encoding="utf8"):
+ """logopen(filename[, encoding])
+
+ Sets the log file to 'filename.'"""
+
with self._loglock:
ts = timestamp()
newlog = codecs.open(filename, "a", encoding=encoding)
@@ -335,8 +442,8 @@ class Connection(object):
handled = []
unhandled = []
errors = []
- for k, addon in enumerate(addons):
- if addons.index(addon) < k:
+ for k, addon in enumerate(addons + [self]):
+ if addon in addons and addons.index(addon) < k:
# Duplicate
continue
@@ -347,25 +454,20 @@ class Connection(object):
# Iterate through all events.
for (method, args, fallback) in events:
- if method in dir(addon) and callable(getattr(addon, method)):
+ try:
f = getattr(addon, method)
- elif fallback and not fellback:
- if "onOther" in dir(addon) and callable(addon.onOther) and data:
- f = addon.onOther
- args = dict(line=line, **data)
- fellback = True
- elif "onUnhandled" in dir(addon) and callable(addon.onUnhandled) and data:
- # Backwards compatability for addons that still use
- # onUnhandled. Use onOther in future development.
- f = addon.onUnhandled
+ except AttributeError:
+ if fallback and not fellback and data:
+ try:
+ f = getattr(addon, "onOther")
+ except AttributeError:
+ unhandled.append(addon)
+ continue
args = dict(line=line, **data)
fellback = True
else:
unhandled.append(addon)
continue
- else:
- unhandled.append(addon)
- continue
if type(f) == new.instancemethod:
argspec = inspect.getargspec(f.im_func)
@@ -381,26 +483,19 @@ class Connection(object):
exc, excmsg, tb = sys.exc_info()
errors.append((addon, exc, excmsg, tb))
- # Print to log AND stderr
- tblines = [u"!!! Exception in addon %(addon)s" % vars()]
- tblines.append(u"!!! Function: %s" % f)
- tblines.append(u"!!! Arguments: %s" % args)
- for line in traceback.format_exc().split("\n"):
- tblines.append(u"!!! %s" % autodecode(line))
- self.logwrite(*tblines)
- print >>sys.stderr, "Exception in addon %(addon)s" % vars()
- print >>sys.stderr, u"Function: %s" % f
- print >>sys.stderr, u"Arguments: %s" % args
- print >>sys.stderr, traceback.format_exc()
+ self.logerror(u"Exception in addon {addon}".format(
+ **vars()), u"Function: %s" % f, u"Arguments: %s" % args)
+
if exceptions: # If set to true, we raise the exception.
raise
else:
handled.append(addon)
return (handled, unhandled, errors)
- # TODO: Build method validation into the next two addons, Complain when a method is not callable or does not take in the expected arguments.
- # Inspects the methods of addon to make sure
def validateAddon(self, addon):
+ """validateAddon(addon)
+
+ Checks the addon's methods and issues warnings when a method's arguments do not line up with what is expected."""
supported = self.eventsupports()
keys = supported.keys()
for fname in dir(addon):
@@ -443,61 +538,82 @@ class Connection(object):
"!!! AddonWarning: Function '%s' does not accept supported arguments: %s" %
(func.__name__, ", ".join(unsupported)))
- def addAddon(self, addon, trusted=False, **params):
+ def addAddon(self, addon, **params):
+ """addAddon(addon[, ...])
+
+ Configures and appends addon to self.addons.
+ Additional keyword arguments are passed onto addon.onAddonAdd whenever the method exists."""
self.validateAddon(addon)
- for a in self.addons:
- if (type(a) == Config and a.addon is addon) or a is addon:
- raise BaseException, "Addon already added."
+
with self.lock:
- if params:
- defconf = Config(addon, **params)
- else:
- defconf = addon
- if hasattr(addon, "onAddonAdd") and callable(addon.onAddonAdd):
- conf = addon.onAddonAdd(self, **params)
- if conf is not None:
- self.addons.append(conf)
- else:
- self.addons.append(defconf)
- else:
- self.addons.append(defconf)
+ addoninstances = [
+ conf.addon if type(conf) == Config else conf for conf in self.addons]
+ if addon in addoninstances:
+ raise AddonError, "Addon already added."
+ conf = self._configureAddon(addon, **params)
+ self.addons.append(conf)
self.logwrite("*** Addon %s added." % repr(addon))
- if trusted:
- self.trusted.append(addon)
- def insertAddon(self, index, addon, trusted=False, **params):
+ def insertAddon(self, index, addon, **params):
+ """insertAddon(index, addon[, ...])
+
+ The 'list.insert' version of addAddon."""
self.validateAddon(addon)
- for a in self.addons:
- if (type(a) == Config and a.addon is addon) or a is addon:
- raise BaseException, "Addon already added."
+
with self.lock:
- if params:
- defconf = Config(addon, **params)
- else:
- defconf = addon
- if hasattr(addon, "onAddonAdd") and callable(addon.onAddonAdd):
- conf = addon.onAddonAdd(self, **params)
- if conf is not None:
- self.addons.insert(index, conf)
- else:
- self.addons.insert(index, defconf)
- else:
- self.addons.insert(index, defconf)
+ addoninstances = [
+ conf.addon if type(conf) == Config else conf for conf in self.addons]
+ if addon in addoninstances:
+ raise AddonError, "Addon already added."
+ conf = self._configureAddon(addon, **params)
+ self.addons.insert(index, conf)
self.logwrite("*** Addon %s inserted into index %d." %
(repr(addon), index))
- if trusted:
- self.trusted.append(addon)
+ # Configures an addon by calling the addon's onAddonAdd instance (if it
+ # exists) and returns the appropriate config object (or just the addon
+ # instance if no config) to put into self.addons
+ def _configureAddon(self, addon, **params):
+ if hasattr(addon, "onAddonAdd") and callable(addon.onAddonAdd):
+ try:
+ conf = addon.onAddonAdd(self, **params)
+ except:
+ self.logerror(
+ u"An exception has occurred while trying to configure addon {addon}.".format(**vars()))
+ raise
+ if conf is None:
+ return addon
+ return conf
+ elif params:
+ return Config(addon, **params)
+ else:
+ return addon
+
+ # Removes addon from self.addons
def rmAddon(self, addon):
+ """rmAddon(addon)
+
+ Removes addon from self.addons."""
with self.lock:
- self.addons.remove(addon)
+ addoninstances = [
+ conf.addon if type(conf) == Config else conf for conf in self.addons]
+
+ del self.addons[addoninstances.index(addon)]
self.logwrite("*** Addon %s removed." % repr(addon))
- if addon in self.trusted:
- self.trusted.remove(addon)
if hasattr(addon, "onAddonRem") and callable(addon.onAddonAdd):
- addon.onAddonRem(self)
+ try:
+ addon.onAddonRem(self)
+ except:
+ self.logerror(
+ u"An exception has occurred while trying to configure addon {addon}.".format(**vars()))
def connect(self, server=None, port=None, secure=None, ipvers=None, forcereconnect=False, blocking=False):
+ """connect([...])
+
+ Starts connection to the IRC server. Optional arguments server, port, secure, and ipvers can be
+ provided to override the current settings.
+ Use 'forcereconnect=True' to quit existing connection if already connected.
+ Use 'blocking=True' to wait until connection is established (or maxretries is exhausted)."""
if ipvers != None:
ipvers = ipvers if type(ipvers) == tuple else (ipvers,)
else:
@@ -527,6 +643,7 @@ class Connection(object):
raise NotConnected
def _connect(self, addr, ipver, secure, hostname=None):
+ """Makes a single attempt to connect to server."""
with self.lock:
if self._connected:
raise AlreadyConnected
@@ -568,17 +685,19 @@ class Connection(object):
raise
else:
# Run onConnect on all addons to signal connection was established.
+ self._connected = True
with self.lock:
self._event(
self.getalladdons(), [("onConnect", dict(), False)])
self.logwrite(
"*** Connection to {addrstr} established.".format(**vars()))
self.addr = addr
- self._connected = True
with self._connecting:
self._connecting.notifyAll()
def _tryaddrs(self, server, addrs, ipver, secure):
+ """Iterates through addrs until a connection is successful, returning True, or returning False when no connections are made.
+ Raises an exception when it detects Network is unreachable (e.g., IPv6 network is not available)."""
for addr in addrs:
try:
if server == addr[0]:
@@ -600,6 +719,7 @@ class Connection(object):
return False
def _tryipver(self, server, port, ipver, secure):
+ """Attempts to resolve 'server' to a one or more IP addresses, then tries to establish a connection."""
if ipver == socket.AF_INET6:
self.logwrite(
"*** Attempting to resolve {server} to an IPv6 address...".format(**vars()))
@@ -631,6 +751,7 @@ class Connection(object):
return self._tryaddrs(server, addrs, ipver, secure)
def _tryipvers(self, server, port, ipvers, secure):
+ """Attempts to try a connection for each IP version in ipvers until a connection is successful."""
for ipver in ipvers:
try:
ret = self._tryipver(server, port, ipver, secure)
@@ -651,6 +772,7 @@ class Connection(object):
return False
def _procrecvline(self, line):
+ """Called whenever a line of data is received from the IRC server."""
matches = re.findall(_ircmatch, line)
# We have a match!
@@ -667,26 +789,13 @@ class Connection(object):
self.logwrite("<<< %s" % line)
if origin == "" and cmd == "PING":
- self._send(u"PONG :%s" % extinfo)
+ self.send(u"PONG :%s" % extinfo)
with self.lock:
data = dict(origin=origin, cmd=cmd, target=target,
targetprefix=None, params=params, extinfo=extinfo)
- if not self._registered:
- if type(cmd) == int and cmd != 451 and target != "*": # Registration complete!
- self.identity = self.user(target, init=True)
- self.serv = origin
- self._event(self.getalladdons(), [
- ("onRegistered", dict(), False)], line, data)
- self._registered = True
-
- elif cmd == 433 and target == "*": # Server reports nick taken, so we need to try another.
- self._trynick()
- if not self._registered: # Registration is not yet complete
- return
-
- if username and host:
+ if username and host and self._registered:
nickname = origin
origin = self.user(origin)
if origin.nick != nickname:
@@ -698,6 +807,8 @@ class Connection(object):
if origin.host != host:
# Origin host has changed
origin.host = host
+ else:
+ origin = self.getserver(origin)
# Check to see if target matches a channel (optionally with
# prefix)
@@ -715,7 +826,7 @@ class Connection(object):
# Check to see if target matches a valid nickname. Do NOT
# convert target to User instance if cmd is NICK.
- elif re.match(_nickmatch, target) and cmd != "NICK":
+ elif re.match(_nickmatch, target) and cmd in ("PRIVMSG", "NOTICE", "MODE", "INVITE", "KILL") and self._registered:
targetprefix = ""
target = self.user(target)
@@ -739,20 +850,13 @@ class Connection(object):
if hasattr(self, parsename) and callable(getattr(self, parsename)):
parsemethod = getattr(self, parsename)
try:
- addons, events = parsemethod(
+ ret = parsemethod(
origin, target, targetprefix, params, extinfo)
+ addons, events = ret if ret is not None else (
+ self.addons, [])
except:
- exc, excmsg, tb = sys.exc_info()
-
- # Print to log AND stderr
- tblines = [
- u"!!! There was an error in parsing the following line:", u"!!! %s" % line]
- for tbline in traceback.format_exc().split("\n"):
- tblines.append(u"!!! %s" % autodecode(tbline))
- self.logwrite(*tblines)
- print >>sys.stderr, u"There was an error in parsing the following line:"
- print >>sys.stderr, u"%s" % line
- print >>sys.stderr, traceback.format_exc()
+ self.logerror(
+ u"There was an error in parsing the following line:", line)
return
else:
addons = self.addons
@@ -763,19 +867,20 @@ class Connection(object):
events = [
("on%s" % cmd.upper(), dict(line=line, origin=origin, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo), True)]
- # Supress pings and pongs if self.quietpingpong is set to True
+ # Suppress pings and pongs if self.quietpingpong is set to True
if cmd in ("PING", "PONG") and self.quietpingpong:
return
# Send parsed data to addons having onRecv method first
self._event(
- addons + [self], [("onRecv", dict(line=line, **data), False)], line, data)
+ addons, [("onRecv", dict(line=line, **data), False)], line, data)
# Support for further addon events is taken care of here. We also treat the irc.Connection instance itself as an addon for the purpose of
# tracking the IRC state, and should be invoked *last*.
- self._event(addons + [self], events, line, data)
+ self._event(addons, events, line, data)
def _recvhandler(self, server, port, ipvers, secure):
+ """Function that is run as a separate thread, both managing the connection and handling data coming from the IRC server."""
if currentThread() != self._recvhandlerthread: # Enforce that this function must only be run from within self._sendhandlerthread.
raise RuntimeError, "This function is designed to run in its own thread."
@@ -842,12 +947,10 @@ class Connection(object):
self._sendline.notify()
# Attempt initial registration.
- nick = self.nick[0]
- if self.passwd:
- self._send(u"PASS %s" % self.passwd)
- self._trynick()
- self._send(u"USER %s * * :%s" %
- (self.username.split("\n")[0].rstrip(), self.realname.split("\n")[0].rstrip()))
+ # nick=self.nick[0]
+ # if self.passwd:
+ #self.send(u"PASS %s" % self.passwd)
+ # self._trynick()
# Initialize buffers
linebuf = []
@@ -860,7 +963,7 @@ class Connection(object):
if pingreq and pingreq in self._outgoing:
self._outgoing.remove(pingreq)
pingreq = (time.time() + self.pinginterval, u"PING %s %s" % (
- self.identity.nick if self.identity else "*", self.serv), self)
+ (self.identity.nick, self.identity.server) if self.identity else ("*", self.server)), self)
self._outgoing.append(pingreq)
self._sendline.notify()
@@ -887,7 +990,7 @@ class Connection(object):
exc, excmsg, tb = sys.exc_info()
with self.lock:
self.logwrite(
- "*** Connection to %(server)s:%(port)s failed: %(excmsg)s." % vars())
+ "*** Connection to {self:uri} failed: {excmsg}.".format(**vars()))
self._event(self.getalladdons(), [
("onConnectFail", dict(exc=exc, excmsg=excmsg, tb=tb), False)])
@@ -928,10 +1031,7 @@ class Connection(object):
pass
except: # Print exception to log file
- self.logwrite(*["!!! FATAL Exception"] + ["!!! %s" %
- line for line in traceback.format_exc().split("\n")])
- print >>sys.stderr, "FATAL Exception"
- print >>sys.stderr, traceback.format_exc()
+ self.logerror(u"FATAL Exception")
sys.exit()
finally:
@@ -944,8 +1044,19 @@ class Connection(object):
self._outgoing.append("quit")
self._sendline.notify()
- # Gets a list of *all* addons, including channel-specific addons.
+ def lower(self, s):
+ """lower(s)
+
+ Transforms a string into lowercase, using whatever casemapping the server is using, whether ascii or rfc1459."""
+ if self.supports.get("CASEMAPPING", "rfc1459") == "ascii":
+ return s.lower()
+ else:
+ return s.translate(_rfc1459casemapping)
+
def getalladdons(self):
+ """getalladdons() --> list
+
+ Returns list of *all* addons, including channel-specific addons."""
return self.addons + reduce(lambda x, y: x + y, [chan.addons for chan in self.channels], [])
# The following methods matching parse* are used to determine what addon methods will be called, and prepares the arguments to be passed.
@@ -959,6 +1070,20 @@ class Connection(object):
# 'fallback' is a flag to determine when a fallback to 'onOther' is permitted.
# Each of these functions should allow passing None to all arguments, in
# which case, should report back *all* supported methods.
+ def parseCAP(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False):
+ if outgoing:
+ return ([], [])
+ if origin == None:
+ return (None, [
+ ("onCapLS", dict(origin=None, capabilities=None), True),
+ ("onCapAck", dict(origin=None, capabilities=None), True),
+ ])
+ if params.upper() == "LS":
+ return (self.getalladdons(), [("onCapLS", dict(capabilities=extinfo.split()), True)])
+ if params.upper() == "ACK":
+ return (self.getalladdons(), [("onCapAck", dict(capabilities=extinfo.split()), True)])
+ return ([], [])
+
def parse001(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None):
return (self.getalladdons(), [("onWelcome", dict(origin=origin, msg=extinfo), True)])
@@ -1015,10 +1140,10 @@ class Connection(object):
return (self.addons, [("onChanCount", dict(origin=origin, chancount=chancount), True)])
def parse305(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Returned from away status
- return (self.getalladdons(), [("onReturn", dict(origin=origin, msg=extinfo), True)])
+ return (self.getalladdons(), [("onMeReturn", dict(origin=origin, msg=extinfo), True)])
def parse306(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Entered away status
- return (self.getalladdons(), [("onAway", dict(origin=origin, msg=extinfo), True)])
+ return (self.getalladdons(), [("onMeAway", dict(origin=origin, msg=extinfo), True)])
def parse311(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Start of WHOIS data
if origin == None:
@@ -1064,6 +1189,7 @@ class Connection(object):
return (None, [("onWhoisServer", dict(origin=None, user=None, nickname=None, server=None, servername=None), True)])
nickname, server = params.split(" ")
user = self.user(nickname)
+ server = self.getserver(server)
return (self.addons, [("onWhoisServer", dict(origin=origin, user=user, nickname=nickname, server=server, servername=extinfo), True)])
def parse313(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # IRC Op
@@ -1107,7 +1233,7 @@ class Connection(object):
return (self.addons, [("onWhoisEnd", dict(origin=origin, user=user, nickname=params, msg=extinfo), True)])
def parse321(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Start LIST
- return (None, [("onListStart", dict(origin=origin, params=params, extinfo=extinfo), True)])
+ return (self.addons, [("onListStart", dict(origin=origin, params=params, extinfo=extinfo), True)])
def parse322(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # LIST item
if origin == None:
@@ -1119,7 +1245,7 @@ class Connection(object):
return (self.addons, [("onListEntry", dict(origin=origin, channel=chan, population=int(pop), extinfo=extinfo), True)])
def parse323(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # End of LIST
- return (None, [("onListEnd", dict(origin=None, endmsg=None), True)])
+ return (self.addons, [("onListEnd", dict(origin=None, endmsg=None), True)])
def parse324(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Modes
if origin == None:
@@ -1180,6 +1306,7 @@ class Connection(object):
channel = None
user = self.user(nick)
+ serv = self.getserver(serv)
if type(channel) == Channel:
return (self.addons + channel.addons, [("onWhoEntry", dict(origin=origin, channel=channel, user=user, channame=channame, username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname), True)])
@@ -1227,6 +1354,53 @@ class Connection(object):
def parse376(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None):
return (self.addons, [("onMOTDEnd", dict(origin=origin, motdend=extinfo), True)])
+ def parseACCOUNT(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False):
+ if outgoing:
+ return ([], [])
+ if origin == None:
+ return (None, [
+ ("onAccountLogin", dict(user=None, account=None), True),
+ ("onMeAccountLogin", dict(account=None), False),
+ ("onAccountLogout", dict(user=None), True),
+ ("onMeAccountLogout", dict(), False)
+ ])
+
+ addons = reduce(
+ lambda x, y: x + y, [channel.addons for channel in origin.channels if self.identity in channel.users], [])
+
+ if origin == self.identity:
+ if target == "*":
+ return (self.addons + addons, [
+ ("onAccountLogout", dict(user=origin), True),
+ ("onMeAccountLogout", dict(), False)
+ ])
+ else:
+ return (self.addons + addons, [
+ ("onAccountLogin", dict(
+ user=origin, account=target), True),
+ ("onMeAccountLogin", dict(account=target), False)
+ ])
+ else:
+ if target == "*":
+ return (self.addons + addons, [("onAccountLogout", dict(user=origin), True)])
+ else:
+ return (self.addons + addons, [("onAccountLogin", dict(user=origin, account=target), True)])
+
+ def parseAWAY(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False):
+ if outgoing:
+ return ([], [])
+
+ if origin == None:
+ return (None, [("onAway", dict(user=None, awaymsg=None), True), ("onReturn", dict(user=None), True)])
+
+ addons = reduce(
+ lambda x, y: x + y, [channel.addons for channel in origin.channels if self.identity in channel.users], [])
+
+ if extinfo:
+ return (self.addons + addons, [("onAway", dict(user=origin, awaymsg=extinfo), True)])
+ else:
+ return (self.addons + addons, [("onReturn", dict(user=origin), True)])
+
def parseNICK(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False):
if outgoing:
return ([], [])
@@ -1255,8 +1429,8 @@ class Connection(object):
if origin == None:
return (None, [
- ("onMeJoin", dict(channel=None), False),
- ("onJoin", dict(user=None, channel=None), True)
+ ("onMeJoin", dict(channel=None, loggedinas=None, realname=None), False),
+ ("onJoin", dict(user=None, channel=None, loggedinas=None, realname=None), True)
])
if type(target) == Channel:
@@ -1265,13 +1439,20 @@ class Connection(object):
channel = self.channel(extinfo)
channel.name = extinfo
+ if "extended-join" in self.enabledcaps:
+ loggedinas = params if params != "*" else None
+ realname = extinfo
+ else:
+ loggedinas = realname = None
+
if origin == self.identity:
return (self.addons + channel.addons, [
- ("onMeJoin", dict(channel=channel), False),
- ("onJoin", dict(user=origin, channel=channel), True),
+ ("onMeJoin", dict(channel=channel, loggedinas=loggedinas, realname=realname), False),
+ ("onJoin", dict(user=origin, channel=channel,
+ loggedinas=loggedinas, realname=realname), True),
])
- return (self.addons + channel.addons, [("onJoin", dict(user=origin, channel=channel), True)])
+ return (self.addons + channel.addons, [("onJoin", dict(user=origin, channel=channel, loggedinas=loggedinas, realname=realname), True)])
def parseKICK(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False):
if outgoing:
@@ -1486,14 +1667,16 @@ class Connection(object):
return (self.addons + target.addons, [("onSendChanMsg", dict(origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo), True)])
if origin == None:
return (None, [
- ("onPrivMsg", dict(user=None, msg=None), True),
- ("onChanMsg", dict(user=None, channel=None, targetprefix=None, msg=None), True),
- ("onCTCP", dict(user=None, ctcptype=None, params=None), True),
- ("onChanCTCP", dict(user=None, channel=None,
- targetprefix=None, ctcptype=None, params=None), True),
- ("onPrivAction", dict(user=None, action=None), True),
- ("onChanAction", dict(
- user=None, channel=None, targetprefix=None, action=None), True),
+ ("onPrivMsg", dict(user=None, msg=None, identified=None), True),
+ ("onChanMsg", dict(user=None, channel=None,
+ targetprefix=None, msg=None, identified=None), True),
+ ("onCTCP", dict(user=None, ctcptype=None, params=None, identified=None), True),
+ ("onChanCTCP", dict(user=None, channel=None, targetprefix=None,
+ ctcptype=None, params=None, identified=None), True),
+ ("onPrivAction", dict(
+ user=None, action=None, identified=None), True),
+ ("onChanAction", dict(user=None, channel=None,
+ targetprefix=None, action=None, identified=None), True),
("onSendPrivMsg", dict(
origin=None, user=None, msg=None), True),
("onSendChanMsg", dict(
@@ -1506,22 +1689,26 @@ class Connection(object):
("onSendChanCTCP", dict(origin=None, channel=None,
targetprefix=None, ctcptype=None, params=None), True),
])
+ if "identify-msg" in self.enabledcaps and extinfo[0] in "+-":
+ identified, extinfo = extinfo.startswith("+"), extinfo[1:]
+ else:
+ identified = None
ctcp = re.findall(_ctcpmatch, extinfo)
if ctcp:
(ctcptype, ext) = ctcp[0]
if target == self.identity:
if ctcptype.upper() == "ACTION":
- return (self.addons, [("onPrivAction", dict(user=origin, action=ext), True)])
- return (self.addons, [("onCTCP", dict(user=origin, ctcptype=ctcptype, params=ext), True)])
+ return (self.addons, [("onPrivAction", dict(user=origin, action=ext, identified=identified), True)])
+ return (self.addons, [("onCTCP", dict(user=origin, ctcptype=ctcptype, params=ext, identified=identified), True)])
if type(target) == Channel:
if ctcptype.upper() == "ACTION":
- return (self.addons, [("onChanAction", dict(user=origin, channel=target, targetprefix=targetprefix, action=ext), True)])
- return (self.addons, [("onChanCTCP", dict(user=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext), True)])
+ return (self.addons, [("onChanAction", dict(user=origin, channel=target, targetprefix=targetprefix, action=ext, identified=identified), True)])
+ return (self.addons, [("onChanCTCP", dict(user=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext, identified=identified), True)])
else:
if type(target) == Channel:
- return (self.addons + target.addons, [("onChanMsg", dict(user=origin, channel=target, targetprefix=targetprefix, msg=extinfo), True)])
+ return (self.addons + target.addons, [("onChanMsg", dict(user=origin, channel=target, targetprefix=targetprefix, msg=extinfo, identified=identified), True)])
elif target == self.identity:
- return (self.addons, [("onPrivMsg", dict(user=origin, msg=extinfo), True)])
+ return (self.addons, [("onPrivMsg", dict(user=origin, msg=extinfo, identified=identified), True)])
def parseNOTICE(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None, outgoing=False):
if outgoing:
@@ -1536,28 +1723,35 @@ class Connection(object):
return (self.addons, [("onSendPrivNotice", dict(origin=origin, user=target, msg=extinfo), True)])
if origin == None:
return (None, [
- ("onPrivNotice", dict(origin=None, msg=None), True),
- ("onChanNotice", dict(
- origin=None, channel=None, targetprefix=None, msg=None), True),
+ ("onPrivNotice", dict(
+ origin=None, msg=None, identified=None), True),
+ ("onServNotice", dict(
+ origin=None, msg=None, identified=None), True),
+ ("onChanNotice", dict(origin=None, channel=None,
+ targetprefix=None, msg=None, identified=None), True),
("onCTCPReply", dict(
- origin=None, ctcptype=None, params=None), True),
+ origin=None, ctcptype=None, params=None, identified=None), True),
("onSendPrivNotice", dict(origin=None, msg=None), True),
("onSendChanNotice", dict(
origin=None, channel=None, targetprefix=None, msg=None), True),
("onSendCTCPReply", dict(
origin=None, ctcptype=None, params=None), True),
])
+ if "identify-msg" in self.enabledcaps and extinfo[0] in "+-":
+ identified, extinfo = extinfo.startswith("+"), extinfo[1:]
+ else:
+ identified = None
ctcp = re.findall(_ctcpmatch, extinfo)
- # print ctcp
if ctcp and target == self.identity:
(ctcptype, ext) = ctcp[0]
- return (self.addons, [("onCTCPReply", dict(origin=origin, ctcptype=ctcptype, params=ext), True)])
+ return (self.addons, [("onCTCPReply", dict(origin=origin, ctcptype=ctcptype, params=ext, identified=identified), True)])
else:
+ if type(origin) == Server:
+ return (self.addons, [("onServNotice", dict(origin=origin, msg=extinfo, identified=identified), True)])
if type(target) == Channel:
- return (self.addons + target.addons, [("onChanNotice", dict(origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo), True)])
+ return (self.addons + target.addons, [("onChanNotice", dict(origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo, identified=identified), True)])
elif target == self.identity:
- # print "onPrivNotice"
- return (self.addons, [("onPrivNotice", dict(origin=origin, msg=extinfo), True)])
+ return (self.addons, [("onPrivNotice", dict(origin=origin, msg=extinfo, identified=identified), True)])
def parse367(self, origin=None, target=None, targetprefix=None, params=None, extinfo=None): # Channel Ban list
if origin == None:
@@ -1652,6 +1846,9 @@ class Connection(object):
return (self.addons + channel.addons, [("onQuietListEnd", dict(channel=channel, endmsg=extinfo), True)])
def eventsupports(self):
+ """eventsupports() --> {eventname: arguments, ...}
+
+ Generates and returns a dict of supported events and associated arguments. Good for attempting to validate addon events."""
supports = {}
for item in dir(self):
if re.match(r"parse(\d{3}|[A-Z]+)", item):
@@ -1673,7 +1870,100 @@ class Connection(object):
})
return supports
+ def _register(self):
+ if self.passwd:
+ self.send(u"PASS %s" % self.passwd)
+ self._trynick()
+ self.send(
+ u"USER {self.username} * * :{self.realname}".format(**vars()))
+
+ def requestcapls(self, origin=None):
+ """requestcapls(...)
+
+ Sends "CAP LS" to the server to request supported capabilities. Please use this method instead of send()."""
+ if not self._caplsrequested:
+ self.send("CAP LS", origin=origin)
+ self._caplsrequested = True
+
# Here are the builtin event handlers.
+
+ def onRecv(self, context, line, origin, cmd, target, targetprefix, params, extinfo):
+ if not self._registered:
+ if type(cmd) == int and cmd < 100 and target != "*": # Registration complete!
+ self.identity = self.user(target, init=True)
+ self.identity.server = origin
+ self._event(self.getalladdons(), [
+ ("onRegistered", dict(), False)])
+
+ def onRegistered(self, context):
+ self._registered = True
+
+ def onConnect(self, context):
+ if self.requestcaps:
+ self.requestcapls()
+ elif self.starttls:
+ self.send("STARTLS")
+ elif len(self._requestedcaps) == 0 and not self._caplsrequested:
+ self._register()
+
+ def onCapLS(self, context, capabilities):
+ self.supportedcaps = capabilities
+ self._caplsrequested = False
+ if self.starttls and "tls" in capabilities and not self.secure and not self._starttls:
+ self.send("STARTTLS")
+ elif not self.registered:
+ requestcaps = [
+ cap for cap in self.requestcaps if cap in capabilities]
+ if requestcaps:
+ self.sendcapsrequest(requestcaps)
+ elif len(self._requestedcaps) == 0:
+ self.send("CAP END")
+ self._register()
+
+ def onCapAck(self, context, capabilities):
+ for cap in capabilities:
+ mods, capname = re.findall(
+ "^([%s]*)(.+)$" % re.escape(_capmodifiers), cap)[0]
+ if "-" in mods and capname in self.enabledcaps:
+ self.enabledcaps.remove(capname)
+ elif cap not in self.enabledcaps:
+ self.enabledcaps.append(capname)
+ if cap in self._requestedcaps:
+ self._requestedcaps.remove(cap)
+ if not self.registered and len(self._requestedcaps) == 0:
+ self.send("CAP END")
+ self._register()
+
+ def onCapNak(self, context, capabilities):
+ for cap in capabilities:
+ mods, capname = re.findall(
+ "^([%s]*)(.+)$" % re.escape(_capmodifiers), cap)[0]
+ if cap in self._requestedcaps:
+ self._requestedcaps.remove(cap)
+ if not self.registered and len(self._requestedcaps) == 0:
+ self.send("CAP END")
+ self._register()
+
+ def on433(self, context, line, origin, target, params, extinfo):
+ if not self._registered: # Server reports nick taken, so we need to try another.
+ self._trynick()
+
+ def on670(self, context, line, origin, target, params, extinfo):
+ self.logwrite("*** Attempting StartTLS")
+ self._connection = ssl.wrap_socket(
+ self._connection, cert_reqs=ssl.CERT_NONE) # Server says go ahead with starttls.
+ self._event(self.getalladdons(), [("onStartTLS", dict(), False)])
+
+ def on691(self, context, line, origin, target, params, extinfo): # STARTTLS Failure
+ self.logwrite("*** StartTLS Failed")
+ if self.requestcaps:
+ self.send("CAP END")
+ self._register()
+
+ def onStartTLS(self, context):
+ self._starttls = True
+ self.onConnect(self)
+
def onWelcome(self, context, origin, msg):
self.welcome = msg # Welcome message
@@ -1690,7 +1980,7 @@ class Connection(object):
protos = u" ".join(
[proto for proto in self.protoctl if proto in supports.keys()])
if protos:
- self._send(u"PROTOCTL {protos}".format(**vars()))
+ self.send(u"PROTOCTL {protos}".format(**vars()))
self.supports.update(supports)
def onSnoMask(self, context, origin, snomask): # Snomask
@@ -1712,13 +2002,20 @@ class Connection(object):
def onChanCount(self, context, origin, chancount):
self.chancount = chancount
- def onReturn(self, identity, origin, msg): # Returned from away status
+ def onReturn(self, context, user): # Returned from away status
+ user.away = False
+ user.awaymsg = None
+
+ def onAway(self, context, user, awaymsg): # Entered away status
+ user.away = True
+ user.awaymsg = awaymsg
+
+ def onMeReturn(self, context, origin): # Returned from away status
self.identity.away = False
self.identity.awaymsg = None
- def onAway(self, identity, origin, msg): # Entered away status
+ def onMeAway(self, context, origin, msg): # Entered away status
self.identity.away = True
- self.identity.awaymsg = msg
def onWhoisStart(self, context, origin, user, nickname, username, host, realname): # Start of WHOIS data
user.nick = nickname
@@ -1807,14 +2104,14 @@ class Connection(object):
channel.modes[mode].append(user)
def onMOTDLine(self, context, origin, motdline): # MOTD line
- self.motd.append(motdline)
+ origin.motd.append(motdline)
def onMOTDStart(self, context, origin, motdgreet): # Begin MOTD
- self.motdgreet = motdgreet
- self.motd = []
+ origin.motdgreet = motdgreet
+ origin.motd = []
def onMOTDEnd(self, context, origin, motdend):
- self.motdend = motdend # End of MOTD
+ origin.motdend = motdend # End of MOTD
# elif cmd==386 and "q" in self.supports["PREFIX"][0]: # Channel Owner (Unreal)
#(channame,owner)=params.split()
@@ -1838,24 +2135,49 @@ class Connection(object):
#if user not in channel.modes["a"]: channel.modes["a"].append(user)
# else: channel.modes["a"]=[user]
+ # def onNickChange(self, context, user, newnick):
+ # for other in self.users:
+ # if self.supports.get("CASEMAPPING", "rfc1459")=="ascii":
+ # collision=other.nick.lower()==newnick.lower()
+ # else:
+ # collision=other.nick.translate(_rfc1459casemapping)==newnick.translate(_rfc1459casemapping)
+ # if collision:
+ # self.users.remove(other) ### Nick collision, safe to assume this orphaned user is offline, so we shall remove the old instance.
+ # for channel in self.channels:
+ # If for some odd reason, the old user still appears common channels, then we will remove the user anyway.
+ # if other in channel.users:
+ # channel.users.remove(other)
+ # user.nick=newnick
+
def onNickChange(self, context, user, newnick):
- for other in self.users:
- if self.supports.get("CASEMAPPING", "rfc1459") == "ascii":
- collision = other.nick.lower() == newnick.lower()
+ if self.lower(user.nick) == self.lower(newnick):
+ user.nick = newnick
+ else:
+ try:
+ other = self.users[newnick]
+ except KeyError:
+ pass
else:
- collision = other.nick.translate(
- _rfc1459casemapping) == newnick.translate(_rfc1459casemapping)
- if collision:
- self.users.remove(
- other) # Nick collision, safe to assume this orphaned user is offline, so we shall remove the old instance.
for channel in self.channels:
# If for some odd reason, the old user still appears common
# channels, then we will remove the user anyway.
if other in channel.users:
channel.users.remove(other)
- user.nick = newnick
+ self.users.remove(other)
+ self.users.remove(user)
+ user.nick = newnick
+ self.users.append(user)
+
+ def onAccountLogin(self, context, user, account):
+ user.loggedinas = account
- def onJoin(self, context, user, channel):
+ def onAccountLogout(self, context, user):
+ user.loggedinas = None
+
+ def onJoin(self, context, user, channel, loggedinas, realname):
+ if "extended-join" in self.enabledcaps:
+ user.loggedinas = loggedinas
+ user.realname = realname
if channel not in user.channels:
user.channels.append(channel)
if user not in channel.users:
@@ -1867,10 +2189,10 @@ class Connection(object):
if channel._joinrequested:
channel._joinreply = "JOIN"
channel._joining.notify()
- self._send(u"MODE %s" % channel.name)
- self._send(u"WHO %s" % channel.name)
- self._send(u"MODE %s :%s" %
- (channel.name, self.supports.get("CHANMODES", _defaultchanmodes)[0]))
+ self.send(u"MODE %s" % channel.name)
+ self.send(u"WHO %s" % channel.name)
+ self.send(u"MODE %s :%s" %
+ (channel.name, self.supports.get("CHANMODES", _defaultchanmodes)[0]))
def onKick(self, context, kicker, channel, kicked, kickmsg):
if channel in kicked.channels:
@@ -2082,10 +2404,19 @@ class Connection(object):
nick = self.nick[s] if type(self.nick) in (list, tuple) else self.nick
if q > 0:
nick = "%s%d" % (nick, q)
- self._send(u"NICK %s" % nick)
+ self.send(u"NICK %s" % nick)
self.trynick += 1
- def _send(self, line, origin=None, T=None):
+ def send(self, line, origin=None, T=None):
+ """send(line[, ...])
+
+ Sends 'line' to IRC server. Try to use this method sparingly by using other methods designed to format requests correctly.
+ Supported optional arguments:
+
+ 'origin': Used (voluntarily) by addons to identify origin of sent data. Good for helping addons ignore lines they send
+ so as to avoid infinite loops.
+
+ 'T': Specifies what time to send the data if not immediately. This method currently throttles PRIVMSGs to avoid floods."""
with self.lock:
if not self.connected:
raise NotConnected
@@ -2125,6 +2456,7 @@ class Connection(object):
self._sendline.notify()
def _procsendline(self, line, origin=None):
+ """Function responsible for sending data to the IRC server and calling all applicable event methods."""
match = re.findall(_ircmatch, line)
if len(match) == 0:
return
@@ -2176,31 +2508,6 @@ class Connection(object):
elif cscmd[0].upper() not in ("GLIST", "ACCESS", "SASET", "DROP", "SENDPASS", "ALIST", "INFO", "LIST", "LOGOUT", "STATUS", "UPDATE", "GETPASS", "FORBID", "SUSPEND", "UNSUSPEND", "OINFO"):
extinfo = "********"
line = "%s %s :%s" % (cmd, target, extinfo)
-
- #ctcp=re.findall(_ctcpmatch, extinfo)
- # if ctcp:
- #(ctcptype,ext)=ctcp[0]
- # if ctcptype.upper()=="ACTION":
- # if type(target)==Channel:
- #self._event("onSendChanAction", self.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, action=ext)
- # elif type(target)==User:
- #self._event("onSendPrivAction", self.addons, origin=origin, user=target, action=ext)
- # else:
- # if type(target)==Channel:
- #self._event("onSendChanCTCP", self.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext)
- # elif type(target)==User:
- #self._event("onSendPrivCTCP", self.addons, origin=origin, user=target, ctcptype=ctcptype, params=ext)
- # else:
- # if type(target)==Channel:
- #self._event("onSendChanMsg", self.addons+target.addons, origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo)
- # elif type(target)==User:
- #self._event("onSendPrivMsg", self.addons, origin=origin, user=target, msg=extinfo)
- # elif target.upper()=="CHANSERV":
- #msg=extinfo.split(" ")
- # if msg[0].upper() in ("IDENTIFY", "REGISTER") and len(msg)>2:
- # msg[2]="********"
- #extinfo=" ".join(msg)
- #line="%s %s :%s"%(cmd, target, extinfo)
elif cmd.upper() in ("NS", "NICKSERV"):
if target.upper() in ("IDENTIFY", "REGISTER"):
params = params.split(" ")
@@ -2247,7 +2554,7 @@ class Connection(object):
target.name = channame
# Check to see if target matches a valid nickname. Do NOT convert
# target to User instance if cmd is NICK.
- elif re.match(_nickmatch, target) and cmd != "NICK":
+ elif re.match(_nickmatch, target) and cmd in ("PRIVMSG", "NOTICE", "MODE", "INVITE", "CHGHOST", "CHGIDENT", "CHGNAME", "WHOIS", "KILL", "SAMODE", "SETHOST", "WHO"):
targetprefix = ""
target = self.user(target)
@@ -2260,20 +2567,13 @@ class Connection(object):
parsemethod = getattr(self, parsename)
if callable(parsemethod):
try:
- addons, events = parsemethod(
+ ret = parsemethod(
origin, target, targetprefix, params, extinfo, outgoing=True)
+ addons, events = ret if ret is not None else (
+ self.events, [])
except:
- exc, excmsg, tb = sys.exc_info()
-
- # Print to log AND stderr
- tblines = [
- u"!!! There was an error in parsing the following line:", u"!!! %s" % line]
- for tbline in traceback.format_exc().split("\n"):
- tblines.append(u"!!! %s" % autodecode(tbline))
- self.logwrite(*tblines)
- print >>sys.stderr, u"There was an error in parsing the following line:"
- print >>sys.stderr, u"%s" % line
- print >>sys.stderr, traceback.format_exc()
+ self.logerror(
+ u"There was an error in parsing the following line:", line)
return
else:
addons = self.addons
@@ -2288,8 +2588,8 @@ class Connection(object):
if cmd not in ("PING", "PONG") or not self.quietpingpong: # Supress pings and pongs if self.quietpingpong is set to True
self._event(
- addons + [self], [("onSend", dict(origin=origin if origin else self, line=line, cmd=cmd, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo), False)], line)
- self._event(addons + [self], events, line)
+ addons, [("onSend", dict(origin=origin if origin else self, line=line, cmd=cmd, target=target, targetprefix=targetprefix, params=params, extinfo=extinfo), False)], line)
+ self._event(addons, events, line)
if not (cmd in ("PING", "PONG") and self.quietpingpong):
#self._event(self.addons, [("onSend" , dict(origin=origin, line=line, cmd=cmd, target=target, params=params, extinfo=extinfo), False)])
@@ -2347,14 +2647,8 @@ class Connection(object):
pass
except:
- tb = traceback.format_exc()
self._quitexpected = True
- tblines = [u"!!! FATAL Exception"]
- for line in traceback.format_exc().split("\n"):
- tblines.append(u"!!! %s" % autodecode(line))
- self.logwrite(*tblines)
- print >>sys.stderr, "FATAL Exception in {self}".format(**vars())
- print >>sys.stderr, tb
+ self.logerror("FATAL Exception in {self}".format(**vars()))
with self._sendline:
try:
self._connection.send(
@@ -2366,14 +2660,12 @@ class Connection(object):
with self._sendline:
self._outgoing.clear() # Clear out _outgoing.
- # For compatibility, when modules still expect irc.Connection to be a
- # subclass of threading.Thread
def isAlive(self):
+ """For compatibility, when modules still expect irc.Connection to be a subclass of threading.Thread."""
return type(self._recvhandlerthread) == Thread and self._recvhandlerthread.isAlive() and type(self._sendhandlerthread) == Thread and self._sendhandlerthread.isAlive()
- # For compatibility, when modules still expect irc.Connection to be a
- # subclass of threading.Thread
def start(self):
+ """For compatibility, when modules still expect irc.Connection to be a subclass of threading.Thread."""
return self.connect()
def __repr__(self):
@@ -2394,51 +2686,86 @@ class Connection(object):
return "irc{ssl}{proto}://[{self.server}]:{port}".format(**locals())
else:
return "irc{ssl}{proto}://{self.server}:{port}".format(**locals())
+ else:
+ return repr(self)
def oper(self, name, passwd, origin=None):
+ """oper(name, passwd[, origin])
+
+ Sends an OPER request to the server. Warning: Invalid oper credentials may be reported to IRC network admins!"""
if re.match(".*[\n\r\\s]", name) or re.match(".*[\n\r\\s]", passwd):
raise InvalidCharacter
- self._send(u"OPER {name} {passwd}".format(**vars()), origin=origin)
+ self.send(u"OPER {name} {passwd}".format(**vars()), origin=origin)
def list(self, params="", origin=None):
+ """list(...)
+
+ Sends a LIST request to the server.
+ TODO: Implement optional blocking."""
if re.match(".*[\n\r\\s]", params):
raise InvalidCharacter
if params:
- self._send(u"LIST {params}".format(**vars()), origin=origin)
+ self.send(u"LIST {params}".format(**vars()), origin=origin)
else:
- self._send(u"LIST", origin=origin)
+ self.send(u"LIST", origin=origin)
- def getmotd(self, target="", origin=None):
- if re.match(".*[\n\r\\s]", name) or re.match(".*[\n\r\\s]", passwd):
- raise InvalidCharacter
- if len(re.findall("^([^\r\n\\s]*)", target)[0]):
- self._send(u"MOTD %s" %
- (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
+ def getmotd(self, server=None, origin=None):
+ """getmotd(...)
+
+ Sends an MOTD request to the server, optionally specifying server.
+ TODO: Implement optional blocking."""
+ if server:
+ self.send(u"MOTD %s" % server.name, origin=origin)
else:
- self._send(u"MOTD", origin=origin)
+ self.send(u"MOTD", origin=origin)
+
+ def version(self, server=None, origin=None):
+ """version(...)
- def version(self, target="", origin=None):
- if len(re.findall("^([^\r\n\\s]*)", target)[0]):
- self._send(u"VERSION %s" %
- (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
+ Sends an VERSION request to the server, optionally specifying server.
+ This is NOT the same as requesting CTCP version from another user.
+ TODO: Implement optional blocking."""
+ if server:
+ self.send(u"VERSION %s" % server.name, origin=origin)
else:
- self._send(u"VERSION", origin=origin)
+ self.send(u"VERSION", origin=origin)
- def stats(self, query, target="", origin=None):
- if len(re.findall("^([^\r\n\\s]*)", target)[0]):
- self._send(u"STATS %s %s" %
- (query, re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
+ def stats(self, query, server=None, origin=None):
+ """stats(query[,...])
+
+ Sends an STATS request to the server, optionally specifying server.
+ STATS requests may be logged by IRC network admins. Use responsibly!
+ TODO: Implement optional blocking."""
+ if server:
+ self.send(u"STATS %s %s" % (query, server.name), origin=origin)
else:
- self._send(u"STATS %s" % query, origin=origin)
+ self.send(u"STATS %s" % query, origin=origin)
+
+ def sendcapsrequest(self, capabilities, origin=None):
+ """sendcapsrequest(capabilities)
+
+ Request capabilities with "CAP REQ". Please use this method instead of using send(...)."""
+ with self.lock:
+ for cap in capabilities:
+ if cap not in self._requestedcaps:
+ self._requestedcaps.append(cap)
+ self.send("CAP REQ {cap}".format(**vars()), origin=origin)
- # Quit IRC session gracefully
def quit(self, msg="", origin=None, blocking=False):
+ """quit(...)
+
+ Quit IRC session gracefully by first sending a QUIT request to the server.
+
+ Optional arguments:
+ 'msg': Quit message
+ 'origin': See help on method 'send'
+ 'blocking': Wait until connection is terminated."""
if "\r" in msg or "\n" in msg:
raise InvalidCharacter
if msg:
- self._send(u"QUIT :%s" % msg, origin=origin)
+ self.send(u"QUIT :%s" % msg, origin=origin)
else:
- self._send(u"QUIT", origin=origin)
+ self.send(u"QUIT", origin=origin)
if blocking:
with self._disconnecting:
while self.connected:
@@ -2446,13 +2773,19 @@ class Connection(object):
self._recvhandlerthread.join()
self._sendhandlerthread.join()
- # Force disconnect -- Not even sending QUIT to server.
def disconnect(self):
+ """disconnect()
+
+ Force disconnect -- Goes right for the jugular, not even sending QUIT to server."""
with self.lock:
self._quitexpected = True
self._connection.shutdown(2)
def ctcpversion(self):
+ """ctcpversion() --> string
+
+ Formats a CTCP version reply from this instance and all attached addons."""
+
reply = []
# Prepare reply for this module
reply.append(
@@ -2478,58 +2811,72 @@ class Connection(object):
return u"; ".join(reply)
def raw(self, line, origin=None):
- self._send(line, origin=origin)
+ """raw(line[, origin])
+
+ Deprecated. Use send() instead."""
+ self.send(line, origin=origin)
def user(self, nick, init=False):
- if type(nick) == str:
- nick = autodecode(nick)
- if self.supports.get("CASEMAPPING", "rfc1459") == "ascii":
- users = [
- user for user in self.users if user.nick.lower() == nick.lower()]
- else:
- users = [user for user in self.users if user.nick.translate(
- _rfc1459casemapping) == nick.translate(_rfc1459casemapping)]
- if len(users):
- if init:
- users[0]._init()
- return users[0]
- else:
- user = User(nick, self)
- self.users.append(user)
- timestamp = reduce(lambda x, y: x + ":" + y, [
- str(t).rjust(2, "0") for t in time.localtime()[0:6]])
- return user
+ """user(nick)
+
+ Return a User object associated with a nickname.
+ Specify init=True to reset all that is known about user."""
+ with self.lock:
+ try:
+ return self.users[nick]
+ except KeyError:
+ user = User(nick, self)
+ self.users.append(user)
+ return user
def channel(self, name, init=False):
- if type(name) == str:
- name = autodecode(name)
- if self.supports.get("CASEMAPPING", "rfc1459") == "ascii":
- channels = [
- chan for chan in self.channels if chan.name.lower() == name.lower()]
- else:
- channels = [chan for chan in self.channels if chan.name.translate(
- _rfc1459casemapping) == name.translate(_rfc1459casemapping)]
- if len(channels):
- if init:
- channels[0]._init()
- return channels[0]
- else:
- timestamp = reduce(lambda x, y: x + ":" + y, [
- str(t).rjust(2, "0") for t in time.localtime()[0:6]])
- chan = Channel(name, self)
- self.channels.append(chan)
- return chan
+ """channel(name)
+
+ Return a Channel object associated with a channel name.
+ Specify init=True to reset all that is known about the channel."""
+ with self.lock:
+ try:
+ return self.channels[name]
+ except KeyError:
+ channel = Channel(name, self)
+ self.channels.append(channel)
+ return channel
+
+ def getserver(self, name, init=False):
+ """server(name)
+
+ Return a Server object associated with a server name.
+ Specify init=True to reset all that is known about the server."""
+ with self.lock:
+ if type(name) == str:
+ name = autodecode(name)
+ servers = [server for server in self.servers if self.lower(
+ server.name) == self.lower(name)]
+
+ if len(servers):
+ if init:
+ servers[0]._init()
+ return servers[0]
+ else:
+ server = Server(name, self)
+ self.servers.append(server)
+ return server
def __getitem__(self, item):
chantypes = self.supports.get("CHANTYPES", _defaultchantypes)
+ if "\r" in item or "\n" in item or " " in item:
+ raise InvalidCharacter
if re.match(_chanmatch % re.escape(chantypes), item):
return self.channel(item)
elif re.match(_usermatch, item):
return self.user(item)
else:
- raise TypeError, "String argument does not match valid channel name or nick name."
+ return self.getserver(item)
def fmtsupports(self):
+ """fmtsupports() --> list
+
+ Formats a valid 005 response from known information."""
supports = [
"CHANMODES=%s" % (",".join(value)) if name == "CHANMODES" else "PREFIX=(%s)%s" %
value if name == "PREFIX" else "%s=%s" % (name, value) if value else name for name, value in self.supports.items()]
@@ -2548,7 +2895,9 @@ class Connection(object):
return lines
def fmtgreeting(self):
- # Prepare greeting (Responses 001 through 004)
+ """fmtgreeting() --> list
+
+ Formats a valid greeting from known information (Responses 001 through 004)."""
lines = []
if self.welcome:
lines.append(
@@ -2565,14 +2914,21 @@ class Connection(object):
return lines
def fmtusermodes(self):
- # Prepars 221 response
+ """fmtusermodes() --> list
+
+ Formats a valid user modes reply from known information (Response 221)."""
return u":{self.serv} 221 {self.identity.nick} +{self.identity.modes}".format(**vars())
def fmtsnomasks(self):
- # Prepare 008 response
+ """fmtsnomasks() --> list
+
+ Formats a valid snomasks reply from known information (Response 008)."""
return u":{self.serv} 008 {self.identity.nick} +{self.identity.snomask} :Server notice mask".format(**vars())
def fmtmotd(self):
+ """fmtmotd() --> list
+
+ Formats a valid MOTD reply from known information (Response 375, 372, and 376; Response 422 if no MOTD)."""
if self.motdgreet and self.motd and self.motdend:
lines = []
lines.append(
@@ -2606,7 +2962,7 @@ class Channel(object):
self._partreply = None
def _init(self):
- for user in self.context.users:
+ for user in self.context.users._dict.values():
if self in user.channels:
user.channels.remove(self)
self.addons = []
@@ -2622,19 +2978,19 @@ class Channel(object):
if target and target not in self.context.supports.get("PREFIX", ("ohv", "@%+"))[1]:
raise InvalidPrefix
for line in re.findall("([^\r\n]+)", msg):
- self.context._send(u"PRIVMSG %s%s :%s" %
- (target, self.name, line), origin=origin)
+ self.context.send(u"PRIVMSG %s%s :%s" %
+ (target, self.name, line), origin=origin)
def who(self, origin=None, blocking=False):
# Send WHO request to server
- self.context._send(u"WHO %s" % (self.name), origin=origin)
+ self.context.send(u"WHO %s" % (self.name), origin=origin)
def fmtwho(self):
# Create WHO reply from current data. TODO
pass
def names(self, origin=None):
- self.context._send(u"NAMES %s" % (self.name), origin=origin)
+ self.context.send(u"NAMES %s" % (self.name), origin=origin)
def fmtnames(self, sort=None, uhnames=False, namesx=False):
# Create NAMES reply from current data.
@@ -2646,9 +3002,9 @@ class Channel(object):
users = list(self.users)
if sort == "mode":
users.sort(key=lambda user: ([user not in self.modes.get(mode, [])
- for mode, char in zip(*self.context.supports.get("PREFIX", ("ohv", "@%+")))], user.nick.lower()))
+ for mode, char in zip(*self.context.supports.get("PREFIX", ("ohv", "@%+")))], self.context.lower(user.nick)))
elif sort == "nick":
- users.sort(key=lambda user: user.nick.lower())
+ users.sort(key=lambda user: self.context.lower(user.nick))
if uhnames:
template = u"{prefixes}{user:full}"
else:
@@ -2715,12 +3071,12 @@ class Channel(object):
if target and target not in self.context.supports.get("PREFIX", ("ohv", "@%+"))[1]:
raise InvalidPrefix
for line in re.findall("([^\r\n]+)", msg):
- self.context._send(u"NOTICE %s%s :%s" %
- (target, self.name, line), origin=origin)
+ self.context.send(u"NOTICE %s%s :%s" %
+ (target, self.name, line), origin=origin)
def settopic(self, msg, origin=None):
- self.context._send(u"TOPIC %s :%s" %
- (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
+ self.context.send(u"TOPIC %s :%s" %
+ (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
def ctcp(self, act, msg="", origin=None):
if len(re.findall("^([^\r\n]*)", msg)[0]):
@@ -2750,10 +3106,10 @@ class Channel(object):
raise ActionAlreadyRequested
self._partrequested = True
if len(re.findall("^([^\r\n]*)", msg)[0]):
- self.context._send(
+ self.context.send(
u"PART %s :%s" % (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
else:
- self.context._send(u"PART %s" % self.name, origin=origin)
+ self.context.send(u"PART %s" % self.name, origin=origin)
# Anticipated Numeric Replies:
@@ -2783,8 +3139,8 @@ class Channel(object):
user) == User else re.findall("^([^\r\n\\s]*)", user)[0]
if nickname == "":
raise InvalidName
- self.context._send(u"INVITE %s %s" %
- (nickname, self.name), origin=origin)
+ self.context.send(u"INVITE %s %s" %
+ (nickname, self.name), origin=origin)
def join(self, key="", blocking=False, timeout=30, origin=None):
with self.context.lock:
@@ -2799,10 +3155,10 @@ class Channel(object):
raise ActionAlreadyRequested
self._joinrequested = True
if len(re.findall("^([^\r\n\\s]*)", key)[0]):
- self.context._send(
+ self.context.send(
u"JOIN %s %s" % (self.name, re.findall("^([^\r\n\\s]*)", key)[0]), origin=origin)
else:
- self.context._send(u"JOIN %s" % self.name, origin=origin)
+ self.context.send(u"JOIN %s" % self.name, origin=origin)
# Anticipated Numeric Replies:
@@ -2836,11 +3192,11 @@ class Channel(object):
if nickname == "":
raise InvalidName
if len(re.findall("^([^\r\n]*)", msg)[0]):
- self.context._send(u"KICK %s %s :%s" %
- (self.name, nickname, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
+ self.context.send(u"KICK %s %s :%s" %
+ (self.name, nickname, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
else:
- self.context._send(u"KICK %s %s" %
- (self.name, nickname), origin=origin)
+ self.context.send(u"KICK %s %s" %
+ (self.name, nickname), origin=origin)
def __repr__(self):
return u"<Channel: {self.name} on {self.context:uri}>".format(**vars())
@@ -2878,6 +3234,7 @@ class User(object):
self.signontime = None
self.secure = None
self.away = None
+ self.loggedinas = None
def __repr__(self):
return (u"<User: %(nick)s!%(username)s@%(host)s>" % vars(self)).encode("utf8")
@@ -2890,13 +3247,13 @@ class User(object):
def msg(self, msg, origin=None):
for line in re.findall("([^\r\n]+)", msg):
- self.context._send(u"PRIVMSG %s :%s" %
- (self.nick, line), origin=origin)
+ self.context.send(u"PRIVMSG %s :%s" %
+ (self.nick, line), origin=origin)
def notice(self, msg, origin=None):
for line in re.findall("([^\r\n]+)", msg):
- self.context._send(u"NOTICE %s :%s" %
- (self.nick, line), origin=origin)
+ self.context.send(u"NOTICE %s :%s" %
+ (self.nick, line), origin=origin)
def ctcp(self, act, msg="", origin=None):
if len(re.findall("^([^\r\n]*)", msg)[0]):
@@ -2967,7 +3324,8 @@ class Config(object):
class ChanList(list):
- def __init__(self, iterable=None, context=None):
+ def __init__(self, iterable=None, context=None, withdict=False):
+ self._dict = {} if withdict else None
if context != None and type(context) != Connection:
raise TypeError, "context must be irc.Connection object or None"
self.context = context
@@ -2976,18 +3334,30 @@ class ChanList(list):
for channel in iterable:
if type(channel) == Channel:
chanlist.append(channel)
+ if context and channel.context != context:
+ raise ValueError, "Channel object does not belong to context."
elif type(channel) in (str, unicode):
if context == None:
raise ValueError, "No context given for string object."
chanlist.append(context.channel(channel))
list.__init__(self, chanlist)
+ if self._dict is not None:
+ if self.context:
+ self._dict.update(
+ {self.context.lower(channel.name): channel for channel in chanlist})
+ else:
+ self._dict.update(
+ {(channel.context, channel.context.lower(channel.name)): channel for channel in chanlist})
else:
list.__init__(self)
def append(self, item):
if type(item) in (str, unicode):
if self.context:
- list.append(self, self.context.channel(item))
+ channel = self.context.channel(item)
+ list.append(self, channel)
+ if self._dict is not None:
+ self._dict[self.context.lower(item)] = channel
return
else:
raise ValueError, "No context given for string object."
@@ -2996,11 +3366,19 @@ class ChanList(list):
if self.context and item.context != self.context:
raise ValueError, "Channel object does not belong to context."
list.append(self, item)
+ if self._dict is not None:
+ if self.context:
+ self._dict[self.context.lower(item.name)] = item
+ else:
+ self._dict[item.context, item.context.lower(item.name)] = item
def insert(self, index, item):
if type(item) in (str, unicode):
if self.context:
- list.insert(self, index, self.context.channel(item))
+ channel = self.context.channel(item)
+ list.insert(self, index, channel)
+ if self._dict is not None:
+ self._dict[self.context.lower(item)] = channel
return
else:
raise ValueError, "No context given for string object."
@@ -3009,53 +3387,74 @@ class ChanList(list):
if self.context and item.context != self.context:
raise ValueError, "Channel object does not belong to context."
list.insert(self, index, item)
+ if self._dict is not None:
+ if self.context:
+ self._dict[self.context.lower(item.name)] = item
+ else:
+ self._dict[item.context, item.context.lower(item.name)] = item
def extend(self, iterable):
- chanlist = []
- for item in iterable:
- if type(item) in (str, unicode):
- if self.context:
- chanlist.append(self.context.channel(item))
- return
- else:
- raise ValueError, "No context given for string object."
- if type(item) != Channel:
- raise TypeError, "Only channel objects are permitted in list"
- if self.context and item.context != self.context:
- raise ValueError, "Channel object does not belong to context."
- chanlist.append(item)
- list.extend(self, chanlist)
+ list.extend(self, ChanList(iterable, context=self.context))
def join(self, origin=None):
if not self.context:
raise ValueError, "No context defined."
if any([channel.key for channel in self]):
- self.context._send(u"JOIN %s %s" %
- (self, ",".join([channel.key if channel.key else "" for channel in self])), origin=origin)
+ self.context.send(u"JOIN %s %s" %
+ (self, ",".join([channel.key if channel.key else "" for channel in self])), origin=origin)
else:
- self.context._send(u"JOIN %s" % self, origin=origin)
+ self.context.send(u"JOIN %s" % self, origin=origin)
def part(self, partmsg=None, origin=None):
if not self.context:
raise ValueError, "No context defined."
if partmsg:
- self.context._send(u"PART %s :%s" %
- (",".join([channel.name for channel in self]), partmsg), origin=origin)
+ self.context.send(u"PART %s :%s" %
+ (",".join([channel.name for channel in self]), partmsg), origin=origin)
else:
- self.context._send(u"PART %s" % self, origin=origin)
+ self.context.send(u"PART %s" % self, origin=origin)
def msg(self, msg, origin=None):
if not self.context:
raise ValueError, "No context defined."
- self.context._send(u"PRIVMSG %s :%s" % (self, msg), origin=origin)
+ self.context.send(u"PRIVMSG %s :%s" % (self, msg), origin=origin)
def __str__(self):
return ",".join([channel.name for channel in self])
+ def __getitem__(self, key):
+ if type(key) in (int, long):
+ return list.__getitem__(self, key)
+ else:
+ if self._dict is not None:
+ if self.context == None:
+ raise ValueError, "No context given for string object."
+ keylower = self.context.lower(key)
+ return self._dict[keylower]
+ else:
+ raise ValueError, "No dict available."
+
+ def __delitem__(self, key):
+ if type(key) in (int, long):
+ channel = self[key]
+ del self._dict[self.context.lower(channel.name)]
+ list.__delitem__(self, channel)
+ else:
+ if self._dict is not None:
+ if self.context == None:
+ raise ValueError, "No context given for string object."
+ keylower = self.context.lower(key)
+ list.__delitem__(self, self._dict[keylower])
+ del self._dict[keylower]
+ else:
+ raise ValueError, "No dict available."
+
class UserList(list):
+ __doc__ = "Subclass of list, with builtin validation."
- def __init__(self, iterable=None, context=None):
+ def __init__(self, iterable=None, context=None, withdict=False):
+ self._dict = {} if withdict else None
if context != None and type(context) != Connection:
raise TypeError, "context must be irc.Connection object or None"
self.context = context
@@ -3063,19 +3462,35 @@ class UserList(list):
userlist = []
for user in iterable:
if type(user) == User:
+ if context and user.context != context:
+ raise ValueError, "User object does not belong to context."
userlist.append(user)
elif type(user) in (str, unicode):
if context == None:
raise ValueError, "No context given for string object."
userlist.append(context.user(user))
list.__init__(self, userlist)
+ if self._dict is not None:
+ if self.context:
+ self._dict.update(
+ {self.context.lower(user.nick): user for user in userlist})
+ else:
+ self._dict.update(
+ {(user.context, user.context.lower(user.nick)): user for user in userlist})
else:
list.__init__(self)
def append(self, item):
+ """append(item)
+
+ Like list.append, but enforces that the appended item must be a User instance.
+ If item is a string, then a User instance will be appended in its place."""
if type(item) in (str, unicode):
if self.context:
- list.append(self, self.context.user(item))
+ user = self.context.user(item)
+ list.append(self, user)
+ if self._dict is not None:
+ self._dict[self.context.lower(item)] = user
return
else:
raise ValueError, "No context given for string object."
@@ -3084,11 +3499,22 @@ class UserList(list):
if self.context and item.context != self.context:
raise ValueError, "User object does not belong to context."
list.append(self, item)
+ if self._dict is not None:
+ if self.context:
+ self._dict[self.context.lower(item.nick)] = item
+ else:
+ self._dict[item.context, item.context.lower(item.nick)] = item
def insert(self, index, item):
+ """insert(index, item)
+
+ Like list.insert."""
if type(item) in (str, unicode):
if self.context:
- list.insert(self, index, self.context.user(item))
+ user = self.context.user(item)
+ list.insert(self, index, user)
+ if self._dict is not None:
+ self._dict[self.context.lower(item)] = user
return
else:
raise ValueError, "No context given for string object."
@@ -3097,31 +3523,66 @@ class UserList(list):
if self.context and item.context != self.context:
raise ValueError, "User object does not belong to context."
list.insert(self, index, item)
+ if self._dict is not None:
+ if self.context:
+ self._dict[self.context.lower(item.nick)] = item
+ else:
+ self._dict[item.context, item.context.lower(item.nick)] = item
def extend(self, iterable):
- userlist = []
- for item in iterable:
- if type(item) in (str, unicode):
- if self.context:
- userlist.append(self.context.user(item))
- return
- else:
- raise ValueError, "No context given for string object."
- if type(item) != User:
- raise TypeError, "Only user objects are permitted in list"
- if self.context and item.context != self.context:
- raise ValueError, "User object does not belong to context."
- userlist.append(item)
- list.extend(self, userlist)
+ """extend(iterable)
+
+ Like list.extend."""
+ list.extend(self, UserList(iterable, context=self.context))
def msg(self, msg, origin=None):
+ """msg(msg[, origin])
+
+ Sends a PRIVMSG to all users on list."""
if not self.context:
raise ValueError, "No context defined."
- self.context._send(u"PRIVMSG %s :%s" % (self, msg), origin=origin)
+ self.context.send(u"PRIVMSG %s :%s" % (self, msg), origin=origin)
def __str__(self):
return ",".join([user.nick for user in self])
+ def __getitem__(self, index):
+ if type(index) in (int, long):
+ return list.__getitem__(self, index)
+ else:
+ if self._dict is not None:
+ if self.context == None:
+ raise ValueError, "No context given for string object."
+ return self._dict[self.context.lower(index)]
+ else:
+ raise ValueError, "No dict available."
+
+ def __delitem__(self, index):
+ if type(index) in (int, long):
+ user = self[index]
+ del self._dict[self.context.lower(user.name)]
+ list.__delitem__(self, user)
+ else:
+ if self._dict is not None:
+ if self.context == None:
+ raise ValueError, "No context given for string object."
+ index = self.context.lower(index)
+ list.__delitem__(self, self._dict[index])
+ del self._dict[index]
+ else:
+ raise ValueError, "No dict available."
+
+ def remove(self, item):
+ if type(item) == User:
+ list.remove(self, item)
+ if self._dict is not None:
+ if self.context:
+ del self._dict[self.context.lower(item.nick)]
+ else:
+ del self._dict[item.context, item.context.lower(item.nick)]
+ else:
+ self.remove(self[item])
+
class Server(object):
@@ -3138,3 +3599,90 @@ class Server(object):
self.motdgreet = None
self.motd = []
self.motdend = None
+
+ def stats(self, query, origin=None):
+ self.context(query, self, origin=origin)
+
+ def __repr__(self):
+ return u"<Server: {self.name} on {self.context:uri}>".format(**vars())
+
+ def __str__(self):
+ return self.name
+
+
+class ServerList(list):
+ __doc__ = "Subclass of list, with builtin validation."
+
+ def __init__(self, iterable=None, context=None):
+ if context != None and type(context) != Connection:
+ raise TypeError, "context must be irc.Connection object or None"
+ self.context = context
+ if iterable:
+ serverlist = []
+ for server in iterable:
+ if type(server) == Server:
+ if self.context and server.context != self.context:
+ raise ValueError, "Server object does not belong to context."
+ serverlist.append(server)
+ elif type(server) in (str, unicode):
+ if context == None:
+ raise ValueError, "No context given for string object."
+ serverlist.append(context.getserver(server))
+ list.__init__(self, serverlist)
+ else:
+ list.__init__(self)
+
+ def append(self, item):
+ """append(item)
+
+ Like list.append, but enforces that the appended item must be a Server instance.
+ If item is a string, then a Server instance will be appended in its place."""
+ if type(item) in (str, unicode):
+ if self.context:
+ list.append(self, self.context.getserver(item))
+ return
+ else:
+ raise ValueError, "No context given for string object."
+ if type(item) != Server:
+ raise TypeError, "Only Server objects are permitted in list"
+ if self.context and item.context != self.context:
+ raise ValueError, "Server object does not belong to context."
+ list.append(self, item)
+
+ def insert(self, index, item):
+ """insert(index, item)
+
+ Like list.insert."""
+ if type(item) in (str, unicode):
+ if self.context:
+ list.insert(self, index, self.context.getserver(item))
+ return
+ else:
+ raise ValueError, "No context given for string object."
+ if type(item) != Server:
+ raise TypeError, "Only Server objects are permitted in list"
+ if self.context and item.context != self.context:
+ raise ValueError, "Server object does not belong to context."
+ list.insert(self, index, item)
+
+ def extend(self, iterable):
+ """extend(iterable)
+
+ Like list.extend."""
+ serverlist = []
+ for item in iterable:
+ if type(item) in (str, unicode):
+ if self.context:
+ serverlist.append(self.context.getserver(item))
+ return
+ else:
+ raise ValueError, "No context given for string object."
+ if type(item) != User:
+ raise TypeError, "Only Server objects are permitted in list"
+ if self.context and item.context != self.context:
+ raise ValueError, "Server object does not belong to context."
+ serverlist.append(item)
+ list.extend(self, serverlist)
+
+ def __str__(self):
+ return ",".join([user.nick for user in self])
diff --git a/ircapp.conf b/ircapp.conf
new file mode 100644
index 0000000..72fe9a2
--- /dev/null
+++ b/ircapp.conf
@@ -0,0 +1,64 @@
+{
+ "addons": {
+ "ax": {
+ "class": "autoexec.Autoexec"
+ },
+ "log": {
+ "class": "logger.Logger",
+ "logroot": "~/pyIRC-logs"
+ }
+ },
+ "networks": {
+ "Freenode": {
+ "class": "irc.Connection",
+ "server": "irc.freenode.net",
+ "secure": true,
+ "nick": "pyIRC-user",
+ "requestcaps": [
+ "away-notify",
+ "multi-prefix",
+ "userhost-in-names",
+ "account-notify"
+ ],
+ "addons": [
+ {
+ "addon": <addons.log>,
+ "label": "Freenode"
+ },
+ {
+ "addon": <addons.ax>,
+ "label": "Freenode",
+ "autojoin": [
+ "#pyirc-ng"
+ ]
+ }
+ ]
+ },
+ "InsomniaIRC": {
+ "class": "irc.Connection",
+ "server": "irc.insomniairc.net",
+ "secure": true,
+ "nick": "pyIRC-user",
+ "requestcaps": [
+ "extended-join",
+ "away-notify",
+ "multi-prefix",
+ "userhost-in-names",
+ "account-notify"
+ ],
+ "addons": [
+ {
+ "addon": <addons.log>,
+ "label": "InsomniaIRC"
+ },
+ {
+ "addon": <addons.ax>,
+ "label": "InsomniaIRC",
+ "autojoin": [
+ "#chat"
+ ]
+ }
+ ]
+ }
+ }
+}
diff --git a/ircapp.py b/ircapp.py
new file mode 100755
index 0000000..3bc444f
--- /dev/null
+++ b/ircapp.py
@@ -0,0 +1,125 @@
+#!/usr/bin/python
+import os
+import re
+import time
+import signal
+import sys
+import irc
+import modjson
+import readline
+import rlcompleter
+import types
+import code
+
+nonaddontypes = (types.ModuleType, types.MethodType,
+ types.FunctionType, types.TypeType, irc.Connection)
+
+
+class IRCApplication:
+
+ def __init__(self, conffile=None):
+ self._quitting = False
+ self.conffile = conffile
+ self.termcaught = False
+ self.confdecoder = modjson.ModJSONDecoder()
+ self.confencoder = modjson.ModJSONEncoder(indent=3)
+ signal.signal(signal.SIGTERM, self.sigterm)
+ self.namespace = {}
+ if conffile and os.path.isfile(conffile):
+ with open(conffile, "r") as f:
+ pyirc = self.confdecoder.decode(f.read())
+ if "addons" in pyirc.keys():
+ self.namespace.update(pyirc["addons"])
+ if "networks" in pyirc.keys():
+ self.namespace.update(pyirc["networks"])
+ self.shell = code.InteractiveConsole(locals=self.namespace)
+ self.namespace["quit"] = self.quit
+ self.namespace["save"] = self.save
+ # self.namespace["exit"]=self.exit
+ self.namespace["irc"] = irc
+
+ def quit(self, quitmsg="Goodbye!"):
+ networks = [
+ o for o in self.namespace.values() if type(o) == irc.Connection]
+ for context in networks:
+ if type(context) == irc.Connection and context.isAlive():
+ context.quit(quitmsg)
+ for context in networks:
+ if type(context) == irc.Connection:
+ with context._disconnecting:
+ while context.connected:
+ context._disconnecting.wait(30)
+ if context._recvhandlerthread:
+ context._recvhandlerthread.join()
+ if context._sendhandlerthread:
+ context._sendhandlerthread.join()
+
+ def complete(self, text, state):
+ raise NotImplemented
+
+ def start(self):
+ sys.ps1 = "(ircapp) "
+ sys.ps2 = "........ "
+ readline.parse_and_bind("tab: complete")
+ completer = rlcompleter.Completer(self.namespace)
+ readline.set_completer(completer.complete)
+ for o in self.namespace.values():
+ if type(o) == irc.Connection:
+ o.connect()
+ while True:
+ try:
+ self.shell.interact(banner="Welcome to pyIRC!")
+ except SystemExit, quitmsg:
+ if not self._quitting:
+ if quitmsg.message:
+ self.quit(quitmsg.message)
+ else:
+ self.quit()
+ break
+ # In case CTRL+D is accidentally sent to the console.
+ print "Ooops... Did you mean to do that?"
+
+ def sigterm(self, signum, frame):
+ if not self.termcaught:
+ self.termcaught = True
+ self.exit("Caught SIGTERM")
+
+ def save(self, conffile=None):
+ addons = {key: o for (key, o) in self.namespace.items()
+ if not isinstance(o, nonaddontypes) and not key.startswith("_")}
+ extraaddons = []
+ networks = {key: o for (key, o) in self.namespace.items() if type(
+ o) == irc.Connection and not key.startswith("_")}
+ if not conffile:
+ conffile = self.conffile
+ with open(conffile, "w") as f:
+ print >>f, self.confencoder.encode(
+ dict(addons=addons, networks=networks))
+
+ def exit(self, quitmsg="Goodbye!"):
+ self.quit(quitmsg)
+ addons = [o for (key, o) in self.namespace.items()
+ if not isinstance(o, nonaddontypes) and not key.startswith("_")]
+ networks = [o for (key, o) in self.namespace.items() if type(
+ o) == irc.Connection and not key.startswith("_")]
+ for context in networks:
+ for conf in list(context.addons):
+ addon = conf.addon if type(conf) == irc.Config else conf
+ context.rmAddon(addon)
+ if addon not in addons:
+ addons.append(addon)
+ for addon in addons:
+ if "stop" in dir(addon) and callable(addon.stop) and "isAlive" in dir(addon) and callable(addon.isAlive) and addon.isAlive():
+ try:
+ addon.stop()
+ except:
+ pass
+ print "Quit: {quitmsg}".format(**vars())
+ self._quitting = True
+ sys.exit()
+
+if __name__ == "__main__":
+ ircapp = IRCApplication(
+ sys.argv[1] if len(sys.argv) > 1 else "ircapp.conf")
+ ircapp.start()
+ ircapp.exit()
diff --git a/logger.py b/logger.py
index 244ce86..779fb9d 100644
--- a/logger.py
+++ b/logger.py
@@ -24,9 +24,9 @@ def LoggerReload(log):
with newlog.rotatelock, log.rotatelock:
newlog.logs = log.logs
log.logs = {}
- for context, label in log.labels.items():
+ for context, conf in log.conf.items():
context.rmAddon(log)
- context.addAddon(newlog, label=label)
+ context.addAddon(newlog, label=conf.label)
return newlog
@@ -34,7 +34,7 @@ class Logger(Thread):
def __init__(self, logroot):
self.logroot = logroot
- path = [logroot]
+ path = [os.path.expanduser(logroot)]
while not os.path.isdir(path[0]):
split = os.path.split(path[0])
@@ -48,7 +48,7 @@ class Logger(Thread):
os.mkdir(path[0])
self.logs = {}
- self.labels = {}
+ self.conf = {}
self.rotatelock = Lock()
Thread.__init__(self)
@@ -66,7 +66,9 @@ class Logger(Thread):
if all([not log or log.closed for log in self.logs.values()]):
break
Y, M, D, h, m, s, w, d, dst = now = time.localtime()
- for context in self.labels.keys():
+
+ logroot = os.path.expanduser(self.logroot)
+ for context in self.conf.keys():
if context.connected:
with context.lock:
try:
@@ -92,20 +94,27 @@ class Logger(Thread):
context.logwrite(*["!!! [Logger] Exception in module %(module)s" % vars()] + [
"!!! [Logger] %s" % tbline for tbline in traceback.format_exc().split("\n")])
context.logopen(
- os.path.join(self.logroot, self.labels[context], "rawdata-%04d.%02d.%02d.log" % now[:3]))
+ os.path.join(logroot, self.conf[context].label, "rawdata-%04d.%02d.%02d.log" % now[:3]))
nextrotate = int(time.mktime((Y, M, D + 1, 0, 0, 0, 0, 0, -1)))
finally:
Thread.__init__(self)
self.daemon = True
def onAddonAdd(self, context, label):
- if label in self.labels.values():
- raise BaseException, "Label already exists"
- if context in self.labels.keys():
- raise BaseException, "Network already exists"
- if not os.path.isdir(os.path.join(self.logroot, label)):
- os.mkdir(os.path.join(self.logroot, label))
- self.labels[context] = label
+ logroot = os.path.expanduser(self.logroot)
+
+ for (context2, conf2) in self.conf.items():
+ if context == context2:
+ raise ValueError, "Context already exists in config."
+ if label == conf2.label:
+ raise ValueError, "Unique label required."
+
+ conf = irc.Config(self, label=label)
+
+ contextroot = os.path.join(logroot, label)
+ if not os.path.isdir(contextroot):
+ os.mkdir(contextroot)
+
if context.connected:
self.openLog(context)
if context.identity:
@@ -115,7 +124,9 @@ class Logger(Thread):
timestamp = reduce(lambda x, y: x + ":" + y, [
str(t).rjust(2, "0") for t in now[0:6]])
context.logopen(
- os.path.join(self.logroot, self.labels[context], "rawdata-%04d.%02d.%02d.log" % now[:3]))
+ os.path.join(contextroot, "rawdata-%04d.%02d.%02d.log" % now[:3]))
+ self.conf[context] = conf
+ return conf
def onAddonRem(self, context):
if context.connected:
@@ -129,9 +140,11 @@ class Logger(Thread):
self.closeLog(user)
if context in self.logs.keys() and not self.logs[context].closed:
self.closeLog(context)
- del self.labels[context]
+ del self.conf[context]
def openLog(self, window):
+ logroot = os.path.expanduser(self.logroot)
+
with self.rotatelock:
if not self.isAlive():
self.start()
@@ -145,12 +158,12 @@ class Logger(Thread):
str(t).rjust(2, "0") for t in now[0:6]])
if type(window) == irc.Connection:
log = self.logs[window] = codecs.open(
- os.path.join(self.logroot, self.labels[window], "console-%04d.%02d.%02d.log" % now[:3]), "a", encoding="utf8")
+ os.path.join(logroot, self.conf[window].label, "console-%04d.%02d.%02d.log" % now[:3]), "a", encoding="utf8")
print >>log, "%s ### Log file opened" % (irc.timestamp())
elif type(window) == irc.Channel:
- label = self.labels[window.context]
- log = self.logs[window] = codecs.open(os.path.join(self.logroot, label, "channel-%s-%04d.%02d.%02d.log" % (
+ label = self.conf[window.context].label
+ log = self.logs[window] = codecs.open(os.path.join(logroot, label, "channel-%s-%04d.%02d.%02d.log" % (
(urllib2.quote(window.name.lower().decode("utf8")).replace("/", "%2f"),) + now[:3])), "a", encoding="utf8")
print >>log, "%s ### Log file opened" % (irc.timestamp())
self.logs[window].flush()
@@ -169,8 +182,10 @@ class Logger(Thread):
irc.timestamp(), window.fmtchancreated())
if type(window) == irc.User:
- logname = os.path.join(self.logroot, self.labels[window.context], "query-%s-%04d.%02d.%02d.log" % (
- (urllib2.quote(window.nick.lower()).replace("/", "%2f"),) + now[:3]))
+ label = self.conf[window.context].label
+ logname = os.path.join(
+ logroot, label, "query-%s-%04d.%02d.%02d.log" %
+ ((urllib2.quote(window.nick.lower()).replace("/", "%2f"),) + now[:3]))
for (other, log) in self.logs.items():
if other == window:
continue
diff --git a/modjson.py b/modjson.py
index f02c935..d52237e 100644
--- a/modjson.py
+++ b/modjson.py
@@ -351,7 +351,7 @@ class ModJSONDecoder(json.JSONDecoder):
return obj, end
-class JSONEncoder(object):
+class ModJSONEncoder(object):
"""Extensible JSON <http://json.org> encoder for Python data structures.
@@ -478,7 +478,49 @@ class JSONEncoder(object):
return JSONEncoder.default(self, o)
"""
- if path:
+ if "json" in dir(o) and callable(o.json):
+ conf = o.json()
+
+ else:
+ conf = collections.OrderedDict()
+ conf["class"] = "{o.__class__.__module__}.{o.__class__.__name__}".format(
+ **vars())
+
+ if "__init__" in dir(o) and type(o.__init__) == new.instancemethod:
+ try:
+ arginspect = inspect.getargspec(o.__init__)
+ except:
+ raise TypeError(repr(o) + " is not JSON serializable")
+
+ if arginspect.defaults:
+ requiredargs = arginspect.args[
+ 1:len(arginspect.args) - len(arginspect.defaults)]
+ argswithdefaults = arginspect.args[
+ len(arginspect.args) - len(arginspect.defaults):]
+ defaultvalues = arginspect.defaults
+ else:
+ requiredargs = arginspect.args[1:]
+ argswithdefaults = []
+ defaultvalues = []
+
+ for key in requiredargs:
+ try:
+ conf[key] = getattr(o, key)
+ except AttributeError:
+ print key
+ print refs.keys()
+ raise TypeError(
+ repr(o) + " is not JSON serializable (Cannot recover required argument '%s')" % key)
+
+ for key, default in zip(argswithdefaults, defaultvalues):
+ try:
+ value = getattr(o, key)
+ if value != default:
+ conf[key] = getattr(o, key)
+ except AttributeError:
+ pass
+
+ if path and not isinstance(conf, (int, long, bool, basestring)) and conf is not None:
pathstr = str(path[0])
numindices = []
for index in path[1:]:
@@ -494,45 +536,7 @@ class JSONEncoder(object):
numindices = []
if pathstr not in refs.keys():
refs[pathstr] = o
- if "json" in dir(o) and callable(o.json):
- return o.json()
-
- conf = collections.OrderedDict()
- conf["class"] = "{o.__class__.__module__}.{o.__class__.__name__}".format(
- **vars())
-
- if "__init__" in dir(o) and type(o.__init__) == new.instancemethod:
- try:
- arginspect = inspect.getargspec(o.__init__)
- except:
- raise TypeError(repr(o) + " is not JSON serializable")
-
- if arginspect.defaults:
- requiredargs = arginspect.args[
- 1:len(arginspect.args) - len(arginspect.defaults)]
- argswithdefaults = arginspect.args[
- len(arginspect.args) - len(arginspect.defaults):]
- defaultvalues = arginspect.defaults
- else:
- requiredargs = arginspect.args[1:]
- argswithdefaults = []
- defaultvalues = []
- for key in requiredargs:
- try:
- conf[key] = getattr(o, key)
- except AttributeError:
- print key
- raise TypeError(
- repr(o) + " is not JSON serializable (Cannot recover required argument '%s')" % key)
-
- for key, default in zip(argswithdefaults, defaultvalues):
- try:
- value = getattr(o, key)
- if value != default:
- conf[key] = getattr(o, key)
- except AttributeError:
- pass
return conf
def encode(self, o):
@@ -630,10 +634,10 @@ class JSONEncoder(object):
o, _current_indent_level, markers, refs, path)
if ref:
yield ref
- elif isinstance(o, (list, tuple)):
+ elif isinstance(o, (list, tuple)) and "json" not in dir(o):
for chunk in self._iterencode_list(o, _current_indent_level, markers, refs, path):
yield chunk
- elif isinstance(o, dict):
+ elif isinstance(o, dict) and "json" not in dir(o):
for chunk in self._iterencode_dict(o, _current_indent_level, markers, refs, path):
yield chunk
else:
@@ -714,10 +718,10 @@ class JSONEncoder(object):
yield buf + ref
else:
yield buf
- if isinstance(value, (list, tuple)):
+ if isinstance(value, (list, tuple)) and "json" not in dir(value):
chunks = self._iterencode_list(
value, _current_indent_level, markers, refs, path + (k,))
- elif isinstance(value, dict):
+ elif isinstance(value, dict) and "json" not in dir(value):
chunks = self._iterencode_dict(
value, _current_indent_level, markers, refs, path + (k,))
else:
@@ -816,10 +820,10 @@ class JSONEncoder(object):
if ref:
yield ref
else:
- if isinstance(value, (list, tuple)):
+ if isinstance(value, (list, tuple)) and "json" not in dir(value):
chunks = self._iterencode_list(
value, _current_indent_level, markers, refs, path + (key,))
- elif isinstance(value, dict):
+ elif isinstance(value, dict) and "json" not in dir(value):
chunks = self._iterencode_dict(
value, _current_indent_level, markers, refs, path + (key,))
else:
diff --git a/startirc.py b/startirc.py
deleted file mode 100755
index 111d9d0..0000000
--- a/startirc.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/python -i
-import os
-import re
-import time
-import logger
-import signal
-import figlet
-import cannon
-import wallet
-import autoexec
-import sys
-import irc
-import bouncer
-import readline
-import rlcompleter
-readline.parse_and_bind("tab: complete")
-
-networks = {}
-
-
-def quit(quitmsg="Goodbye!"):
- global networks
- addons = []
- for IRC in networks.values():
- if IRC.isAlive():
- IRC.quit(quitmsg)
- while any([IRC.isAlive() for IRC in networks.values()]):
- time.sleep(0.25)
- for IRC in networks.values():
- for addon in list(IRC.addons):
- IRC.rmAddon(addon)
- if addon not in addons:
- addons.append(addon)
- for addon in addons:
- if "stop" in dir(addon) and callable(addon.stop) and "isAlive" in dir(addon) and callable(addon.isAlive) and addon.isAlive():
- try:
- addon.stop()
- except:
- pass
- print "Goodbye!"
- sys.exit()
-
-termcaught = False
-
-
-def sigterm(signum, frame):
- global termcaught
- if not termcaught:
- termcaught = True
- quit("Caught SIGTERM")
-
-signal.signal(signal.SIGTERM, sigterm)
-
-logroot = os.path.join(os.environ["HOME"], "IRC")
-
-InsomniaIRC = networks["InsomniaIRC"] = irc.Connection(
- server="irc.insomniairc.net", nick="pyIRC", secure=True)
-
-ax = autoexec.Autoexec()
-log = logger.Logger(logroot)
-
-### Be sure to generate your own cert.pem and key.pem files!
-BNC = bouncer.Bouncer(
- "", 16698, secure=True, certfile="cert.pem", keyfile="key.pem", autoaway="I'm off to see the wizard!")
-
-for (label, IRC) in networks.items():
- IRC.addAddon(log, label=label)
- ### The password is 'hunter2'
- IRC.addAddon(BNC, label=label, passwd="6b97ed68d14eb3f1aa959ce5d49c7dc612e1eb1dafd73b1e705847483fd6a6c809f2ceb4e8df6ff9984c6298ff0285cace6614bf8daa9f0070101b6c89899e22", hashtype="sha512")
-
-InsomniaIRC.addAddon(ax, label="InsomniaIRC", autojoin=["#chat"])
-
-for (label, IRC) in networks.items():
- IRC.start()
diff --git a/wallet.py b/wallet.py
index a5126d5..7f355db 100644
--- a/wallet.py
+++ b/wallet.py
@@ -4,27 +4,30 @@ import Crypto.Cipher.Blowfish
import os
import getpass
from threading import Lock
+from collections import OrderedDict
class Wallet(dict):
+
def __init__(self, filename):
+ self.filename = filename
self.lock = Lock()
if os.path.isfile(filename):
self.f = open(filename, "rb+")
self.passwd = getpass.getpass()
self.crypt = Crypto.Cipher.Blowfish.new(self.passwd)
contents_encrypted = self.f.read()
- contents = self.crypt.decrypt(contents_encrypted +
- "\x00"*((-len(contents_encrypted))%8))
+ contents = self.crypt.decrypt(
+ contents_encrypted + "\x00" * ((-len(contents_encrypted)) % 8))
if contents.startswith(self.passwd):
self.update(dict(pickle.loads(contents[len(self.passwd):])))
else:
self.f.close()
- raise BaseException("Incorrect Password")
+ raise BaseException, "Incorrect Password"
else:
self.f = open(filename, "wb+")
passwd = self.passwd = None
- while passwd is None or passwd != self.passwd:
+ while passwd == None or passwd != self.passwd:
passwd = getpass.getpass("Enter new password: ")
self.passwd = getpass.getpass("Confirm new password: ")
if passwd != self.passwd:
@@ -33,13 +36,22 @@ class Wallet(dict):
self.flush()
def flush(self):
- contents = self.passwd+pickle.dumps(self.items(), protocol=2)
+ contents = self.passwd + pickle.dumps(self.items(), protocol=2)
self.lock.acquire()
try:
self.f.seek(0)
- self.f.write(self.crypt.encrypt(
- contents+"\x00"*((-len(contents))%8)))
+ self.f.write(
+ self.crypt.encrypt(contents + "\x00" * ((-len(contents)) % 8)))
self.f.truncate()
self.f.flush()
finally:
self.lock.release()
+
+ def json(self):
+ return OrderedDict([("class", "wallet.Wallet"), ("filename", self.filename)])
+
+ def __repr__(self):
+ return "<Wallet: %s>" % self.filename
+
+ def __str__(self):
+ return "<Wallet: %s>" % self.filename