summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--irc.py945
1 files changed, 624 insertions, 321 deletions
diff --git a/irc.py b/irc.py
index e542ae5..fe23ae9 100644
--- a/irc.py
+++ b/irc.py
@@ -12,6 +12,8 @@ import ssl
import glob
from collections import deque
import iqueue as Queue
+import chardet
+import codecs
class InvalidName(BaseException):
@@ -118,7 +120,7 @@ class RejoinDelay(BaseException):
pass
_rfc1459casemapping = string.maketrans(string.ascii_uppercase + r'\[]~',
- string.ascii_lowercase + r'|{}^')
+ string.ascii_lowercase + r'|{}^').decode("ISO-8859-2")
# The IRC RFC does not permit the first character in a nickname to be a
# numeral. However, this is not always adhered to.
@@ -129,8 +131,7 @@ _chanmatch = r"^[%%s][^%s\s\n]*$" % re.escape("\x07,")
_targchanmatch = r"^([%%s]?)([%%s][^%s\s\n]*)$" % re.escape("\x07,")
_usermatch = r"^[A-Za-z0-9\-\^\`\\\|\_\{\}\[\]]+$"
_realnamematch = r"^[^\n]*$"
-_ircrecvmatch = r"^:(.+?)(?:!(.+?)@(.+?))?\s+(.+?)(?:\s+(.+?)(?:\s+(.+?))??)??(?:\s+:(.*))?$"
-_ircsendmatch = r"^(.+?)(?:\s+(.+?)(?:\s+(.+?))??)??(?:\s+:(.*))?$"
+_ircmatch = r"^(?::(.+?)(?:!(.+?)@(.+?))?\s+)?([A-Za-z0-9]+?)\s*(?:\s+(.+?)(?:\s+(.+?))??)??(?:\s+:(.*))?$"
_ctcpmatch = "^\x01(.*?)(?:\\s+(.*?)\\s*)?\x01$"
_prefixmatch = r"\((.*)\)(.*)"
@@ -154,11 +155,11 @@ def timestamp():
class Connection(object):
- def __init__(self, server, nick="ircbot", username="python", realname="Python IRC Library", passwd=None, port=None, ipv6=False, ssl=False, autoreconnect=True, log=sys.stderr, timeout=300, retrysleep=5, maxretries=15, onlogin=None):
+ def __init__(self, server, nick="ircbot", username="python", realname="Python IRC Library", passwd=None, port=None, ipv6=False, ssl=False, autoreconnect=True, log=sys.stderr, timeout=300, retrysleep=5, maxretries=15, onlogin=None, quietpingpong=True, pinginterval=60):
self.__name__ = "pyIRC"
- self.__version__ = "1.2"
+ self.__version__ = "1.3"
self.__author__ = "Brian Sherson"
- self.__date__ = "December 28, 2013"
+ self.__date__ = "February 8, 2014"
if port == None:
self.port = 6667 if not ssl else 6697
@@ -199,6 +200,8 @@ class Connection(object):
self.maxretries = maxretries
self.timeout = timeout
self.retrysleep = retrysleep
+ self.quietpingpong = quietpingpong
+ self.pinginterval = pinginterval
self._quitexpected = False
self.log = log
@@ -217,6 +220,8 @@ class Connection(object):
self._recvhandlerthread = None
# Initialize IRC environment variables
+ self.users = UserList(context=self)
+ self.channels = ChanList(context=self)
self._init()
def _init(self):
@@ -226,8 +231,6 @@ class Connection(object):
self.trynick = 0
self.identity = None
- self.users = []
- self.channels = []
self.motdgreet = None
self.motd = None
@@ -258,10 +261,10 @@ class Connection(object):
print >>self.log, "%s %s" % (ts, line)
self.log.flush()
- def logopen(self, filename):
+ def logopen(self, filename, encoding="utf8"):
with self._loglock:
ts = timestamp()
- newlog = open(filename, "a")
+ newlog = codecs.open(filename, "a", encoding=encoding)
if type(self.log) == file and not self.log.closed:
if self.log not in (sys.stdout, sys.stderr):
print >>self.log, "%s ### Log file closed" % (ts)
@@ -270,7 +273,7 @@ class Connection(object):
print >>self.log, "%s ### Log file opened" % (ts)
self.log.flush()
- def _event(self, method, modlist, exceptions=False, **params):
+ def _event(self, method, modlist, exceptions=False, data=None, **params):
# Used to call event handlers on all attached addons, when applicable.
handled = []
unhandled = []
@@ -280,25 +283,39 @@ class Connection(object):
# Duplicate
continue
if method in dir(addon) and callable(getattr(addon, method)):
- try:
- getattr(addon, method)(self, **params)
- except:
- exc, excmsg, tb = sys.exc_info()
- errors.append((addon, exc, excmsg, tb))
-
- # Print to log AND stderr
- self.logwrite(*["!!! Exception in addon %(addon)s" % vars()] + [
- "!!! %s" % line for line in traceback.format_exc().split("\n")])
- print >>sys.stderr, "Exception in addon %(addon)s" % vars()
- print >>sys.stderr, traceback.format_exc()
- if exceptions: # If set to true, we raise the exception.
- raise
- else:
- handled.append(addon)
+ f = getattr(addon, method)
+ args = params
+ elif "onOther" in dir(addon) and callable(addon.onOther) and data:
+ f = addon.onOther
+ args = data
+ 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
+ args = data
else:
unhandled.append(addon)
+ continue
+ try:
+ f(self, **args)
+ except:
+ exc, excmsg, tb = sys.exc_info()
+ errors.append((addon, exc, excmsg, tb))
+
+ # Print to log AND stderr
+ self.logwrite(*["!!! Exception in addon %(addon)s" % vars()] + [
+ "!!! %s" % line for line in traceback.format_exc().split("\n")])
+ print >>sys.stderr, "Exception in addon %(addon)s" % vars()
+ print >>sys.stderr, traceback.format_exc()
+ 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.
+
def addAddon(self, addon, trusted=False, **params):
if addon in self.addons:
raise BaseException, "Addon already added."
@@ -328,12 +345,16 @@ class Connection(object):
if addon in self.trusted:
self.trusted.remove(addon)
- def connect(self):
+ def connect(self, server=None, port=None, ssl=None, ipv6=None):
if self.isAlive():
raise AlreadyConnected
+ with self._sendline:
+ self._outgoing.clear()
with self.lock:
- self._recvhandlerthread = Thread(target=self._recvhandler)
- self._sendhandlerthread = Thread(target=self._sendhandler)
+ self._recvhandlerthread = Thread(
+ target=self._recvhandler, name="Receive Handler", kwargs=dict(server=None, port=None, ssl=None, ipv6=None))
+ self._sendhandlerthread = Thread(
+ target=self._sendhandler, name="Send Handler")
self._recvhandlerthread.start()
self._sendhandlerthread.start()
@@ -382,18 +403,19 @@ class Connection(object):
self._connected = True
def _procrecvline(self, line):
- # If received PING, then just pong back transparently, bypassing
- # _outgoingthread.
- ping = re.findall("^PING :?(.*)$", line)
- if len(ping):
- with self.lock:
- self._connection.send("PONG :%s\n" % ping[0])
- return
-
- self.logwrite("<<< %s" % line)
+ # If received PING, then just pong back transparently, bypassing _outgoingthread.
+ #ping=re.findall("^PING :?(.*)$", line)
+ # if len(ping):
+ # if not self.quietpingpong:
+ #self.logwrite("<<< %s" % line)
+ # with self.lock:
+ #self._connection.send("PONG :%s\n" % ping[0])
+ # if not self.quietpingpong:
+ #self.logwrite(">>> %s" % "PONG :%s" % ping[0])
+ # return
# Attempts to match against pattern ":src cmd target params :extinfo"
- matches = re.findall(_ircrecvmatch, line)
+ matches = re.findall(_ircmatch, line)
# We have a match!
if len(matches):
@@ -406,10 +428,16 @@ class Connection(object):
else:
cmd = cmd.upper()
+ if cmd not in ("PING", "PONG") or not self.quietpingpong:
+ self.logwrite("<<< %s" % line)
+
+ if origin == "" and cmd == "PING":
+ self._send(u"PONG :%s" % extinfo)
+
with self.lock:
if not self._registered:
if type(cmd) == int and cmd != 451 and target != "*": # Registration complete!
- self.identity = self.user(target)
+ self.identity = self.user(target, init=True)
self.serv = origin
self._event("onRegistered", self.addons + reduce(
lambda x, y: x + y, [chan.addons for chan in self.channels], []))
@@ -447,12 +475,13 @@ class Connection(object):
else:
targetprefix = ""
- data = dict(origin=origin, cmd=cmd, target=target,
+ data = dict(line=line, origin=origin, cmd=cmd, target=target,
targetprefix=targetprefix, params=params, extinfo=extinfo)
# Major codeblock here! Track IRC state.
# Send line to addons having onRecv method first
- self._event("onRecv", self.addons, line=line, **data)
+ if cmd not in ("PING", "PONG") or not self.quietpingpong:
+ self._event("onRecv", self.addons, **data)
# Support for further addon events is taken care of here. Each invocation of self._event will return (handled, unhandled, exceptions),
# where handled is the list of addons that have an event handler, and was executed without error, unhandled gives the list of addons
@@ -461,20 +490,20 @@ class Connection(object):
# deadlock.
if cmd == 1:
- (handled, unhandled, exceptions) = self._event("onWelcome", self.addons + reduce(
- lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo)
+ self._event("onWelcome", self.addons + reduce(
+ lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo, data=data)
self.welcome = extinfo # Welcome message
elif cmd == 2:
- (handled, unhandled, exceptions) = self._event("onYourHost", self.addons + reduce(
- lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo)
+ self._event("onYourHost", self.addons + reduce(
+ lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo, data=data)
self.hostinfo = extinfo # Your Host
elif cmd == 3:
- (handled, unhandled, exceptions) = self._event("onServerCreated", self.addons + reduce(
- lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo)
+ self._event("onServerCreated", self.addons + reduce(
+ lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, msg=extinfo, data=data)
self.servcreated = extinfo # Server Created
elif cmd == 4:
- (handled, unhandled, exceptions) = self._event("onServInfo", self.addons + reduce(
- lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, servinfo=params)
+ self._event("onServInfo", self.addons + reduce(
+ lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, servinfo=params, data=data)
self.servinfo = params # What is this code?
elif cmd == 5: # Server Supports
support = dict(
@@ -488,8 +517,8 @@ class Connection(object):
else:
del support[
"PREFIX"] # Might as well delete the info if it doesn't match expected pattern
- (handled, unhandled, exceptions) = self._event("onSupports", self.addons + reduce(
- lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, supports=support)
+ self._event("onSupports", self.addons + reduce(
+ lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, supports=support, data=data)
self.supports.update(support)
if "serv005" in dir(self) and type(self.serv005) == list:
self.serv005.append(params)
@@ -497,129 +526,140 @@ class Connection(object):
self.serv005 = [params]
elif cmd == 8: # Snomask
snomask = params.lstrip("+")
- (handled, unhandled, exceptions) = self._event("onSnoMask", self.addons + reduce(
- lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, snomask=snomask)
+ self._event("onSnoMask", self.addons + reduce(
+ lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, snomask=snomask, data=data)
self.identity.snomask = snomask
if "s" not in self.identity.modes:
self.snomask = ""
elif cmd == 221: # User Modes
modes = (params if params else extinfo).lstrip("+")
- (handled, unhandled, exceptions) = self._event("onUserModes", self.addons + reduce(
- lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, snomask=modes)
+ self._event("onUserModes", self.addons + reduce(
+ lambda x, y: x + y, [chan.addons for chan in self.channels], []), origin=origin, snomask=modes, data=data)
self.identity.modes = modes
if "s" not in self.identity.modes:
self.snomask = ""
elif cmd == 251: # Net Stats
- (handled, unhandled, exceptions) = self._event(
- "onNetStats", self.addons, origin=origin, netstats=extinfo)
+ self._event(
+ "onNetStats", self.addons, origin=origin, netstats=extinfo, data=data)
self.netstats = extinfo
elif cmd == 252:
opcount = int(params)
- (handled, unhandled, exceptions) = self._event(
- "onOpCount", self.addons, origin=origin, opcount=opcount)
+ self._event(
+ "onOpCount", self.addons, origin=origin, opcount=opcount, data=data)
self.opcount = opcount
elif cmd == 254:
chancount = int(params)
- (handled, unhandled, exceptions) = self._event(
- "onChanCount", self.addons, origin=origin, chancount=chancount)
+ self._event(
+ "onChanCount", self.addons, origin=origin, chancount=chancount, data=data)
self.chancount = chancount
elif cmd == 305: # Returned from away status
- (handled, unhandled, exceptions) = self._event(
- "onReturn", self.addons, origin=origin, msg=extinfo)
+ self._event(
+ "onReturn", self.addons, origin=origin, msg=extinfo, data=data)
self.identity.away = False
elif cmd == 306: # Entered away status
- (handled, unhandled, exceptions) = self._event(
- "onAway", self.addons, origin=origin, msg=extinfo)
+ self._event(
+ "onAway", self.addons, origin=origin, msg=extinfo, data=data)
self.identity.away = True
elif cmd == 311: # Start of WHOIS data
nickname, username, host, star = params.split()
user = self.user(nickname)
- (handled, unhandled, exceptions) = self._event("onWhoisStart", self.addons, origin=origin,
- user=user, nickname=nickname, username=username, host=host, realname=extinfo)
+ self._event(
+ "onWhoisStart", self.addons, origin=origin, user=user,
+ nickname=nickname, username=username, host=host, realname=extinfo, data=data)
user.nick = nickname
user.username = username
user.host = host
elif cmd == 301: # Away Message
user = self.user(params)
- (handled, unhandled, exceptions) = self._event(
- "onWhoisAway", self.addons, origin=origin, user=user, nickname=params, awaymsg=extinfo)
+ self._event("onWhoisAway", self.addons, origin=origin,
+ user=user, nickname=params, awaymsg=extinfo, data=data)
user.away = True
user.awaymsg = extinfo
elif cmd == 303: # ISON Reply
users = [self.user(user) for user in extinfo.split(" ")]
- (handled, unhandled, exceptions) = self._event(
- "onIsonReply", self.addons, origin=origin, isonusers=users)
+ self._event(
+ "onIsonReply", self.addons, origin=origin, isonusers=users, data=data)
elif cmd == 307: # Is a registered nick
- (handled, unhandled, exceptions) = self._event("onWhoisRegisteredNick",
- self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo)
+ self._event(
+ "onWhoisRegisteredNick", self.addons, origin=origin,
+ user=self.user(params), nickname=params, msg=extinfo, data=data)
elif cmd == 378: # Connecting From
- (handled, unhandled, exceptions) = self._event("onWhoisConnectingFrom",
- self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo)
+ self._event(
+ "onWhoisConnectingFrom", self.addons, origin=origin,
+ user=self.user(params), nickname=params, msg=extinfo, data=data)
elif cmd == 319: # Channels
- (handled, unhandled, exceptions) = self._event("onWhoisChannels", self.addons,
- origin=origin, user=self.user(params), nickname=params, chanlist=extinfo.split(" "))
+ self._event("onWhoisChannels", self.addons, origin=origin, user=self.user(
+ params), nickname=params, chanlist=extinfo.split(" "), data=data)
elif cmd == 310: # Availability
- (handled, unhandled, exceptions) = self._event("onWhoisAvailability",
- self.addons, origin=origin, user=self.user(params), nickname=params, msg=extinfo)
+ self._event(
+ "onWhoisAvailability", self.addons, origin=origin,
+ user=self.user(params), nickname=params, msg=extinfo, data=data)
elif cmd == 312: # Server
nickname, server = params.split(" ")
user = self.user(nickname)
- (handled, unhandled, exceptions) = self._event("onWhoisServer", self.addons,
- origin=origin, user=user, nickname=nickname, server=server, servername=extinfo)
+ self._event(
+ "onWhoisServer", self.addons, origin=origin, user=user,
+ nickname=nickname, server=server, servername=extinfo, data=data)
user.server = server
elif cmd == 313: # IRC Op
user = self.user(params)
- (handled, unhandled, exceptions) = self._event(
- "onWhoisOp", self.addons, origin=origin, user=user, nickname=params, msg=extinfo)
+ self._event("onWhoisOp", self.addons, origin=origin,
+ user=user, nickname=params, msg=extinfo, data=data)
user.ircop = True
user.ircopmsg = extinfo
elif cmd == 317: # Idle and Signon times
nickname, idletime, signontime = params.split(" ")
user = self.user(nickname)
- (handled, unhandled, exceptions) = self._event("onWhoisTimes", self.addons, origin=origin,
- user=user, nickname=nickname, idletime=int(idletime), signontime=int(signontime), msg=extinfo)
+ self._event(
+ "onWhoisTimes", self.addons, origin=origin, user=user, nickname=nickname,
+ idletime=int(idletime), signontime=int(signontime), msg=extinfo, data=data)
user.idlesince = int(time.time()) - int(idletime)
user.signontime = int(signontime)
elif cmd == 671: # SSL
user = self.user(params)
- (handled, unhandled, exceptions) = self._event(
- "onWhoisSSL", self.addons, origin=origin, user=user, nickname=params, msg=extinfo)
+ self._event("onWhoisSSL", self.addons, origin=origin,
+ user=user, nickname=params, msg=extinfo, data=data)
user.ssl = True
elif cmd == 379: # User modes
- (handled, unhandled, exceptions) = self._event("onWhoisModes", self.addons,
- origin=origin, user=self.user(params), nickname=params, msg=extinfo)
+ self._event("onWhoisModes", self.addons, origin=origin, user=self.user(
+ params), nickname=params, msg=extinfo, data=data)
elif cmd == 330: # Logged in as
nickname, loggedinas = params.split(" ")
user = self.user(nickname)
- (handled, unhandled, exceptions) = self._event("onWhoisLoggedInAs", self.addons,
- origin=origin, user=user, nickname=nickname, loggedinas=loggedinas, msg=extinfo)
+ self._event(
+ "onWhoisLoggedInAs", self.addons, origin=origin, user=user,
+ nickname=nickname, loggedinas=loggedinas, msg=extinfo, data=data)
user.loggedinas = loggedinas
elif cmd == 318: # End of WHOIS
- (handled, unhandled, exceptions) = self._event("onWhoisEnd", self.addons,
- origin=origin, user=self.user(params), nickname=params, msg=extinfo)
+ try:
+ user = self.user(params)
+ except InvalidName:
+ user = params
+ self._event("onWhoisEnd", self.addons, origin=origin,
+ user=user, nickname=params, msg=extinfo, data=data)
elif cmd == 321: # Start LIST
- (handled, unhandled, exceptions) = self._event(
- "onListStart", self.addons, origin=origin, params=params, extinfo=extinfo)
+ self._event(
+ "onListStart", self.addons, origin=origin, params=params, extinfo=extinfo, data=data)
elif cmd == 322: # LIST item
(chan, pop) = params.split(" ", 1)
- (handled, unhandled, exceptions) = self._event("onListEntry", self.addons,
- origin=origin, channel=self.channel(chan), population=int(pop), extinfo=extinfo)
+ self._event("onListEntry", self.addons, origin=origin, channel=self.channel(
+ chan), population=int(pop), extinfo=extinfo, data=data)
elif cmd == 323: # End of LIST
- (handled, unhandled, exceptions) = self._event(
- "onListEnd", self.addons, origin=origin, endmsg=extinfo)
+ self._event(
+ "onListEnd", self.addons, origin=origin, endmsg=extinfo, data=data)
elif cmd == 324: # Channel Modes
modeparams = params.split()
channame = modeparams.pop(0)
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
+ self._event("onRecv", channel.addons, **data)
if channel.name != channame:
channel.name = channame # Server seems to have changed the idea of the case of the channel name
setmodes = modeparams.pop(0)
@@ -632,8 +672,8 @@ class Connection(object):
modedelta.append(("+%s" % mode, param))
elif mode in self.supports["CHANMODES"][3]:
modedelta.append(("+%s" % mode, None))
- (handled, unhandled, exceptions) = self._event(
- "onChannelModes", self.addons + channel.addons, channel=channel, modedelta=modedelta)
+ self._event("onChannelModes", self.addons + channel.addons,
+ channel=channel, modedelta=modedelta, data=data)
for ((modeset, mode), param) in modedelta:
if mode in self.supports["CHANMODES"][2]:
channel.modes[mode] = param
@@ -644,25 +684,26 @@ class Connection(object):
channame, created = params.split()
created = int(created)
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onChanCreated", self.addons + channel.addons, channel=channel, created=created)
+ self._event("onRecv", channel.addons, **data)
+ self._event("onChanCreated", self.addons + channel.addons,
+ channel=channel, created=created, data=data)
channel.created = int(created)
elif cmd == 332: # Channel Topic
channame = params
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onTopic", self.addons + channel.addons, origin=origin, channel=channel, topic=extinfo)
+ self._event("onRecv", channel.addons, **data)
+ self._event("onTopic", self.addons + channel.addons,
+ origin=origin, channel=channel, topic=extinfo, data=data)
channel.topic = extinfo
elif cmd == 333: # Channel Topic info
(channame, nick, dt) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onTopicInfo", self.addons +
- channel.addons, origin=origin, channel=channel, topicsetby=nick, topictime=int(dt))
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onTopicInfo", self.addons + channel.addons, origin=origin,
+ channel=channel, topicsetby=nick, topictime=int(dt), data=data)
channel.topicsetby = nick
channel.topictime = int(dt)
@@ -683,9 +724,15 @@ class Connection(object):
user = self.user(nick)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onWhoEntry", self.addons + channel.addons, origin=origin, channel=channel,
- user=user, channame=channame, username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname)
+ if type(channel) == Channel:
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onWhoEntry", self.addons + channel.addons, origin=origin, channel=channel, user=user, channame=channame,
+ username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname, data=data)
+ else:
+ self._event(
+ "onWhoEntry", self.addons, origin=origin, channel=channel, user=user, channame=channame,
+ username=username, host=host, serv=serv, nick=nick, flags=flags, hops=int(hops), realname=realname, data=data)
user.hops = hops
user.realname = realname
user.username = username
@@ -709,16 +756,16 @@ class Connection(object):
chantypes = self.supports.get("CHANTYPES", "&#+!")
if re.match(_chanmatch % re.escape(chantypes), params):
channel = self.channel(params)
- (handled, unhandled, exceptions) = self._event(
- "onWhoEnd", self.addons + channel.addons, origin=origin, param=params, endmsg=extinfo)
+ self._event("onWhoEnd", self.addons + channel.addons,
+ origin=origin, param=params, endmsg=extinfo, data=data)
else:
- (handled, unhandled, exceptions) = self._event(
- "onWhoEnd", self.addons, origin=origin, param=params, endmsg=extinfo)
+ self._event(
+ "onWhoEnd", self.addons, origin=origin, param=params, endmsg=extinfo, data=data)
elif cmd == 353: # NAMES reply
(flag, channame) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
+ self._event("onRecv", channel.addons, **data)
if self.supports.has_key("PREFIX"):
names = re.findall(
@@ -730,8 +777,9 @@ class Connection(object):
# Still put it into tuple form for
# compatibility in the next
# structure
- (handled, unhandled, exceptions) = self._event("onNames", self.addons + channel.addons,
- origin=origin, channel=channel, flag=flag, channame=channame, nameslist=names)
+ self._event(
+ "onNames", self.addons + channel.addons, origin=origin,
+ channel=channel, flag=flag, channame=channame, nameslist=names, data=data)
for (symbs, nick, username, host) in names:
user = self.user(nick)
@@ -757,27 +805,28 @@ class Connection(object):
elif cmd == 366: # End of NAMES reply
channel = self.channel(params)
- (handled, unhandled, exceptions) = self._event("onNamesEnd", self.addons +
- channel.addons, origin=origin, channel=channel, channame=params, endmsg=extinfo)
+ self._event(
+ "onNamesEnd", self.addons + channel.addons, origin=origin,
+ channel=channel, channame=params, endmsg=extinfo, data=data)
elif cmd == 372: # MOTD line
- (handled, unhandled, exceptions) = self._event(
- "onMOTDLine", self.addons, origin=origin, motdline=extinfo)
+ self._event(
+ "onMOTDLine", self.addons, origin=origin, motdline=extinfo, data=data)
self.motd.append(extinfo)
elif cmd == 375: # Begin MOTD
- (handled, unhandled, exceptions) = self._event(
- "onMOTDStart", self.addons, origin=origin, motdgreet=extinfo)
+ self._event(
+ "onMOTDStart", self.addons, origin=origin, motdgreet=extinfo, data=data)
self.motdgreet = extinfo
self.motd = []
elif cmd == 376:
- (handled, unhandled, exceptions) = self._event(
- "onMOTDEnd", self.addons, origin=origin, motdend=extinfo)
+ self._event(
+ "onMOTDEnd", self.addons, origin=origin, motdend=extinfo, data=data)
self.motdend = extinfo # End of MOTD
elif cmd == 386 and "q" in self.supports["PREFIX"][0]: # Channel Owner (Unreal)
(channame, owner) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
+ self._event("onRecv", channel.addons, **data)
if channel.name != channame:
channel.name = channame # Server seems to have changed the idea of the case of the channel name
user = self.user(owner)
@@ -792,7 +841,7 @@ class Connection(object):
elif cmd == 388 and "a" in self.supports["PREFIX"][0]: # Channel Admin (Unreal)
(channame, admin) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
+ self._event("onRecv", channel.addons, **data)
if channel.name != channame:
channel.name = channame # Server seems to have changed the idea of the case of the channel name
user = self.user(admin)
@@ -809,11 +858,11 @@ class Connection(object):
addons = reduce(
lambda x, y: x + y, [chan.addons for chan in origin.channels], [])
- self._event("onRecv", addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onNickChange", self.addons + addons, user=origin, newnick=newnick)
+ self._event("onRecv", addons, **data)
+ self._event(
+ "onNickChange", self.addons + addons, user=origin, newnick=newnick, data=data)
if origin == self.identity:
- (handled, unhandled, exceptions) = self._event(
+ self._event(
"onMeNickChange", self.addons + addons, newnick=newnick)
for u in self.users:
@@ -831,9 +880,9 @@ class Connection(object):
elif cmd == "JOIN":
channel = target if type(
target) == Channel else self.channel(extinfo)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onJoin", self.addons + channel.addons, user=origin, channel=channel)
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onJoin", self.addons + channel.addons, user=origin, channel=channel, data=data)
if origin == self.identity: # This means the bot is entering the room,
# and will reset all the channel data, on the assumption that such data may have changed.
@@ -842,17 +891,14 @@ class Connection(object):
if channel._joinrequested:
channel._joinreply = cmd
channel._joining.notify()
- channel.topic = ""
- channel.topicmod = ""
- channel.modes = {}
- channel.users = []
+ channel._init()
self._event(
"onMeJoin", self.addons + channel.addons, channel=channel)
- self.raw("MODE %s" % channel.name)
- self.raw("WHO %s" % channel.name)
+ self._send(u"MODE %s" % channel.name)
+ self._send(u"WHO %s" % channel.name)
if "CHANMODES" in self.supports.keys():
- self.raw(
- "MODE %s :%s" % (channel.name, self.supports["CHANMODES"][0]))
+ self._send(
+ u"MODE %s :%s" % (channel.name, self.supports["CHANMODES"][0]))
if channel not in origin.channels:
origin.channels.append(channel)
@@ -864,15 +910,16 @@ class Connection(object):
if kicked.nick != params:
kicked.nick = params
- self._event("onRecv", target.addons, line=line, **data)
+ self._event("onRecv", target.addons, **data)
if origin == self.identity:
self._event(
"onMeKick", self.addons + target.addons, channel=target, kicked=kicked, kickmsg=extinfo)
if kicked == self.identity:
self._event("onMeKicked", self.addons + target.addons,
kicker=origin, channel=target, kickmsg=extinfo)
- (handled, unhandled, exceptions) = self._event("onKick", self.addons +
- target.addons, kicker=origin, channel=target, kicked=kicked, kickmsg=extinfo)
+ self._event(
+ "onKick", self.addons + target.addons, kicker=origin,
+ channel=target, kicked=kicked, kickmsg=extinfo, data=data)
if target in kicked.channels:
kicked.channels.remove(target)
@@ -885,16 +932,16 @@ class Connection(object):
elif cmd == "PART":
try:
- self._event("onRecv", target.addons, line=line, **data)
+ self._event("onRecv", target.addons, **data)
if origin == self.identity:
- with target ._parting:
+ with target._parting:
if target._partrequested:
target._partreply = cmd
target._parting.notify()
self._event(
"onMePart", self.addons + target.addons, channel=target, partmsg=extinfo)
- (handled, unhandled, exceptions) = self._event(
- "onPart", self.addons + target.addons, user=origin, channel=target, partmsg=extinfo)
+ self._event("onPart", self.addons + target.addons,
+ user=origin, channel=target, partmsg=extinfo, data=data)
if target in origin.channels:
origin.channels.remove(target)
@@ -911,9 +958,9 @@ class Connection(object):
channels = list(origin.channels)
addons = reduce(
lambda x, y: x + y, [chan.addons for chan in origin.channels], [])
- self._event("onRecv", addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onQuit", self.addons + addons, user=origin, quitmsg=extinfo)
+ self._event("onRecv", addons, **data)
+ self._event(
+ "onQuit", self.addons + addons, user=origin, quitmsg=extinfo, data=data)
for channel in origin.channels:
with channel.lock:
if origin in channel.users:
@@ -926,7 +973,7 @@ class Connection(object):
elif cmd == "MODE":
if type(target) == Channel:
- self._event("onRecv", target.addons, line=line, **data)
+ self._event("onRecv", target.addons, **data)
modedelta = []
modeparams = params.split()
setmodes = modeparams.pop(0)
@@ -943,9 +990,13 @@ class Connection(object):
if modeset == "+":
eventname = _maskmodeeventnames[
mode][0]
+ if mode == "k":
+ target.key = param
if modeset == "-":
eventname = _maskmodeeventnames[
mode][1]
+ if mode == "k":
+ target.key = None
matchesbot = glob.fnmatch.fnmatch(
"%s!%s@%s".lower() % (self.identity.nick, self.identity.username, self.identity.host), param.lower())
self._event(
@@ -981,8 +1032,9 @@ class Connection(object):
"onMe%s" % eventname, self.addons + target.addons, user=origin, channel=target)
modedelta.append(
("%s%s" % (modeset, mode), modeuser))
- (handled, unhandled, exceptions) = self._event(
- "onChanModeSet", self.addons + target.addons, user=origin, channel=target, modedelta=modedelta)
+ self._event(
+ "onChanModeSet", self.addons + target.addons,
+ user=origin, channel=target, modedelta=modedelta, data=data)
with target.lock:
for ((modeset, mode), param) in modedelta:
if mode in self.supports["CHANMODES"][0]:
@@ -1035,23 +1087,42 @@ class Connection(object):
target.modes[mode] = [param]
elif target.modes.has_key(mode) and param in target.modes[mode]:
target.modes[mode].remove(param)
- elif type(target) == User:
+ elif target == self.identity:
modeparams = (params if params else extinfo).split()
setmodes = modeparams.pop(0)
+ modedelta = []
modeset = "+"
for mode in setmodes:
if mode in "+-":
modeset = mode
continue
if modeset == "+":
+ if mode == "s":
+ if len(modeparams):
+ snomask = modeparams.pop(0)
+ snomaskdelta = []
+ snomodeset = "+"
+ for snomode in snomask:
+ if snomode in "+-":
+ snomodeset = snomode
+ continue
+ snomaskdelta.append(
+ "%s%s" % (snomodeset, snomode))
+ modedelta.append(("+s", snomaskdelta))
+ else:
+ modedelta.append(("+s", []))
+ else:
+ modedelta.append(("+%s" % mode, None))
+ if modeset == "-":
+ modedelta.append(("-%s" % mode, None))
+ self._event(
+ "onUserModeSet", self.addons, origin=origin, modedelta=modedelta, data=data)
+ for ((modeset, mode), param) in modedelta:
+ if modeset == "+":
if mode not in target.modes:
target.modes += mode
- if mode == "s" and len(modeparams):
- snomask = modeparams.pop(0)
- for snomode in snomask:
- if snomode in "+-":
- snomodeset = snomode
- continue
+ if mode == "s":
+ for snomodeset, snomode in param:
if snomodeset == "+" and snomode not in target.snomask:
target.snomask += snomode
if snomodeset == "-" and snomode in target.snomask:
@@ -1065,9 +1136,9 @@ class Connection(object):
target.snomask = ""
elif cmd == "TOPIC":
- self._event("onRecv", target.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onTopicSet", self.addons + target.addons, user=origin, channel=target, topic=extinfo)
+ self._event("onRecv", target.addons, **data)
+ self._event("onTopicSet", self.addons + target.addons,
+ user=origin, channel=target, topic=extinfo, data=data)
with target.lock:
target.topic = extinfo
@@ -1076,13 +1147,13 @@ class Connection(object):
elif cmd == "INVITE":
channel = self.channel(extinfo if extinfo else params)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onInvite", self.addons + channel.addons, user=origin, channel=channel)
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onInvite", self.addons + channel.addons, user=origin, channel=channel, data=data)
elif cmd == "PRIVMSG":
if type(target) == Channel:
- self._event("onRecv", target.addons, line=line, **data)
+ self._event("onRecv", target.addons, **data)
# CTCP handling
ctcp = re.findall(_ctcpmatch, extinfo)
@@ -1090,18 +1161,20 @@ class Connection(object):
(ctcptype, ext) = ctcp[0]
if ctcptype.upper() == "ACTION":
if type(target) == Channel:
- (handled, unhandled, exceptions) = self._event(
- "onChanAction", self.addons + target.addons, user=origin, channel=target, targetprefix=targetprefix, action=ext)
+ self._event(
+ "onChanAction", self.addons + target.addons, user=origin,
+ channel=target, targetprefix=targetprefix, action=ext, data=data)
elif target == self.identity:
- (handled, unhandled, exceptions) = self._event(
- "onPrivAction", self.addons, user=origin, action=ext)
+ self._event(
+ "onPrivAction", self.addons, user=origin, action=ext, data=data)
else:
if type(target) == Channel:
- (handled, unhandled, exceptions) = self._event("onChanCTCP", self.addons + target.addons,
- user=origin, channel=target, targetprefix=targetprefix, ctcptype=ctcptype, params=ext)
+ self._event(
+ "onChanCTCP", self.addons + target.addons, user=origin, channel=target,
+ targetprefix=targetprefix, ctcptype=ctcptype, params=ext, data=data)
elif target == self.identity:
- (handled, unhandled, exceptions) = self._event(
- "onPrivCTCP", self.addons, user=origin, ctcptype=ctcptype, params=ext)
+ self._event(
+ "onPrivCTCP", self.addons, user=origin, ctcptype=ctcptype, params=ext, data=data)
if ctcptype.upper() == "VERSION":
origin.ctcpreply("VERSION", self.ctcpversion())
if ctcptype.upper() == "TIME":
@@ -1115,36 +1188,39 @@ class Connection(object):
origin.ctcpreply("FINGER", "%(ext)s" % vars())
else:
if type(target) == Channel:
- (handled, unhandled, exceptions) = self._event(
- "onChanMsg", self.addons + target.addons, user=origin, channel=target, targetprefix=targetprefix, msg=extinfo)
+ self._event(
+ "onChanMsg", self.addons + target.addons, user=origin,
+ channel=target, targetprefix=targetprefix, msg=extinfo, data=data)
elif target == self.identity:
- (handled, unhandled, exceptions) = self._event(
- "onPrivMsg", self.addons, user=origin, msg=extinfo)
+ self._event(
+ "onPrivMsg", self.addons, user=origin, msg=extinfo, data=data)
elif cmd == "NOTICE":
if type(target) == Channel:
- self._event("onRecv", target.addons, line=line, **data)
+ self._event("onRecv", target.addons, **data)
# CTCP handling
ctcp = re.findall(_ctcpmatch, extinfo)
if ctcp and target == self.identity:
(ctcptype, ext) = ctcp[0]
- (handled, unhandled, exceptions) = self._event(
- "onCTCPReply", self.addons, origin=origin, ctcptype=ctcptype, params=ext)
+ self._event(
+ "onCTCPReply", self.addons, origin=origin, ctcptype=ctcptype, params=ext, data=data)
else:
if type(target) == Channel:
- (handled, unhandled, exceptions) = self._event(
- "onChanNotice", self.addons + target.addons, origin=origin, channel=target, targetprefix=targetprefix, msg=extinfo)
+ self._event(
+ "onChanNotice", self.addons + target.addons, origin=origin,
+ channel=target, targetprefix=targetprefix, msg=extinfo, data=data)
elif target == self.identity:
- (handled, unhandled, exceptions) = self._event(
- "onPrivNotice", self.addons, origin=origin, msg=extinfo)
+ self._event(
+ "onPrivNotice", self.addons, origin=origin, msg=extinfo, data=data)
elif cmd == 367: # Channel Ban list
(channame, mask, setby, settime) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onBanListEntry", self.addons + channel.addons,
- origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime))
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onBanListEntry", self.addons + channel.addons, origin=origin,
+ channel=channel, mask=mask, setby=setby, settime=int(settime), data=data)
if "b" in channel.modes.keys():
if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["b"]]:
channel.modes["b"].append(
@@ -1153,16 +1229,17 @@ class Connection(object):
channel.modes["b"] = [(mask, setby, int(settime))]
elif cmd == 368:
channel = self.channel(params)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onBanListEnd", self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo)
+ self._event("onRecv", channel.addons, **data)
+ self._event("onBanListEnd", self.addons + channel.addons,
+ origin=origin, channel=channel, endmsg=extinfo, data=data)
elif cmd == 346: # Channel Invite list
(channame, mask, setby, settime) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onInviteListEntry", self.addons +
- channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime))
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onInviteListEntry", self.addons + channel.addons, origin=origin,
+ channel=channel, mask=mask, setby=setby, settime=int(settime), data=data)
if "I" in channel.modes.keys():
if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["I"]]:
channel.modes["I"].append(
@@ -1171,16 +1248,18 @@ class Connection(object):
channel.modes["I"] = [(mask, setby, int(settime))]
elif cmd == 347:
channel = self.channel(params)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onInviteListEnd", self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo)
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onInviteListEnd", self.addons + channel.addons,
+ origin=origin, channel=channel, endmsg=extinfo, data=data)
elif cmd == 348: # Channel Ban Exception list
(channame, mask, setby, settime) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onBanExceptListEntry", self.addons +
- channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime))
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onBanExceptListEntry", self.addons + channel.addons, origin=origin,
+ channel=channel, mask=mask, setby=setby, settime=int(settime), data=data)
if "e" in channel.modes.keys():
if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["e"]]:
channel.modes["e"].append(
@@ -1189,16 +1268,18 @@ class Connection(object):
channel.modes["e"] = [(mask, setby, int(settime))]
elif cmd == 349:
channel = self.channel(params)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onBanExceptListEnd",
- self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo)
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onBanExceptListEnd", self.addons + channel.addons,
+ origin=origin, channel=channel, endmsg=extinfo, data=data)
elif cmd == 910: # Channel Access List
(channame, mask, setby, settime) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onAccessListEntry", self.addons +
- channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime))
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onAccessListEntry", self.addons + channel.addons, origin=origin,
+ channel=channel, mask=mask, setby=setby, settime=int(settime), data=data)
if "w" in channel.modes.keys():
if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["b"]]:
channel.modes["w"].append(
@@ -1207,16 +1288,18 @@ class Connection(object):
channel.modes["w"] = [(mask, setby, int(settime))]
elif cmd == 911:
channel = self.channel(params)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onAccessListEnd", self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo)
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onAccessListEnd", self.addons + channel.addons,
+ origin=origin, channel=channel, endmsg=extinfo, data=data)
elif cmd == 941: # Spam Filter list
(channame, mask, setby, settime) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onSpamfilterListEntry", self.addons +
- channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime))
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onSpamfilterListEntry", self.addons + channel.addons, origin=origin,
+ channel=channel, mask=mask, setby=setby, settime=int(settime), data=data)
if "g" in channel.modes.keys():
if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["g"]]:
channel.modes["g"].append(
@@ -1225,16 +1308,18 @@ class Connection(object):
channel.modes["g"] = [(mask, setby, int(settime))]
elif cmd == 940:
channel = self.channel(params)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onSpamfilterListEnd",
- self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo)
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onSpamfilterListEnd", self.addons + channel.addons,
+ origin=origin, channel=channel, endmsg=extinfo, data=data)
elif cmd == 954: # Channel exemptchanops list
(channame, mask, setby, settime) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onExemptChanOpsListEntry", self.addons +
- channel.addons, origin=origin, channel=channel, mask=mask, setby=setby, settime=int(settime))
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onExemptChanOpsListEntry", self.addons + channel.addons, origin=origin,
+ channel=channel, mask=mask, setby=setby, settime=int(settime), data=data)
if "X" in channel.modes.keys():
if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["X"]]:
channel.modes["X"].append(
@@ -1243,16 +1328,18 @@ class Connection(object):
channel.modes["X"] = [(mask, setby, int(settime))]
elif cmd == 953:
channel = self.channel(params)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onExemptChanOpsListEnd",
- self.addons + channel.addons, origin=origin, channel=channel, endmsg=extinfo)
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onExemptChanOpsListEnd", self.addons + channel.addons,
+ origin=origin, channel=channel, endmsg=extinfo, data=data)
elif cmd == 728: # Channel quiet list
(channame, modechar, mask, setby, settime) = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event("onQuietListEntry", self.addons + channel.addons,
- origin=origin, channel=channel, modechar=modechar, mask=mask, setby=setby, settime=int(settime))
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onQuietListEntry", self.addons + channel.addons, origin=origin, channel=channel,
+ modechar=modechar, mask=mask, setby=setby, settime=int(settime), data=data)
if "q" in channel.modes.keys():
if mask.lower() not in [m.lower() for (m, s, t) in channel.modes["q"]]:
channel.modes["q"].append(
@@ -1262,23 +1349,24 @@ class Connection(object):
elif cmd == 729:
channame, modechar = params.split()
channel = self.channel(channame)
- self._event("onRecv", channel.addons, line=line, **data)
- (handled, unhandled, exceptions) = self._event(
- "onQuietListEnd", self.addons + channel.addons, channel=channel, endmsg=extinfo)
+ self._event("onRecv", channel.addons, **data)
+ self._event(
+ "onQuietListEnd", self.addons + channel.addons, channel=channel, endmsg=extinfo, data=data)
elif cmd in (495, 384, 385, 386, 468, 470, 366, 315, 482, 484, 953, 368, 482, 349, 940, 911, 489, 490, 492, 520, 530): # Channels which appear in params
for param in params.split():
if len(param) and param[0] in self.supports["CHANTYPES"]:
channel = self.channel(param)
- self._event(
- "onRecv", channel.addons, line=line, **data)
+ self._event("onRecv", channel.addons, **data)
elif type(cmd) == int:
- (handled, unhandled, exceptions) = self._event("on%03d" %
- cmd, self.addons, line=line, origin=origin, target=target, params=params, extinfo=extinfo)
- else:
- (handled, unhandled, exceptions) = self._event("on%s" %
- cmd, self.addons, line=line, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo)
+ self._event(
+ "on%03d" % cmd, self.addons, line=line, origin=origin,
+ target=target, params=params, extinfo=extinfo, data=data)
+ elif not (cmd in ("PING", "PONG") and self.quietpingpong):
+ self._event(
+ "on%s" % cmd, self.addons, line=line, origin=origin,
+ cmd=cmd, target=target, params=params, extinfo=extinfo, data=data)
if cmd in (384, 403, 405, 471, 473, 474, 475, 476, 520, 477, 489, 495): # Channel Join denied
try:
@@ -1305,18 +1393,19 @@ class Connection(object):
channel._joining.notify()
# Handle events that were not handled.
- self._event("onUnhandled", unhandled, line=line, origin=origin,
- cmd=cmd, target=target, params=params, extinfo=extinfo)
+ # if not (cmd in ("PING", "PONG") and self.quietpingpong):
+ # self._event("onUnhandled", unhandled, line=line, origin=origin, cmd=cmd, target=target, params=params, extinfo=extinfo)
def _trynick(self):
(q, s) = divmod(self.trynick, len(self.nick))
nick = self.nick[s]
if q > 0:
nick = "%s%d" % (nick, q)
- self._send("NICK %s" % nick)
+ self._send(u"NICK %s" % nick)
self.trynick += 1
- def _recvhandler(self):
+ def _recvhandler(self, server=None, port=None, ssl=None, ipv6=None):
+ pingreq = None
# Enforce that this function must only be run from within
# self._sendhandlerthread.
if currentThread() != self._recvhandlerthread:
@@ -1360,9 +1449,9 @@ class Connection(object):
# Attempt initial registration.
nick = self.nick[0]
if self.passwd:
- self._send("PASS %s" % self.passwd)
+ self._send(u"PASS %s" % self.passwd)
self._trynick()
- self._send("USER %s * * :%s" %
+ self._send(u"USER %s * * :%s" %
(self.username.split("\n")[0].rstrip(), self.realname.split("\n")[0].rstrip()))
# Initialize buffers
@@ -1372,6 +1461,13 @@ class Connection(object):
while True: # Main loop of IRC connection.
while len(linebuf) == 0: # Need Moar Data
read = self._connection.recv(512)
+ with self._sendline:
+ 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._outgoing.append(pingreq)
+ self._sendline.notify()
# If read was empty, connection is terminated.
if read == "":
@@ -1386,6 +1482,12 @@ class Connection(object):
readbuf = readbuf[lastlf + 1:]
line = string.rstrip(linebuf.pop(0))
+ try:
+ line = line.decode("utf8")
+ except UnicodeDecodeError:
+ # Attempt to figure encoding
+ charset = chardet.detect(line)['encoding']
+ line = line.decode(charset)
self._procrecvline(line)
except SystemExit: # Connection lost normally.
@@ -1415,8 +1517,9 @@ class Connection(object):
self._outgoing.clear()
self._sendline.notify()
with self.lock:
- (handled, unhandled, exceptions) = self._event("onDisconnect", self.addons + reduce(
+ self._event("onDisconnect", self.addons + reduce(
lambda x, y: x + y, [chan.addons for chan in self.channels], []), expected=self._quitexpected)
+
self._init()
# Notify _outgoingthread that the connection has been
@@ -1447,52 +1550,60 @@ class Connection(object):
finally:
self.logwrite("### Log session ended")
- (handled, unhandled, exceptions) = self._event("onSessionClose", self.addons +
- reduce(lambda x, y: x + y, [chan.addons for chan in self.channels], []))
+ self._event("onSessionClose", self.addons + reduce(
+ lambda x, y: x + y, [chan.addons for chan in self.channels], []))
# Tell _sendhandler to quit
with self._sendline:
self._outgoing.append("quit")
self._sendline.notify()
- def _send(self, line, origin=None):
+ def _send(self, line, origin=None, T=None):
if "\r" in line or "\n" in line:
raise InvalidCharacter
cmd = line.split(" ")[0].upper()
- T = time.time()
- if cmd == "PRIVMSG":
- # Hard-coding a throttling mechanism for PRIVMSGs only here. Will later build support for custom throttlers.
- # The throttle will be triggered when it attempts to send a sixth PRIVMSG in a four-second interval.
- # When the throttle is active, PRIVMSGs will be sent in at least one-second intervals.
- # The throttle is deactivated when three seconds elapse without
- # sending a PRIVMSG.
- while len(self.throttledata) and self.throttledata[0] < T - 4:
- del self.throttledata[0]
- if not self.throttled:
- if len(self.throttledata) >= 5:
- self.throttled = True
- T = self.throttledata[-1] + 1
- else:
- if len(self.throttledata) == 0 or self.throttledata[-1] < T - 2:
- self.throttled = False
+ if T == None:
+ T = time.time()
+ if cmd == "PRIVMSG":
+ # Hard-coding a throttling mechanism for PRIVMSGs only here. Will later build support for custom throttlers.
+ # The throttle will be triggered when it attempts to send a sixth PRIVMSG in a four-second interval.
+ # When the throttle is active, PRIVMSGs will be sent in at least one-second intervals.
+ # The throttle is deactivated when three seconds elapse without
+ # sending a PRIVMSG.
+ while len(self.throttledata) and self.throttledata[0] < T - 4:
+ del self.throttledata[0]
+ if not self.throttled:
+ if len(self.throttledata) >= 5:
+ self.throttled = True
+ T = self.throttledata[-1] + 1
else:
- T = max(T, self.throttledata[-1] + 1)
- self.throttledata.append(T)
+ if len(self.throttledata) == 0 or self.throttledata[-1] < T - 2:
+ self.throttled = False
+ else:
+ T = max(T, self.throttledata[-1] + 1)
+ self.throttledata.append(T)
with self._sendline:
self._outgoing.append((T, line, origin))
self._sendline.notify()
+ def _cancelsend(self, line, origin=None, T=None):
+ with self._sendline:
+ self._outgoing.remove((T, line, origin))
+ self._sendline.notify()
+
def _procsendline(self, line, origin=None):
- match = re.findall(_ircsendmatch, line)
+ match = re.findall(_ircmatch, line)
if len(match) == 0:
return
- (cmd, target, params, extinfo) = match[0]
+ (null, username, host, cmd, target, params, extinfo) = match[0]
cmd = cmd.upper()
with self.lock:
if cmd == "QUIT":
self._quitexpected = True
- self._connection.send("%s\n" % line)
+ if self._connection == None:
+ return
+ origline = line
# Modify line if it contains a password so that the password is not
# logged or sent to any potentially untrustworthy addons
@@ -1612,9 +1723,11 @@ class Connection(object):
elif cmd.upper() == "IDENTIFY":
target = "********"
line = "%s %s" % (cmd, target)
- self._event("onSend", self.addons, origin=origin, line=line,
- cmd=cmd, target=target, params=params, extinfo=extinfo)
- self.logwrite(">>> %s" % line)
+ if not (cmd in ("PING", "PONG") and self.quietpingpong):
+ self._event("onSend", self.addons, origin=origin, line=line,
+ cmd=cmd, target=target, params=params, extinfo=extinfo)
+ self.logwrite(">>> %s" % line)
+ self._connection.send("%s\n" % origline.encode('utf8'))
def _sendhandler(self):
# Enforce that this function must only be run from within
@@ -1624,11 +1737,6 @@ class Connection(object):
try:
while True:
- # Wait for one of the following:
- # (1) An item placed into _outgoing
- # (2) Connection is lost
- # (3) self._recvhandlerthread is set to None
-
with self._sendline:
if "quit" in self._outgoing:
sys.exit()
@@ -1636,11 +1744,19 @@ class Connection(object):
if len(self._outgoing):
T, line, origin = min(self._outgoing)
if T > S:
+ # The next item in the queue (by time) is still
+ # scheduled to be sent later. We wait until then,
+ # or when another item is put into the queue,
+ # whichever is first.
self._sendline.wait(T - S)
continue
else:
+ # The next item in the queue (by time) should be
+ # sent now.
self._outgoing.remove((T, line, origin))
else:
+ # The queue is empty, so we will wait until something
+ # is put into the queue, then restart the while loop.
self._sendline.wait()
continue
@@ -1717,43 +1833,43 @@ class Connection(object):
# locals()
def oper(self, name, passwd, origin=None):
- self.raw("OPER %s %s" %
- (re.findall("^([^\r\n\\s]*)", name)[0], re.findall("^([^\r\n\\s]*)", passwd)[0]), origin=origin)
+ self._send(u"OPER %s %s" %
+ (re.findall("^([^\r\n\\s]*)", name)[0], re.findall("^([^\r\n\\s]*)", passwd)[0]), origin=origin)
def list(self, params="", origin=None):
if len(re.findall("^([^\r\n\\s]*)", params)[0]):
- self.raw("LIST %s" %
- (re.findall("^([^\r\n\\s]*)", params)[0]), origin=origin)
+ self._send(u"LIST %s" %
+ (re.findall("^([^\r\n\\s]*)", params)[0]), origin=origin)
else:
- self.raw("LIST", origin=origin)
+ self._send(u"LIST", origin=origin)
def getmotd(self, target="", origin=None):
if len(re.findall("^([^\r\n\\s]*)", target)[0]):
- self.raw("MOTD %s" %
- (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
+ self._send(u"MOTD %s" %
+ (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
else:
- self.raw("MOTD", origin=origin)
+ self._send(u"MOTD", origin=origin)
def version(self, target="", origin=None):
if len(re.findall("^([^\r\n\\s]*)", target)[0]):
- self.raw("VERSION %s" %
- (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
+ self._send(u"VERSION %s" %
+ (re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
else:
- self.raw("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.raw("STATS %s %s" %
- (query, re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
+ self._send(u"STATS %s %s" %
+ (query, re.findall("^([^\r\n\\s]*)", target)[0]), origin=origin)
else:
- self.raw("STATS %s" % query, origin=origin)
+ self._send(u"STATS %s" % query, origin=origin)
def quit(self, msg="", origin=None):
if len(re.findall("^([^\r\n]*)", msg)[0]):
- self._send("QUIT :%s" %
+ self._send(u"QUIT :%s" %
re.findall("^([^\r\n]*)", msg)[0], origin=origin)
else:
- self._send("QUIT", origin=origin)
+ self._send(u"QUIT", origin=origin)
def ctcpversion(self):
reply = []
@@ -1780,7 +1896,7 @@ class Connection(object):
def raw(self, line, origin=None):
self._send(line, origin=origin)
- def user(self, nick):
+ def user(self, nick, init=False):
if self.supports.get("CASEMAPPING", "rfc1459") == "ascii":
users = [
user for user in self.users if user.nick.lower() == nick.lower()]
@@ -1788,6 +1904,8 @@ class Connection(object):
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)
@@ -1796,7 +1914,7 @@ class Connection(object):
str(t).rjust(2, "0") for t in time.localtime()[0:6]])
return user
- def channel(self, name):
+ def channel(self, name, init=False):
if self.supports.get("CASEMAPPING", "rfc1459") == "ascii":
channels = [
chan for chan in self.channels if chan.name.lower() == name.lower()]
@@ -1804,6 +1922,8 @@ class Connection(object):
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, [
@@ -1812,22 +1932,35 @@ class Connection(object):
self.channels.append(chan)
return chan
+ def __getitem__(self, item):
+ chantypes = self.supports.get("CHANTYPES", "&#+!")
+ 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."
+
class Channel(object):
- def __init__(self, name, context):
+ def __init__(self, name, context, key=None):
chantypes = context.supports.get("CHANTYPES", "&#+!")
if not re.match(_chanmatch % re.escape(chantypes), name):
raise InvalidName, repr(name)
self.name = name
self.context = context
+ self.key = key
+ self._init()
+
+ def _init(self):
self.addons = []
self.topic = ""
self.topicsetby = ""
self.topictime = ()
self.topicmod = ""
self.modes = {}
- self.users = []
+ self.users = UserList(context=self.context)
self.created = None
self.lock = Lock()
self._joinrequested = False
@@ -1841,24 +1974,24 @@ 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("PRIVMSG %s%s :%s" %
+ self.context._send(u"PRIVMSG %s%s :%s" %
(target, self.name, line), origin=origin)
def who(self, origin=None):
- self.context._send("WHO %s" % (self.name), origin=origin)
+ self.context._send(u"WHO %s" % (self.name), origin=origin)
def names(self, origin=None):
- self.context._send("NAMES %s" % (self.name), origin=origin)
+ self.context._send(u"NAMES %s" % (self.name), origin=origin)
def notice(self, msg, target="", origin=None):
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("NOTICE %s%s :%s" %
+ self.context._send(u"NOTICE %s%s :%s" %
(target, self.name, line), origin=origin)
def settopic(self, msg, origin=None):
- self.context._send("TOPIC %s :%s" %
+ self.context._send(u"TOPIC %s :%s" %
(self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
def ctcp(self, act, msg="", origin=None):
@@ -1890,9 +2023,9 @@ class Channel(object):
self._partrequested = True
if len(re.findall("^([^\r\n]*)", msg)[0]):
self.context._send(
- "PART %s :%s" % (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
+ u"PART %s :%s" % (self.name, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
else:
- self.context._send("PART %s" % self.name, origin=origin)
+ self.context._send(u"PART %s" % self.name, origin=origin)
# Anticipated Numeric Replies:
@@ -1922,7 +2055,7 @@ class Channel(object):
user) == User else re.findall("^([^\r\n\\s]*)", user)[0]
if nickname == "":
raise InvalidName
- self.context._send("INVITE %s %s" %
+ self.context._send(u"INVITE %s %s" %
(nickname, self.name), origin=origin)
def join(self, key="", blocking=False, timeout=30, origin=None):
@@ -1937,9 +2070,9 @@ class Channel(object):
self._joinrequested = True
if len(re.findall("^([^\r\n\\s]*)", key)[0]):
self.context._send(
- "JOIN %s %s" % (self.name, re.findall("^([^\r\n\\s]*)", key)[0]), origin=origin)
+ u"JOIN %s %s" % (self.name, re.findall("^([^\r\n\\s]*)", key)[0]), origin=origin)
else:
- self.context._send("JOIN %s" % self.name, origin=origin)
+ self.context._send(u"JOIN %s" % self.name, origin=origin)
# Anticipated Numeric Replies:
@@ -1973,14 +2106,17 @@ class Channel(object):
if nickname == "":
raise InvalidName
if len(re.findall("^([^\r\n]*)", msg)[0]):
- self.context._send("KICK %s %s :%s" %
+ self.context._send(u"KICK %s %s :%s" %
(self.name, nickname, re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
else:
- self.context._send("KICK %s %s" %
+ self.context._send(u"KICK %s %s" %
(self.name, nickname), origin=origin)
def __repr__(self):
- return "<Channel: " + self.name + "@" + self.context.server + "/" + str(self.context.port) + ">"
+ return (u"<Channel: %s@%s/%d>" % (self.name, self.context.server, self.context.port)).encode("utf8")
+
+ def __contains__(self, item):
+ return item in self.users
class User(object):
@@ -1989,10 +2125,13 @@ class User(object):
if not re.match(_nickmatch, nick):
raise InvalidName
self.nick = nick
+ self.context = context
+ self._init()
+
+ def _init(self):
self.username = ""
self.host = ""
- self.channels = []
- self.context = context
+ self.channels = ChanList(context=self.context)
self.modes = ""
self.snomask = ""
self.server = None
@@ -2005,24 +2144,24 @@ class User(object):
self.away = None
def __repr__(self):
- return "<User: %(nick)s!%(username)s@%(host)s>" % vars(self)
+ return (u"<User: %(nick)s!%(username)s@%(host)s>" % vars(self)).encode("utf8")
def msg(self, msg, origin=None):
for line in re.findall("([^\r\n]+)", msg):
- self.context._send("PRIVMSG %s :%s" %
+ 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("NOTICE %s :%s" %
+ 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]):
- self.msg("\01%s %s\01" %
+ self.msg(u"\01%s %s\01" %
(act.upper(), re.findall("^([^\r\n]*)", msg)[0]), origin=origin)
else:
- self.msg("\01%s\01" % act.upper())
+ self.msg(u"\01%s\01" % act.upper())
def ctcpreply(self, act, msg="", origin=None):
if len(re.findall("^([^\r\n]*)", msg)[0]):
@@ -2033,3 +2172,167 @@ class User(object):
def me(self, msg="", origin=None):
self.ctcp("ACTION", msg, origin=origin)
+
+
+class Config(object):
+
+ def __init__(self, **kwargs):
+ self.__dict__.update(kwargs)
+
+
+class ChanList(list):
+
+ 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:
+ chanlist = []
+ for channel in iterable:
+ if type(channel) == Channel:
+ chanlist.append(channel)
+ 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)
+ else:
+ list.__init__(self)
+
+ def append(self, item):
+ if type(item) in (str, unicode):
+ if self.context:
+ list.append(self, 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."
+ list.append(self, item)
+
+ def insert(self, index, item):
+ if type(item) in (str, unicode):
+ if self.context:
+ list.insert(self, index, 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."
+ list.insert(self, index, 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)
+
+ 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)
+ else:
+ 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)
+ else:
+ 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)
+
+ def __str__(self):
+ return ",".join([channel.name for channel in self])
+
+
+class UserList(list):
+
+ 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:
+ userlist = []
+ for user in iterable:
+ if type(user) == User:
+ 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)
+ else:
+ list.__init__(self)
+
+ def append(self, item):
+ if type(item) in (str, unicode):
+ if self.context:
+ list.append(self, 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."
+ list.append(self, item)
+
+ def insert(self, index, item):
+ if type(item) in (str, unicode):
+ if self.context:
+ list.insert(self, index, 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."
+ list.insert(self, index, 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)
+
+ 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)
+
+ def __str__(self):
+ return ",".join([user.nick for user in self])