1 # -*- test-case-name: twisted.words.test.test_irc -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 Internet Relay Chat Protocol for client and server.
11 The way the IRCClient class works here encourages people to implement
12 IRC clients by subclassing the ephemeral protocol class, and it tends
13 to end up with way more state than it should for an object which will
14 be destroyed as soon as the TCP transport drops. Someone oughta do
15 something about that, ya know?
17 The DCC support needs to have more hooks for the client for it to be
18 able to ask the user things like "Do you want to accept this session?"
19 and "Transfer #2 is 67% done." and otherwise manage the DCC sessions.
21 Test coverage needs to be better.
23 @var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC
28 @see: RFC 1459: Internet Relay Chat Protocol
29 @see: RFC 2812: Internet Relay Chat: Client Protocol
30 @see: U{The Client-To-Client-Protocol
31 <http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
34 import errno, os, random, re, stat, struct, sys, time, types, traceback
40 from twisted.internet import reactor, protocol, task
41 from twisted.persisted import styles
42 from twisted.protocols import basic
43 from twisted.python import log, reflect, text
44 from twisted.python.compat import set
52 # This includes the CRLF terminator characters.
53 MAX_COMMAND_LENGTH = 512
55 CHANNEL_PREFIXES = '&#!+'
57 class IRCBadMessage(Exception):
60 class IRCPasswordMismatch(Exception):
65 class IRCBadModes(ValueError):
67 A malformed mode was encountered while attempting to parse a mode string.
73 """Breaks a message from an IRC server into its prefix, command, and arguments.
78 raise IRCBadMessage("Empty line.")
80 prefix, s = s[1:].split(' ', 1)
81 if s.find(' :') != -1:
82 s, trailing = s.split(' :', 1)
88 return prefix, command, args
92 def split(str, length=80):
94 Split a string into multiple lines.
96 Whitespace near C{str[length]} will be preferred as a breaking point.
97 C{"\\n"} will also be used as a breaking point.
99 @param str: The string to split.
102 @param length: The maximum length which will be allowed for any string in
106 @return: C{list} of C{str}
109 for line in str.split('\n')
110 for chunk in textwrap.wrap(line, length)]
113 def _intOrDefault(value, default=None):
115 Convert a value to an integer if possible.
117 @rtype: C{int} or type of L{default}
118 @return: An integer when C{value} can be converted to an integer,
119 otherwise return C{default}
124 except (TypeError, ValueError):
130 class UnhandledCommand(RuntimeError):
132 A command dispatcher could not locate an appropriate command handler.
137 class _CommandDispatcherMixin(object):
139 Dispatch commands to handlers based on their name.
141 Command handler names should be of the form C{prefix_commandName},
142 where C{prefix} is the value specified by L{prefix}, and must
143 accept the parameters as given to L{dispatch}.
145 Attempting to mix this in more than once for a single class will cause
146 strange behaviour, due to L{prefix} being overwritten.
149 @ivar prefix: Command handler prefix, used to locate handler attributes
153 def dispatch(self, commandName, *args):
155 Perform actual command dispatch.
157 def _getMethodName(command):
158 return '%s_%s' % (self.prefix, command)
160 def _getMethod(name):
161 return getattr(self, _getMethodName(name), None)
163 method = _getMethod(commandName)
164 if method is not None:
167 method = _getMethod('unknown')
169 raise UnhandledCommand("No handler for %r could be found" % (_getMethodName(commandName),))
170 return method(commandName, *args)
176 def parseModes(modes, params, paramModes=('', '')):
178 Parse an IRC mode string.
180 The mode string is parsed into two lists of mode changes (added and
181 removed), with each mode change represented as C{(mode, param)} where mode
182 is the mode character, and param is the parameter passed for that mode, or
183 C{None} if no parameter is required.
186 @param modes: Modes string to parse.
188 @type params: C{list}
189 @param params: Parameters specified along with L{modes}.
191 @type paramModes: C{(str, str)}
192 @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take
193 parameters when added or removed.
195 @returns: Two lists of mode changes, one for modes added and the other for
196 modes removed respectively, mode changes in each list are represented as
200 raise IRCBadModes('Empty mode string')
202 if modes[0] not in '+-':
203 raise IRCBadModes('Malformed modes string: %r' % (modes,))
212 raise IRCBadModes('Empty mode sequence: %r' % (modes,))
213 direction = '+-'.index(ch)
217 if ch in paramModes[direction]:
219 param = params.pop(0)
221 raise IRCBadModes('Not enough parameters: %r' % (ch,))
222 changes[direction].append((ch, param))
226 raise IRCBadModes('Too many parameters: %r %r' % (modes, params))
229 raise IRCBadModes('Empty mode sequence: %r' % (modes,))
235 class IRC(protocol.Protocol):
237 Internet Relay Chat server protocol.
245 def connectionMade(self):
247 if self.hostname is None:
248 self.hostname = socket.getfqdn()
251 def sendLine(self, line):
252 if self.encoding is not None:
253 if isinstance(line, unicode):
254 line = line.encode(self.encoding)
255 self.transport.write("%s%s%s" % (line, CR, LF))
258 def sendMessage(self, command, *parameter_list, **prefix):
260 Send a line formatted as an IRC message.
262 First argument is the command, all subsequent arguments are parameters
263 to that command. If a prefix is desired, it may be specified with the
264 keyword argument 'prefix'.
268 raise ValueError, "IRC message requires a command."
270 if ' ' in command or command[0] == ':':
271 # Not the ONLY way to screw up, but provides a little
272 # sanity checking to catch likely dumb mistakes.
273 raise ValueError, "Somebody screwed up, 'cuz this doesn't" \
274 " look like a command to me: %s" % command
276 line = string.join([command] + list(parameter_list))
277 if prefix.has_key('prefix'):
278 line = ":%s %s" % (prefix['prefix'], line)
281 if len(parameter_list) > 15:
282 log.msg("Message has %d parameters (RFC allows 15):\n%s" %
283 (len(parameter_list), line))
286 def dataReceived(self, data):
288 This hack is to support mIRC, which sends LF only, even though the RFC
289 says CRLF. (Also, the flexibility of LineReceiver to turn "line mode"
290 on and off was not required.)
292 lines = (self.buffer + data).split(LF)
293 # Put the (possibly empty) element after the last LF back in the
295 self.buffer = lines.pop()
299 # This is a blank line, at best.
303 prefix, command, params = parsemsg(line)
304 # mIRC is a big pile of doo-doo
305 command = command.upper()
306 # DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
308 self.handleCommand(command, prefix, params)
311 def handleCommand(self, command, prefix, params):
313 Determine the function to call for the given command and call it with
316 method = getattr(self, "irc_%s" % command, None)
318 if method is not None:
319 method(prefix, params)
321 self.irc_unknown(prefix, command, params)
326 def irc_unknown(self, prefix, command, params):
328 Called by L{handleCommand} on a command that doesn't have a defined
329 handler. Subclasses should override this method.
331 raise NotImplementedError(command, prefix, params)
335 def privmsg(self, sender, recip, message):
337 Send a message to a channel or user
339 @type sender: C{str} or C{unicode}
340 @param sender: Who is sending this message. Should be of the form
341 username!ident@hostmask (unless you know better!).
343 @type recip: C{str} or C{unicode}
344 @param recip: The recipient of this message. If a channel, it must
345 start with a channel prefix.
347 @type message: C{str} or C{unicode}
348 @param message: The message being sent.
350 self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message)))
353 def notice(self, sender, recip, message):
355 Send a "notice" to a channel or user.
357 Notices differ from privmsgs in that the RFC claims they are different.
358 Robots are supposed to send notices and not respond to them. Clients
359 typically display notices differently from privmsgs.
361 @type sender: C{str} or C{unicode}
362 @param sender: Who is sending this message. Should be of the form
363 username!ident@hostmask (unless you know better!).
365 @type recip: C{str} or C{unicode}
366 @param recip: The recipient of this message. If a channel, it must
367 start with a channel prefix.
369 @type message: C{str} or C{unicode}
370 @param message: The message being sent.
372 self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message))
375 def action(self, sender, recip, message):
377 Send an action to a channel or user.
379 @type sender: C{str} or C{unicode}
380 @param sender: Who is sending this message. Should be of the form
381 username!ident@hostmask (unless you know better!).
383 @type recip: C{str} or C{unicode}
384 @param recip: The recipient of this message. If a channel, it must
385 start with a channel prefix.
387 @type message: C{str} or C{unicode}
388 @param message: The action being sent.
390 self.sendLine(":%s ACTION %s :%s" % (sender, recip, message))
393 def topic(self, user, channel, topic, author=None):
395 Send the topic to a user.
397 @type user: C{str} or C{unicode}
398 @param user: The user receiving the topic. Only their nick name, not
401 @type channel: C{str} or C{unicode}
402 @param channel: The channel for which this is the topic.
404 @type topic: C{str} or C{unicode} or C{None}
405 @param topic: The topic string, unquoted, or None if there is no topic.
407 @type author: C{str} or C{unicode}
408 @param author: If the topic is being changed, the full username and
409 hostmask of the person changing it.
413 self.sendLine(':%s %s %s %s :%s' % (
414 self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.'))
416 self.sendLine(":%s %s %s %s :%s" % (
417 self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)))
419 self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)))
422 def topicAuthor(self, user, channel, author, date):
424 Send the author of and time at which a topic was set for the given
427 This sends a 333 reply message, which is not part of the IRC RFC.
429 @type user: C{str} or C{unicode}
430 @param user: The user receiving the topic. Only their nick name, not
433 @type channel: C{str} or C{unicode}
434 @param channel: The channel for which this information is relevant.
436 @type author: C{str} or C{unicode}
437 @param author: The nickname (without hostmask) of the user who last set
441 @param date: A POSIX timestamp (number of seconds since the epoch) at
442 which the topic was last set.
444 self.sendLine(':%s %d %s %s %s %d' % (
445 self.hostname, 333, user, channel, author, date))
448 def names(self, user, channel, names):
450 Send the names of a channel's participants to a user.
452 @type user: C{str} or C{unicode}
453 @param user: The user receiving the name list. Only their nick name,
454 not the full hostmask.
456 @type channel: C{str} or C{unicode}
457 @param channel: The channel for which this is the namelist.
459 @type names: C{list} of C{str} or C{unicode}
460 @param names: The names to send.
462 # XXX If unicode is given, these limits are not quite correct
463 prefixLength = len(channel) + len(user) + 10
464 namesLength = 512 - prefixLength
469 if count + len(n) + 1 > namesLength:
470 self.sendLine(":%s %s %s = %s :%s" % (
471 self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
478 self.sendLine(":%s %s %s = %s :%s" % (
479 self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
480 self.sendLine(":%s %s %s %s :End of /NAMES list" % (
481 self.hostname, RPL_ENDOFNAMES, user, channel))
484 def who(self, user, channel, memberInfo):
486 Send a list of users participating in a channel.
488 @type user: C{str} or C{unicode}
489 @param user: The user receiving this member information. Only their
490 nick name, not the full hostmask.
492 @type channel: C{str} or C{unicode}
493 @param channel: The channel for which this is the member information.
495 @type memberInfo: C{list} of C{tuples}
496 @param memberInfo: For each member of the given channel, a 7-tuple
497 containing their username, their hostmask, the server to which they
498 are connected, their nickname, the letter "H" or "G" (standing for
499 "Here" or "Gone"), the hopcount from C{user} to this member, and
500 this member's real name.
502 for info in memberInfo:
503 (username, hostmask, server, nickname, flag, hops, realName) = info
504 assert flag in ("H", "G")
505 self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % (
506 self.hostname, RPL_WHOREPLY, user, channel,
507 username, hostmask, server, nickname, flag, hops, realName))
509 self.sendLine(":%s %s %s %s :End of /WHO list." % (
510 self.hostname, RPL_ENDOFWHO, user, channel))
513 def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels):
515 Send information about the state of a particular user.
517 @type user: C{str} or C{unicode}
518 @param user: The user receiving this information. Only their nick name,
519 not the full hostmask.
521 @type nick: C{str} or C{unicode}
522 @param nick: The nickname of the user this information describes.
524 @type username: C{str} or C{unicode}
525 @param username: The user's username (eg, ident response)
527 @type hostname: C{str}
528 @param hostname: The user's hostmask
530 @type realName: C{str} or C{unicode}
531 @param realName: The user's real name
533 @type server: C{str} or C{unicode}
534 @param server: The name of the server to which the user is connected
536 @type serverInfo: C{str} or C{unicode}
537 @param serverInfo: A descriptive string about that server
540 @param oper: Indicates whether the user is an IRC operator
543 @param idle: The number of seconds since the user last sent a message
546 @param signOn: A POSIX timestamp (number of seconds since the epoch)
547 indicating the time the user signed on
549 @type channels: C{list} of C{str} or C{unicode}
550 @param channels: A list of the channels which the user is participating in
552 self.sendLine(":%s %s %s %s %s %s * :%s" % (
553 self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName))
554 self.sendLine(":%s %s %s %s %s :%s" % (
555 self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo))
557 self.sendLine(":%s %s %s %s :is an IRC operator" % (
558 self.hostname, RPL_WHOISOPERATOR, user, nick))
559 self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % (
560 self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn))
561 self.sendLine(":%s %s %s %s :%s" % (
562 self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels)))
563 self.sendLine(":%s %s %s %s :End of WHOIS list." % (
564 self.hostname, RPL_ENDOFWHOIS, user, nick))
567 def join(self, who, where):
571 @type who: C{str} or C{unicode}
572 @param who: The name of the user joining. Should be of the form
573 username!ident@hostmask (unless you know better!).
575 @type where: C{str} or C{unicode}
576 @param where: The channel the user is joining.
578 self.sendLine(":%s JOIN %s" % (who, where))
581 def part(self, who, where, reason=None):
585 @type who: C{str} or C{unicode}
586 @param who: The name of the user joining. Should be of the form
587 username!ident@hostmask (unless you know better!).
589 @type where: C{str} or C{unicode}
590 @param where: The channel the user is joining.
592 @type reason: C{str} or C{unicode}
593 @param reason: A string describing the misery which caused this poor
597 self.sendLine(":%s PART %s :%s" % (who, where, reason))
599 self.sendLine(":%s PART %s" % (who, where))
602 def channelMode(self, user, channel, mode, *args):
604 Send information about the mode of a channel.
606 @type user: C{str} or C{unicode}
607 @param user: The user receiving the name list. Only their nick name,
608 not the full hostmask.
610 @type channel: C{str} or C{unicode}
611 @param channel: The channel for which this is the namelist.
614 @param mode: A string describing this channel's modes.
616 @param args: Any additional arguments required by the modes.
618 self.sendLine(":%s %s %s %s %s %s" % (
619 self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args)))
623 class ServerSupportedFeatures(_CommandDispatcherMixin):
625 Handle ISUPPORT messages.
627 Feature names match those in the ISUPPORT RFC draft identically.
629 Information regarding the specifics of ISUPPORT was gleaned from
630 <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>.
637 'CHANTYPES': tuple('#&'),
640 'PREFIX': self._parsePrefixParam('(ovh)@+%'),
641 # The ISUPPORT draft explicitly says that there is no default for
642 # CHANMODES, but we're defaulting it here to handle the case where
643 # the IRC server doesn't send us any ISUPPORT information, since
644 # IRCClient.getChannelModeParams relies on this value.
645 'CHANMODES': self._parseChanModesParam(['b', '', 'lk'])}
648 def _splitParamArgs(cls, params, valueProcessor=None):
650 Split ISUPPORT parameter arguments.
652 Values can optionally be processed by C{valueProcessor}.
656 >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2'])
657 (('A', '1'), ('B', '2'))
659 @type params: C{iterable} of C{str}
661 @type valueProcessor: C{callable} taking {str}
662 @param valueProcessor: Callable to process argument values, or C{None}
663 to perform no processing
665 @rtype: C{list} of C{(str, object)}
666 @return: Sequence of C{(name, processedValue)}
668 if valueProcessor is None:
669 valueProcessor = lambda x: x
675 a, b = param.split(':', 1)
676 yield a, valueProcessor(b)
677 return list(_parse())
678 _splitParamArgs = classmethod(_splitParamArgs)
681 def _unescapeParamValue(cls, value):
683 Unescape an ISUPPORT parameter.
685 The only form of supported escape is C{\\xHH}, where HH must be a valid
686 2-digit hexadecimal number.
691 parts = value.split('\\x')
692 # The first part can never be preceeded by the escape.
695 octet, rest = s[:2], s[2:]
697 octet = int(octet, 16)
699 raise ValueError('Invalid hex octet: %r' % (octet,))
700 yield chr(octet) + rest
702 if '\\x' not in value:
704 return ''.join(_unescape())
705 _unescapeParamValue = classmethod(_unescapeParamValue)
708 def _splitParam(cls, param):
710 Split an ISUPPORT parameter.
714 @rtype: C{(str, list)}
715 @return C{(key, arguments)}
719 key, value = param.split('=', 1)
720 return key, map(cls._unescapeParamValue, value.split(','))
721 _splitParam = classmethod(_splitParam)
724 def _parsePrefixParam(cls, prefix):
726 Parse the ISUPPORT "PREFIX" parameter.
728 The order in which the parameter arguments appear is significant, the
729 earlier a mode appears the more privileges it gives.
731 @rtype: C{dict} mapping C{str} to C{(str, int)}
732 @return: A dictionary mapping a mode character to a two-tuple of
733 C({symbol, priority)}, the lower a priority (the lowest being
734 C{0}) the more privileges it gives
738 if prefix[0] != '(' and ')' not in prefix:
739 raise ValueError('Malformed PREFIX parameter')
740 modes, symbols = prefix.split(')', 1)
741 symbols = zip(symbols, xrange(len(symbols)))
743 return dict(zip(modes, symbols))
744 _parsePrefixParam = classmethod(_parsePrefixParam)
747 def _parseChanModesParam(self, params):
749 Parse the ISUPPORT "CHANMODES" parameter.
751 See L{isupport_CHANMODES} for a detailed explanation of this parameter.
753 names = ('addressModes', 'param', 'setParam', 'noParam')
754 if len(params) > len(names):
756 'Expecting a maximum of %d channel mode parameters, got %d' % (
757 len(names), len(params)))
758 items = map(lambda key, value: (key, value or ''), names, params)
760 _parseChanModesParam = classmethod(_parseChanModesParam)
763 def getFeature(self, feature, default=None):
765 Get a server supported feature's value.
767 A feature with the value C{None} is equivalent to the feature being
770 @type feature: C{str}
771 @param feature: Feature name
773 @type default: C{object}
774 @param default: The value to default to, assuming that C{feature}
777 @return: Feature value
779 return self._features.get(feature, default)
782 def hasFeature(self, feature):
784 Determine whether a feature is supported or not.
788 return self.getFeature(feature) is not None
791 def parse(self, params):
793 Parse ISUPPORT parameters.
795 If an unknown parameter is encountered, it is simply added to the
796 dictionary, keyed by its name, as a tuple of the parameters provided.
798 @type params: C{iterable} of C{str}
799 @param params: Iterable of ISUPPORT parameters to parse
802 key, value = self._splitParam(param)
803 if key.startswith('-'):
804 self._features.pop(key[1:], None)
806 self._features[key] = self.dispatch(key, value)
809 def isupport_unknown(self, command, params):
811 Unknown ISUPPORT parameter.
816 def isupport_CHANLIMIT(self, params):
818 The maximum number of each channel type a user may join.
820 return self._splitParamArgs(params, _intOrDefault)
823 def isupport_CHANMODES(self, params):
825 Available channel modes.
827 There are 4 categories of channel mode::
829 addressModes - Modes that add or remove an address to or from a
830 list, these modes always take a parameter.
832 param - Modes that change a setting on a channel, these modes
833 always take a parameter.
835 setParam - Modes that change a setting on a channel, these modes
836 only take a parameter when being set.
838 noParam - Modes that change a setting on a channel, these modes
839 never take a parameter.
842 return self._parseChanModesParam(params)
844 return self.getFeature('CHANMODES')
847 def isupport_CHANNELLEN(self, params):
849 Maximum length of a channel name a client may create.
851 return _intOrDefault(params[0], self.getFeature('CHANNELLEN'))
854 def isupport_CHANTYPES(self, params):
856 Valid channel prefixes.
858 return tuple(params[0])
861 def isupport_EXCEPTS(self, params):
863 Mode character for "ban exceptions".
865 The presence of this parameter indicates that the server supports
868 return params[0] or 'e'
871 def isupport_IDCHAN(self, params):
873 Safe channel identifiers.
875 The presence of this parameter indicates that the server supports
878 return self._splitParamArgs(params)
881 def isupport_INVEX(self, params):
883 Mode character for "invite exceptions".
885 The presence of this parameter indicates that the server supports
888 return params[0] or 'I'
891 def isupport_KICKLEN(self, params):
893 Maximum length of a kick message a client may provide.
895 return _intOrDefault(params[0])
898 def isupport_MAXLIST(self, params):
900 Maximum number of "list modes" a client may set on a channel at once.
902 List modes are identified by the "addressModes" key in CHANMODES.
904 return self._splitParamArgs(params, _intOrDefault)
907 def isupport_MODES(self, params):
909 Maximum number of modes accepting parameters that may be sent, by a
910 client, in a single MODE command.
912 return _intOrDefault(params[0])
915 def isupport_NETWORK(self, params):
922 def isupport_NICKLEN(self, params):
924 Maximum length of a nickname the client may use.
926 return _intOrDefault(params[0], self.getFeature('NICKLEN'))
929 def isupport_PREFIX(self, params):
931 Mapping of channel modes that clients may have to status flags.
934 return self._parsePrefixParam(params[0])
936 return self.getFeature('PREFIX')
939 def isupport_SAFELIST(self, params):
941 Flag indicating that a client may request a LIST without being
942 disconnected due to the large amount of data generated.
947 def isupport_STATUSMSG(self, params):
949 The server supports sending messages to only to clients on a channel
950 with a specific status.
955 def isupport_TARGMAX(self, params):
957 Maximum number of targets allowable for commands that accept multiple
960 return dict(self._splitParamArgs(params, _intOrDefault))
963 def isupport_TOPICLEN(self, params):
965 Maximum length of a topic that may be set.
967 return _intOrDefault(params[0])
971 class IRCClient(basic.LineReceiver):
973 Internet Relay Chat client protocol, with sprinkles.
975 In addition to providing an interface for an IRC client protocol,
976 this class also contains reasonable implementations of many common
981 - Limit the length of messages sent (because the IRC server probably
983 - Add flood protection/rate limiting for my CTCP replies.
984 - NickServ cooperation. (a mix-in?)
986 @ivar nickname: Nickname the client will use.
987 @ivar password: Password used to log on to the server. May be C{None}.
988 @ivar realname: Supplied to the server during login as the "Real name"
989 or "ircname". May be C{None}.
990 @ivar username: Supplied to the server during login as the "User name".
993 @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query. If C{None}, no
994 USERINFO reply will be sent.
995 "This is used to transmit a string which is settable by
996 the user (and never should be set by the client)."
997 @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query. If C{None}, no
998 FINGER reply will be sent.
999 @type fingerReply: Callable or String
1001 @ivar versionName: CTCP VERSION reply, client name. If C{None}, no VERSION
1003 @type versionName: C{str}, or None.
1004 @ivar versionNum: CTCP VERSION reply, client version.
1005 @type versionNum: C{str}, or None.
1006 @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
1007 @type versionEnv: C{str}, or None.
1009 @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
1010 client may be found. If C{None}, no SOURCE reply will be sent.
1012 @ivar lineRate: Minimum delay between lines sent to the server. If
1013 C{None}, no delay will be imposed.
1014 @type lineRate: Number of Seconds.
1016 @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and
1017 I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content
1018 of an I{RPL_MOTD} message.
1020 @ivar erroneousNickFallback: Default nickname assigned when an unregistered
1021 client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register
1022 with an illegal nickname.
1023 @type erroneousNickFallback: C{str}
1025 @ivar _registered: Whether or not the user is registered. It becomes True
1026 once a welcome has been received from the server.
1027 @type _registered: C{bool}
1029 @ivar _attemptedNick: The nickname that will try to get registered. It may
1030 change if it is illegal or already taken. L{nickname} becomes the
1031 L{_attemptedNick} that is successfully registered.
1032 @type _attemptedNick: C{str}
1034 @type supported: L{ServerSupportedFeatures}
1035 @ivar supported: Available ISUPPORT features on the server
1037 @type hostname: C{str}
1038 @ivar hostname: Host name of the IRC server the client is connected to.
1039 Initially the host name is C{None} and later is set to the host name
1040 from which the I{RPL_WELCOME} message is received.
1042 @type _heartbeat: L{task.LoopingCall}
1043 @ivar _heartbeat: Looping call to perform the keepalive by calling
1044 L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or
1045 C{None} if there is no heartbeat.
1047 @type heartbeatInterval: C{float}
1048 @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to
1049 the server as a form of keepalive, defaults to 120 seconds. Use C{None}
1050 to disable the heartbeat.
1058 ### Responses to various CTCP queries.
1061 # fingerReply is a callable returning a string, or a str()able object.
1067 sourceURL = "http://twistedmatrix.com/downloads/"
1072 # If this is false, no attempt will be made to identify
1073 # ourself to the server.
1078 _queueEmptying = None
1080 delimiter = '\n' # '\r\n' will also work (see dataReceived)
1082 __pychecker__ = 'unusednames=params,prefix,channel'
1086 erroneousNickFallback = 'defaultnick'
1089 heartbeatInterval = 120
1092 def _reallySendLine(self, line):
1093 return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
1095 def sendLine(self, line):
1096 if self.lineRate is None:
1097 self._reallySendLine(line)
1099 self._queue.append(line)
1100 if not self._queueEmptying:
1103 def _sendLine(self):
1105 self._reallySendLine(self._queue.pop(0))
1106 self._queueEmptying = reactor.callLater(self.lineRate,
1109 self._queueEmptying = None
1112 def connectionLost(self, reason):
1113 basic.LineReceiver.connectionLost(self, reason)
1114 self.stopHeartbeat()
1117 def _createHeartbeat(self):
1119 Create the heartbeat L{LoopingCall}.
1121 return task.LoopingCall(self._sendHeartbeat)
1124 def _sendHeartbeat(self):
1126 Send a I{PING} message to the IRC server as a form of keepalive.
1128 self.sendLine('PING ' + self.hostname)
1131 def stopHeartbeat(self):
1133 Stop sending I{PING} messages to keep the connection to the server
1138 if self._heartbeat is not None:
1139 self._heartbeat.stop()
1140 self._heartbeat = None
1143 def startHeartbeat(self):
1145 Start sending I{PING} messages every L{IRCClient.heartbeatInterval}
1146 seconds to keep the connection to the server alive during periods of no
1151 self.stopHeartbeat()
1152 if self.heartbeatInterval is None:
1154 self._heartbeat = self._createHeartbeat()
1155 self._heartbeat.start(self.heartbeatInterval, now=False)
1158 ### Interface level client->user output methods
1160 ### You'll want to override these.
1162 ### Methods relating to the server itself
1164 def created(self, when):
1166 Called with creation date information about the server, usually at logon.
1169 @param when: A string describing when the server was created, probably.
1172 def yourHost(self, info):
1174 Called with daemon information about the server, usually at logon.
1177 @param when: A string describing what software the server is running, probably.
1180 def myInfo(self, servername, version, umodes, cmodes):
1182 Called with information about the server, usually at logon.
1184 @type servername: C{str}
1185 @param servername: The hostname of this server.
1187 @type version: C{str}
1188 @param version: A description of what software this server runs.
1190 @type umodes: C{str}
1191 @param umodes: All the available user modes.
1193 @type cmodes: C{str}
1194 @param cmodes: All the available channel modes.
1197 def luserClient(self, info):
1199 Called with information about the number of connections, usually at logon.
1202 @param info: A description of the number of clients and servers
1203 connected to the network, probably.
1206 def bounce(self, info):
1208 Called with information about where the client should reconnect.
1211 @param info: A plaintext description of the address that should be
1215 def isupport(self, options):
1217 Called with various information about what the server supports.
1219 @type options: C{list} of C{str}
1220 @param options: Descriptions of features or limits of the server, possibly
1221 in the form "NAME=VALUE".
1224 def luserChannels(self, channels):
1226 Called with the number of channels existant on the server.
1228 @type channels: C{int}
1231 def luserOp(self, ops):
1233 Called with the number of ops logged on to the server.
1238 def luserMe(self, info):
1240 Called with information about the server connected to.
1243 @param info: A plaintext string describing the number of users and servers
1244 connected to this server.
1247 ### Methods involving me directly
1249 def privmsg(self, user, channel, message):
1251 Called when I have a message from a user to me or a channel.
1255 def joined(self, channel):
1257 Called when I finish joining a channel.
1259 channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
1263 def left(self, channel):
1265 Called when I have left a channel.
1267 channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
1272 def noticed(self, user, channel, message):
1274 Called when I have a notice from a user to me or a channel.
1276 If the client makes any automated replies, it must not do so in
1277 response to a NOTICE message, per the RFC::
1279 The difference between NOTICE and PRIVMSG is that
1280 automatic replies MUST NEVER be sent in response to a
1281 NOTICE message. [...] The object of this rule is to avoid
1282 loops between clients automatically sending something in
1283 response to something it received.
1287 def modeChanged(self, user, channel, set, modes, args):
1289 Called when users or channel's modes are changed.
1292 @param user: The user and hostmask which instigated this change.
1294 @type channel: C{str}
1295 @param channel: The channel where the modes are changed. If args is
1296 empty the channel for which the modes are changing. If the changes are
1297 at server level it could be equal to C{user}.
1299 @type set: C{bool} or C{int}
1300 @param set: True if the mode(s) is being added, False if it is being
1301 removed. If some modes are added and others removed at the same time
1302 this function will be called twice, the first time with all the added
1303 modes, the second with the removed ones. (To change this behaviour
1304 override the irc_MODE method)
1307 @param modes: The mode or modes which are being changed.
1309 @type args: C{tuple}
1310 @param args: Any additional information required for the mode
1314 def pong(self, user, secs):
1316 Called with the results of a CTCP PING query.
1322 Called after sucessfully signing on to the server.
1326 def kickedFrom(self, channel, kicker, message):
1328 Called when I am kicked from a channel.
1332 def nickChanged(self, nick):
1334 Called when my nick has been changed.
1336 self.nickname = nick
1339 ### Things I observe other people doing in a channel.
1341 def userJoined(self, user, channel):
1343 Called when I see another user joining a channel.
1347 def userLeft(self, user, channel):
1349 Called when I see another user leaving a channel.
1353 def userQuit(self, user, quitMessage):
1355 Called when I see another user disconnect from the network.
1359 def userKicked(self, kickee, channel, kicker, message):
1361 Called when I observe someone else being kicked from a channel.
1365 def action(self, user, channel, data):
1367 Called when I see a user perform an ACTION on a channel.
1371 def topicUpdated(self, user, channel, newTopic):
1373 In channel, user changed the topic to newTopic.
1375 Also called when first joining a channel.
1379 def userRenamed(self, oldname, newname):
1381 A user changed their name from oldname to newname.
1385 ### Information from the server.
1387 def receivedMOTD(self, motd):
1389 I received a message-of-the-day banner from the server.
1391 motd is a list of strings, where each string was sent as a seperate
1392 message from the server. To display, you might want to use::
1396 to get a nicely formatted string.
1400 ### user input commands, client->server
1401 ### Your client will want to invoke these.
1403 def join(self, channel, key=None):
1407 @type channel: C{str}
1408 @param channel: The name of the channel to join. If it has no prefix,
1409 C{'#'} will be prepended to it.
1411 @param key: If specified, the key used to join the channel.
1413 if channel[0] not in CHANNEL_PREFIXES:
1414 channel = '#' + channel
1416 self.sendLine("JOIN %s %s" % (channel, key))
1418 self.sendLine("JOIN %s" % (channel,))
1420 def leave(self, channel, reason=None):
1424 @type channel: C{str}
1425 @param channel: The name of the channel to leave. If it has no prefix,
1426 C{'#'} will be prepended to it.
1427 @type reason: C{str}
1428 @param reason: If given, the reason for leaving.
1430 if channel[0] not in CHANNEL_PREFIXES:
1431 channel = '#' + channel
1433 self.sendLine("PART %s :%s" % (channel, reason))
1435 self.sendLine("PART %s" % (channel,))
1437 def kick(self, channel, user, reason=None):
1439 Attempt to kick a user from a channel.
1441 @type channel: C{str}
1442 @param channel: The name of the channel to kick the user from. If it has
1443 no prefix, C{'#'} will be prepended to it.
1445 @param user: The nick of the user to kick.
1446 @type reason: C{str}
1447 @param reason: If given, the reason for kicking the user.
1449 if channel[0] not in CHANNEL_PREFIXES:
1450 channel = '#' + channel
1452 self.sendLine("KICK %s %s :%s" % (channel, user, reason))
1454 self.sendLine("KICK %s %s" % (channel, user))
1459 def invite(self, user, channel):
1461 Attempt to invite user to channel
1464 @param user: The user to invite
1465 @type channel: C{str}
1466 @param channel: The channel to invite the user too
1470 if channel[0] not in CHANNEL_PREFIXES:
1471 channel = '#' + channel
1472 self.sendLine("INVITE %s %s" % (user, channel))
1475 def topic(self, channel, topic=None):
1477 Attempt to set the topic of the given channel, or ask what it is.
1479 If topic is None, then I sent a topic query instead of trying to set the
1480 topic. The server should respond with a TOPIC message containing the
1481 current topic of the given channel.
1483 @type channel: C{str}
1484 @param channel: The name of the channel to change the topic on. If it
1485 has no prefix, C{'#'} will be prepended to it.
1487 @param topic: If specified, what to set the topic to.
1489 # << TOPIC #xtestx :fff
1490 if channel[0] not in CHANNEL_PREFIXES:
1491 channel = '#' + channel
1493 self.sendLine("TOPIC %s :%s" % (channel, topic))
1495 self.sendLine("TOPIC %s" % (channel,))
1498 def mode(self, chan, set, modes, limit = None, user = None, mask = None):
1500 Change the modes on a user or channel.
1502 The C{limit}, C{user}, and C{mask} parameters are mutually exclusive.
1505 @param chan: The name of the channel to operate on.
1507 @param set: True to give the user or channel permissions and False to
1510 @param modes: The mode flags to set on the user or channel.
1512 @param limit: In conjuction with the C{'l'} mode flag, limits the
1513 number of users on the channel.
1515 @param user: The user to change the mode on.
1517 @param mask: In conjuction with the C{'b'} mode flag, sets a mask of
1518 users to be banned from the channel.
1521 line = 'MODE %s +%s' % (chan, modes)
1523 line = 'MODE %s -%s' % (chan, modes)
1524 if limit is not None:
1525 line = '%s %d' % (line, limit)
1526 elif user is not None:
1527 line = '%s %s' % (line, user)
1528 elif mask is not None:
1529 line = '%s %s' % (line, mask)
1533 def say(self, channel, message, length=None):
1535 Send a message to a channel
1537 @type channel: C{str}
1538 @param channel: The channel to say the message on. If it has no prefix,
1539 C{'#'} will be prepended to it.
1540 @type message: C{str}
1541 @param message: The message to say.
1542 @type length: C{int}
1543 @param length: The maximum number of octets to send at a time. This has
1544 the effect of turning a single call to C{msg()} into multiple
1545 commands to the server. This is useful when long messages may be
1546 sent that would otherwise cause the server to kick us off or
1547 silently truncate the text we are sending. If None is passed, the
1548 entire message is always send in one command.
1550 if channel[0] not in CHANNEL_PREFIXES:
1551 channel = '#' + channel
1552 self.msg(channel, message, length)
1555 def _safeMaximumLineLength(self, command):
1557 Estimate a safe maximum line length for the given command.
1559 This is done by assuming the maximum values for nickname length,
1560 realname and hostname combined with the command that needs to be sent
1561 and some guessing. A theoretical maximum value is used because it is
1562 possible that our nickname, username or hostname changes (on the server
1563 side) while the length is still being calculated.
1565 # :nickname!realname@hostname COMMAND ...
1566 theoretical = ':%s!%s@%s %s' % (
1567 'a' * self.supported.getFeature('NICKLEN'),
1568 # This value is based on observation.
1570 # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>.
1575 return MAX_COMMAND_LENGTH - len(theoretical) - fudge
1578 def msg(self, user, message, length=None):
1580 Send a message to a user or channel.
1582 The message will be split into multiple commands to the server if:
1583 - The message contains any newline characters
1584 - Any span between newline characters is longer than the given
1587 @param user: Username or channel name to which to direct the
1591 @param message: Text to send.
1592 @type message: C{str}
1594 @param length: Maximum number of octets to send in a single
1595 command, including the IRC protocol framing. If C{None} is given
1596 then L{IRCClient._safeMaximumLineLength} is used to determine a
1598 @type length: C{int}
1600 fmt = 'PRIVMSG %s :' % (user,)
1603 length = self._safeMaximumLineLength(fmt)
1605 # Account for the line terminator.
1606 minimumLength = len(fmt) + 2
1607 if length <= minimumLength:
1608 raise ValueError("Maximum length must exceed %d for message "
1609 "to %s" % (minimumLength, user))
1610 for line in split(message, length - minimumLength):
1611 self.sendLine(fmt + line)
1614 def notice(self, user, message):
1616 Send a notice to a user.
1618 Notices are like normal message, but should never get automated
1622 @param user: The user to send a notice to.
1623 @type message: C{str}
1624 @param message: The contents of the notice to send.
1626 self.sendLine("NOTICE %s :%s" % (user, message))
1629 def away(self, message=''):
1631 Mark this client as away.
1633 @type message: C{str}
1634 @param message: If specified, the away message.
1636 self.sendLine("AWAY :%s" % message)
1641 Clear the away status.
1643 # An empty away marks us as back
1647 def whois(self, nickname, server=None):
1649 Retrieve user information about the given nick name.
1651 @type nickname: C{str}
1652 @param nickname: The nick name about which to retrieve information.
1657 self.sendLine('WHOIS ' + nickname)
1659 self.sendLine('WHOIS %s %s' % (server, nickname))
1662 def register(self, nickname, hostname='foo', servername='bar'):
1664 Login to the server.
1666 @type nickname: C{str}
1667 @param nickname: The nickname to register.
1668 @type hostname: C{str}
1669 @param hostname: If specified, the hostname to logon as.
1670 @type servername: C{str}
1671 @param servername: If specified, the servername to logon as.
1673 if self.password is not None:
1674 self.sendLine("PASS %s" % self.password)
1675 self.setNick(nickname)
1676 if self.username is None:
1677 self.username = nickname
1678 self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname))
1681 def setNick(self, nickname):
1683 Set this client's nickname.
1685 @type nickname: C{str}
1686 @param nickname: The nickname to change to.
1688 self._attemptedNick = nickname
1689 self.sendLine("NICK %s" % nickname)
1692 def quit(self, message = ''):
1694 Disconnect from the server
1696 @type message: C{str}
1698 @param message: If specified, the message to give when quitting the
1701 self.sendLine("QUIT :%s" % message)
1703 ### user input commands, client->client
1705 def describe(self, channel, action):
1709 @type channel: C{str}
1710 @param channel: The name of the channel to have an action on. If it
1711 has no prefix, it is sent to the user of that name.
1712 @type action: C{str}
1713 @param action: The action to preform.
1716 self.ctcpMakeQuery(channel, [('ACTION', action)])
1722 def ping(self, user, text = None):
1724 Measure round-trip delay to another IRC client.
1726 if self._pings is None:
1730 chars = string.letters + string.digits + string.punctuation
1731 key = ''.join([random.choice(chars) for i in range(12)])
1734 self._pings[(user, key)] = time.time()
1735 self.ctcpMakeQuery(user, [('PING', key)])
1737 if len(self._pings) > self._MAX_PINGRING:
1738 # Remove some of the oldest entries.
1739 byValue = [(v, k) for (k, v) in self._pings.items()]
1741 excess = self._MAX_PINGRING - len(self._pings)
1742 for i in xrange(excess):
1743 del self._pings[byValue[i][1]]
1746 def dccSend(self, user, file):
1747 if type(file) == types.StringType:
1748 file = open(file, 'r')
1750 size = fileSize(file)
1752 name = getattr(file, "name", "file@%s" % (id(file),))
1754 factory = DccSendFactory(file)
1755 port = reactor.listenTCP(0, factory, 1)
1757 raise NotImplementedError,(
1758 "XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. "
1759 "(and stop accepting once we've made a single connection.)")
1761 my_address = struct.pack("!I", my_address)
1763 args = ['SEND', name, my_address, str(port)]
1765 if not (size is None):
1768 args = string.join(args, ' ')
1770 self.ctcpMakeQuery(user, [('DCC', args)])
1773 def dccResume(self, user, fileName, port, resumePos):
1775 Send a DCC RESUME request to another user.
1777 self.ctcpMakeQuery(user, [
1778 ('DCC', ['RESUME', fileName, port, resumePos])])
1781 def dccAcceptResume(self, user, fileName, port, resumePos):
1783 Send a DCC ACCEPT response to clients who have requested a resume.
1785 self.ctcpMakeQuery(user, [
1786 ('DCC', ['ACCEPT', fileName, port, resumePos])])
1788 ### server->client messages
1789 ### You might want to fiddle with these,
1790 ### but it is safe to leave them alone.
1792 def irc_ERR_NICKNAMEINUSE(self, prefix, params):
1794 Called when we try to register or change to a nickname that is already
1797 self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
1798 self.setNick(self._attemptedNick)
1801 def alterCollidedNick(self, nickname):
1803 Generate an altered version of a nickname that caused a collision in an
1804 effort to create an unused related name for subsequent registration.
1806 @param nickname: The nickname a user is attempting to register.
1807 @type nickname: C{str}
1809 @returns: A string that is in some way different from the nickname.
1812 return nickname + '_'
1815 def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
1817 Called when we try to register or change to an illegal nickname.
1819 The server should send this reply when the nickname contains any
1820 disallowed characters. The bot will stall, waiting for RPL_WELCOME, if
1821 we don't handle this during sign-on.
1823 @note: The method uses the spelling I{erroneus}, as it appears in
1824 the RFC, section 6.1.
1826 if not self._registered:
1827 self.setNick(self.erroneousNickFallback)
1830 def irc_ERR_PASSWDMISMATCH(self, prefix, params):
1832 Called when the login was incorrect.
1834 raise IRCPasswordMismatch("Password Incorrect.")
1837 def irc_RPL_WELCOME(self, prefix, params):
1839 Called when we have received the welcome from the server.
1841 self.hostname = prefix
1842 self._registered = True
1843 self.nickname = self._attemptedNick
1845 self.startHeartbeat()
1848 def irc_JOIN(self, prefix, params):
1850 Called when a user joins a channel.
1852 nick = string.split(prefix,'!')[0]
1853 channel = params[-1]
1854 if nick == self.nickname:
1855 self.joined(channel)
1857 self.userJoined(nick, channel)
1859 def irc_PART(self, prefix, params):
1861 Called when a user leaves a channel.
1863 nick = string.split(prefix,'!')[0]
1865 if nick == self.nickname:
1868 self.userLeft(nick, channel)
1870 def irc_QUIT(self, prefix, params):
1872 Called when a user has quit.
1874 nick = string.split(prefix,'!')[0]
1875 self.userQuit(nick, params[0])
1878 def irc_MODE(self, user, params):
1880 Parse a server mode change message.
1882 channel, modes, args = params[0], params[1], params[2:]
1884 if modes[0] not in '-+':
1887 if channel == self.nickname:
1888 # This is a mode change to our individual user, not a channel mode
1890 paramModes = self.getUserModeParams()
1892 paramModes = self.getChannelModeParams()
1895 added, removed = parseModes(modes, args, paramModes)
1897 log.err(None, 'An error occured while parsing the following '
1898 'MODE message: MODE %s' % (' '.join(params),))
1901 modes, params = zip(*added)
1902 self.modeChanged(user, channel, True, ''.join(modes), params)
1905 modes, params = zip(*removed)
1906 self.modeChanged(user, channel, False, ''.join(modes), params)
1909 def irc_PING(self, prefix, params):
1911 Called when some has pinged us.
1913 self.sendLine("PONG %s" % params[-1])
1915 def irc_PRIVMSG(self, prefix, params):
1917 Called when we get a message.
1921 message = params[-1]
1924 # Don't raise an exception if we get blank message.
1927 if message[0] == X_DELIM:
1928 m = ctcpExtract(message)
1930 self.ctcpQuery(user, channel, m['extended'])
1935 message = string.join(m['normal'], ' ')
1937 self.privmsg(user, channel, message)
1939 def irc_NOTICE(self, prefix, params):
1941 Called when a user gets a notice.
1945 message = params[-1]
1947 if message[0]==X_DELIM:
1948 m = ctcpExtract(message)
1950 self.ctcpReply(user, channel, m['extended'])
1955 message = string.join(m['normal'], ' ')
1957 self.noticed(user, channel, message)
1959 def irc_NICK(self, prefix, params):
1961 Called when a user changes their nickname.
1963 nick = string.split(prefix,'!', 1)[0]
1964 if nick == self.nickname:
1965 self.nickChanged(params[0])
1967 self.userRenamed(nick, params[0])
1969 def irc_KICK(self, prefix, params):
1971 Called when a user is kicked from a channel.
1973 kicker = string.split(prefix,'!')[0]
1976 message = params[-1]
1977 if string.lower(kicked) == string.lower(self.nickname):
1979 self.kickedFrom(channel, kicker, message)
1981 self.userKicked(kicked, channel, kicker, message)
1983 def irc_TOPIC(self, prefix, params):
1985 Someone in the channel set the topic.
1987 user = string.split(prefix, '!')[0]
1989 newtopic = params[1]
1990 self.topicUpdated(user, channel, newtopic)
1992 def irc_RPL_TOPIC(self, prefix, params):
1994 Called when the topic for a channel is initially reported or when it
1995 subsequently changes.
1997 user = string.split(prefix, '!')[0]
1999 newtopic = params[2]
2000 self.topicUpdated(user, channel, newtopic)
2002 def irc_RPL_NOTOPIC(self, prefix, params):
2003 user = string.split(prefix, '!')[0]
2006 self.topicUpdated(user, channel, newtopic)
2008 def irc_RPL_MOTDSTART(self, prefix, params):
2009 if params[-1].startswith("- "):
2010 params[-1] = params[-1][2:]
2011 self.motd = [params[-1]]
2013 def irc_RPL_MOTD(self, prefix, params):
2014 if params[-1].startswith("- "):
2015 params[-1] = params[-1][2:]
2016 if self.motd is None:
2018 self.motd.append(params[-1])
2021 def irc_RPL_ENDOFMOTD(self, prefix, params):
2023 I{RPL_ENDOFMOTD} indicates the end of the message of the day
2024 messages. Deliver the accumulated lines to C{receivedMOTD}.
2028 self.receivedMOTD(motd)
2031 def irc_RPL_CREATED(self, prefix, params):
2032 self.created(params[1])
2034 def irc_RPL_YOURHOST(self, prefix, params):
2035 self.yourHost(params[1])
2037 def irc_RPL_MYINFO(self, prefix, params):
2038 info = params[1].split(None, 3)
2039 while len(info) < 4:
2043 def irc_RPL_BOUNCE(self, prefix, params):
2044 self.bounce(params[1])
2046 def irc_RPL_ISUPPORT(self, prefix, params):
2048 # Several ISUPPORT messages, in no particular order, may be sent
2049 # to the client at any given point in time (usually only on connect,
2050 # though.) For this reason, ServerSupportedFeatures.parse is intended
2051 # to mutate the supported feature list.
2052 self.supported.parse(args)
2055 def irc_RPL_LUSERCLIENT(self, prefix, params):
2056 self.luserClient(params[1])
2058 def irc_RPL_LUSEROP(self, prefix, params):
2060 self.luserOp(int(params[1]))
2064 def irc_RPL_LUSERCHANNELS(self, prefix, params):
2066 self.luserChannels(int(params[1]))
2070 def irc_RPL_LUSERME(self, prefix, params):
2071 self.luserMe(params[1])
2073 def irc_unknown(self, prefix, command, params):
2076 ### Receiving a CTCP query from another party
2077 ### It is safe to leave these alone.
2080 def ctcpQuery(self, user, channel, messages):
2082 Dispatch method for any CTCP queries received.
2084 Duplicated CTCP queries are ignored and no dispatch is
2085 made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}.
2088 for tag, data in messages:
2089 method = getattr(self, 'ctcpQuery_%s' % tag, None)
2091 if method is not None:
2092 method(user, channel, data)
2094 self.ctcpUnknownQuery(user, channel, tag, data)
2098 def ctcpUnknownQuery(self, user, channel, tag, data):
2100 Fallback handler for unrecognized CTCP queries.
2102 No CTCP I{ERRMSG} reply is made to remove a potential denial of service
2105 log.msg('Unknown CTCP query from %r: %r %r' % (user, tag, data))
2108 def ctcpQuery_ACTION(self, user, channel, data):
2109 self.action(user, channel, data)
2111 def ctcpQuery_PING(self, user, channel, data):
2112 nick = string.split(user,"!")[0]
2113 self.ctcpMakeReply(nick, [("PING", data)])
2115 def ctcpQuery_FINGER(self, user, channel, data):
2116 if data is not None:
2117 self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
2119 if not self.fingerReply:
2122 if callable(self.fingerReply):
2123 reply = self.fingerReply()
2125 reply = str(self.fingerReply)
2127 nick = string.split(user,"!")[0]
2128 self.ctcpMakeReply(nick, [('FINGER', reply)])
2130 def ctcpQuery_VERSION(self, user, channel, data):
2131 if data is not None:
2132 self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
2135 if self.versionName:
2136 nick = string.split(user,"!")[0]
2137 self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
2139 self.versionNum or '',
2140 self.versionEnv or ''))])
2142 def ctcpQuery_SOURCE(self, user, channel, data):
2143 if data is not None:
2144 self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
2147 nick = string.split(user,"!")[0]
2148 # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
2149 # replies should be responded to with the location of an anonymous
2150 # FTP server in host:directory:file format. I'm taking the liberty
2151 # of bringing it into the 21st century by sending a URL instead.
2152 self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
2155 def ctcpQuery_USERINFO(self, user, channel, data):
2156 if data is not None:
2157 self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
2160 nick = string.split(user,"!")[0]
2161 self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
2163 def ctcpQuery_CLIENTINFO(self, user, channel, data):
2165 A master index of what CTCP tags this client knows.
2167 If no arguments are provided, respond with a list of known tags.
2168 If an argument is provided, provide human-readable help on
2169 the usage of that tag.
2172 nick = string.split(user,"!")[0]
2174 # XXX: prefixedMethodNames gets methods from my *class*,
2175 # but it's entirely possible that this *instance* has more
2177 names = reflect.prefixedMethodNames(self.__class__,
2180 self.ctcpMakeReply(nick, [('CLIENTINFO',
2181 string.join(names, ' '))])
2183 args = string.split(data)
2184 method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
2186 self.ctcpMakeReply(nick, [('ERRMSG',
2188 "Unknown query '%s'"
2189 % (data, args[0]))])
2191 doc = getattr(method, '__doc__', '')
2192 self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
2195 def ctcpQuery_ERRMSG(self, user, channel, data):
2196 # Yeah, this seems strange, but that's what the spec says to do
2197 # when faced with an ERRMSG query (not a reply).
2198 nick = string.split(user,"!")[0]
2199 self.ctcpMakeReply(nick, [('ERRMSG',
2200 "%s :No error has occoured." % data)])
2202 def ctcpQuery_TIME(self, user, channel, data):
2203 if data is not None:
2204 self.quirkyMessage("Why did %s send '%s' with a TIME query?"
2206 nick = string.split(user,"!")[0]
2207 self.ctcpMakeReply(nick,
2209 time.asctime(time.localtime(time.time())))])
2211 def ctcpQuery_DCC(self, user, channel, data):
2212 """Initiate a Direct Client Connection
2216 dcctype = data.split(None, 1)[0].upper()
2217 handler = getattr(self, "dcc_" + dcctype, None)
2219 if self.dcc_sessions is None:
2220 self.dcc_sessions = []
2221 data = data[len(dcctype)+1:]
2222 handler(user, channel, data)
2224 nick = string.split(user,"!")[0]
2225 self.ctcpMakeReply(nick, [('ERRMSG',
2226 "DCC %s :Unknown DCC type '%s'"
2227 % (data, dcctype))])
2228 self.quirkyMessage("%s offered unknown DCC type %s"
2231 def dcc_SEND(self, user, channel, data):
2232 # Use splitQuoted for those who send files with spaces in the names.
2233 data = text.splitQuoted(data)
2235 raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
2237 (filename, address, port) = data[:3]
2239 address = dccParseAddress(address)
2243 raise IRCBadMessage, "Indecipherable port %r" % (port,)
2252 # XXX Should we bother passing this data?
2253 self.dccDoSend(user, address, port, filename, size, data)
2255 def dcc_ACCEPT(self, user, channel, data):
2256 data = text.splitQuoted(data)
2258 raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
2259 (filename, port, resumePos) = data[:3]
2262 resumePos = int(resumePos)
2266 self.dccDoAcceptResume(user, filename, port, resumePos)
2268 def dcc_RESUME(self, user, channel, data):
2269 data = text.splitQuoted(data)
2271 raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
2272 (filename, port, resumePos) = data[:3]
2275 resumePos = int(resumePos)
2278 self.dccDoResume(user, filename, port, resumePos)
2280 def dcc_CHAT(self, user, channel, data):
2281 data = text.splitQuoted(data)
2283 raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
2285 (filename, address, port) = data[:3]
2287 address = dccParseAddress(address)
2291 raise IRCBadMessage, "Indecipherable port %r" % (port,)
2293 self.dccDoChat(user, channel, address, port, data)
2295 ### The dccDo methods are the slightly higher-level siblings of
2296 ### common dcc_ methods; the arguments have been parsed for them.
2298 def dccDoSend(self, user, address, port, fileName, size, data):
2299 """Called when I receive a DCC SEND offer from a client.
2301 By default, I do nothing here."""
2302 ## filename = path.basename(arg)
2303 ## protocol = DccFileReceive(filename, size,
2304 ## (user,channel,data),self.dcc_destdir)
2305 ## reactor.clientTCP(address, port, protocol)
2306 ## self.dcc_sessions.append(protocol)
2309 def dccDoResume(self, user, file, port, resumePos):
2310 """Called when a client is trying to resume an offered file
2311 via DCC send. It should be either replied to with a DCC
2312 ACCEPT or ignored (default)."""
2315 def dccDoAcceptResume(self, user, file, port, resumePos):
2316 """Called when a client has verified and accepted a DCC resume
2317 request made by us. By default it will do nothing."""
2320 def dccDoChat(self, user, channel, address, port, data):
2322 #factory = DccChatFactory(self, queryData=(user, channel, data))
2323 #reactor.connectTCP(address, port, factory)
2324 #self.dcc_sessions.append(factory)
2326 #def ctcpQuery_SED(self, user, data):
2327 # """Simple Encryption Doodoo
2329 # Feel free to implement this, but no specification is available.
2331 # raise NotImplementedError
2334 def ctcpMakeReply(self, user, messages):
2336 Send one or more C{extended messages} as a CTCP reply.
2338 @type messages: a list of extended messages. An extended
2339 message is a (tag, data) tuple, where 'data' may be C{None}.
2341 self.notice(user, ctcpStringify(messages))
2343 ### client CTCP query commands
2345 def ctcpMakeQuery(self, user, messages):
2347 Send one or more C{extended messages} as a CTCP query.
2349 @type messages: a list of extended messages. An extended
2350 message is a (tag, data) tuple, where 'data' may be C{None}.
2352 self.msg(user, ctcpStringify(messages))
2354 ### Receiving a response to a CTCP query (presumably to one we made)
2355 ### You may want to add methods here, or override UnknownReply.
2357 def ctcpReply(self, user, channel, messages):
2359 Dispatch method for any CTCP replies received.
2362 method = getattr(self, "ctcpReply_%s" % m[0], None)
2364 method(user, channel, m[1])
2366 self.ctcpUnknownReply(user, channel, m[0], m[1])
2368 def ctcpReply_PING(self, user, channel, data):
2369 nick = user.split('!', 1)[0]
2370 if (not self._pings) or (not self._pings.has_key((nick, data))):
2371 raise IRCBadMessage,\
2372 "Bogus PING response from %s: %s" % (user, data)
2374 t0 = self._pings[(nick, data)]
2375 self.pong(user, time.time() - t0)
2377 def ctcpUnknownReply(self, user, channel, tag, data):
2378 """Called when a fitting ctcpReply_ method is not found.
2380 XXX: If the client makes arbitrary CTCP queries,
2381 this method should probably show the responses to
2382 them instead of treating them as anomolies.
2384 log.msg("Unknown CTCP reply from %s: %s %s\n"
2385 % (user, tag, data))
2388 ### You may override these with something more appropriate to your UI.
2390 def badMessage(self, line, excType, excValue, tb):
2391 """When I get a message that's so broken I can't use it.
2394 log.msg(string.join(traceback.format_exception(excType,
2398 def quirkyMessage(self, s):
2399 """This is called when I receive a message which is peculiar,
2400 but not wholly indecipherable.
2404 ### Protocool methods
2406 def connectionMade(self):
2407 self.supported = ServerSupportedFeatures()
2409 if self.performLogin:
2410 self.register(self.nickname)
2412 def dataReceived(self, data):
2413 basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
2415 def lineReceived(self, line):
2416 line = lowDequote(line)
2418 prefix, command, params = parsemsg(line)
2419 if numeric_to_symbolic.has_key(command):
2420 command = numeric_to_symbolic[command]
2421 self.handleCommand(command, prefix, params)
2422 except IRCBadMessage:
2423 self.badMessage(line, *sys.exc_info())
2426 def getUserModeParams(self):
2428 Get user modes that require parameters for correct parsing.
2430 @rtype: C{[str, str]}
2431 @return C{[add, remove]}
2436 def getChannelModeParams(self):
2438 Get channel modes that require parameters for correct parsing.
2440 @rtype: C{[str, str]}
2441 @return C{[add, remove]}
2443 # PREFIX modes are treated as "type B" CHANMODES, they always take
2446 prefixes = self.supported.getFeature('PREFIX', {})
2447 params[0] = params[1] = ''.join(prefixes.iterkeys())
2449 chanmodes = self.supported.getFeature('CHANMODES')
2450 if chanmodes is not None:
2451 params[0] += chanmodes.get('addressModes', '')
2452 params[0] += chanmodes.get('param', '')
2453 params[1] = params[0]
2454 params[0] += chanmodes.get('setParam', '')
2458 def handleCommand(self, command, prefix, params):
2459 """Determine the function to call for the given command and call
2460 it with the given arguments.
2462 method = getattr(self, "irc_%s" % command, None)
2464 if method is not None:
2465 method(prefix, params)
2467 self.irc_unknown(prefix, command, params)
2472 def __getstate__(self):
2473 dct = self.__dict__.copy()
2474 dct['dcc_sessions'] = None
2475 dct['_pings'] = None
2479 def dccParseAddress(address):
2484 address = long(address)
2486 raise IRCBadMessage,\
2487 "Indecipherable address %r" % (address,)
2490 (address >> 24) & 0xFF,
2491 (address >> 16) & 0xFF,
2492 (address >> 8) & 0xFF,
2495 address = '.'.join(map(str,address))
2499 class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
2500 """Bare protocol to receive a Direct Client Connection SEND stream.
2502 This does enough to keep the other guy talking, but you'll want to
2503 extend my dataReceived method to *do* something with the data I get.
2508 def __init__(self, resumeOffset=0):
2509 self.bytesReceived = resumeOffset
2510 self.resume = (resumeOffset != 0)
2512 def dataReceived(self, data):
2513 """Called when data is received.
2515 Warning: This just acknowledges to the remote host that the
2516 data has been received; it doesn't *do* anything with the
2517 data, so you'll want to override this.
2519 self.bytesReceived = self.bytesReceived + len(data)
2520 self.transport.write(struct.pack('!i', self.bytesReceived))
2523 class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
2524 """Protocol for an outgoing Direct Client Connection SEND.
2533 def __init__(self, file):
2534 if type(file) is types.StringType:
2535 self.file = open(file, 'r')
2537 def connectionMade(self):
2541 def dataReceived(self, data):
2542 # XXX: Do we need to check to see if len(data) != fmtsize?
2544 bytesShesGot = struct.unpack("!I", data)
2545 if bytesShesGot < self.bytesSent:
2547 # XXX? Add some checks to see if we've stalled out?
2549 elif bytesShesGot > self.bytesSent:
2550 # self.transport.log("DCC SEND %s: She says she has %d bytes "
2551 # "but I've only sent %d. I'm stopping "
2552 # "this screwy transfer."
2554 # bytesShesGot, self.bytesSent))
2555 self.transport.loseConnection()
2560 def sendBlock(self):
2561 block = self.file.read(self.blocksize)
2563 self.transport.write(block)
2564 self.bytesSent = self.bytesSent + len(block)
2566 # Nothing more to send, transfer complete.
2567 self.transport.loseConnection()
2570 def connectionLost(self, reason):
2572 if hasattr(self.file, "close"):
2576 class DccSendFactory(protocol.Factory):
2577 protocol = DccSendProtocol
2578 def __init__(self, file):
2581 def buildProtocol(self, connection):
2582 p = self.protocol(self.file)
2588 """I'll try my damndest to determine the size of this file object.
2591 if hasattr(file, "fileno"):
2592 fileno = file.fileno()
2594 stat_ = os.fstat(fileno)
2595 size = stat_[stat.ST_SIZE]
2601 if hasattr(file, "name") and path.exists(file.name):
2603 size = path.getsize(file.name)
2609 if hasattr(file, "seek") and hasattr(file, "tell"):
2623 class DccChat(basic.LineReceiver, styles.Ephemeral):
2624 """Direct Client Connection protocol type CHAT.
2626 DCC CHAT is really just your run o' the mill basic.LineReceiver
2627 protocol. This class only varies from that slightly, accepting
2628 either LF or CR LF for a line delimeter for incoming messages
2629 while always using CR LF for outgoing.
2631 The lineReceived method implemented here uses the DCC connection's
2632 'client' attribute (provided upon construction) to deliver incoming
2633 lines from the DCC chat via IRCClient's normal privmsg interface.
2634 That's something of a spoof, which you may well want to override.
2643 def __init__(self, client, queryData=None):
2644 """Initialize a new DCC CHAT session.
2646 queryData is a 3-tuple of
2647 (fromUser, targetUserOrChannel, data)
2648 as received by the CTCP query.
2650 (To be honest, fromUser is the only thing that's currently
2651 used here. targetUserOrChannel is potentially useful, while
2652 the 'data' argument is soley for informational purposes.)
2654 self.client = client
2656 self.queryData = queryData
2657 self.remoteParty = self.queryData[0]
2659 def dataReceived(self, data):
2660 self.buffer = self.buffer + data
2661 lines = string.split(self.buffer, LF)
2662 # Put the (possibly empty) element after the last LF back in the
2664 self.buffer = lines.pop()
2669 self.lineReceived(line)
2671 def lineReceived(self, line):
2672 log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
2673 self.client.privmsg(self.remoteParty,
2674 self.client.nickname, line)
2677 class DccChatFactory(protocol.ClientFactory):
2680 def __init__(self, client, queryData):
2681 self.client = client
2682 self.queryData = queryData
2685 def buildProtocol(self, addr):
2686 p = self.protocol(client=self.client, queryData=self.queryData)
2691 def clientConnectionFailed(self, unused_connector, unused_reason):
2692 self.client.dcc_sessions.remove(self)
2694 def clientConnectionLost(self, unused_connector, unused_reason):
2695 self.client.dcc_sessions.remove(self)
2698 def dccDescribe(data):
2699 """Given the data chunk from a DCC query, return a descriptive string.
2703 data = string.split(data)
2707 (dcctype, arg, address, port) = data[:4]
2713 address = long(address)
2718 (address >> 24) & 0xFF,
2719 (address >> 16) & 0xFF,
2720 (address >> 8) & 0xFF,
2723 # The mapping to 'int' is to get rid of those accursed
2724 # "L"s which python 1.5.2 puts on the end of longs.
2725 address = string.join(map(str,map(int,address)), ".")
2727 if dcctype == 'SEND':
2734 size_txt = ' of size %d bytes' % (size,)
2738 dcc_text = ("SEND for file '%s'%s at host %s, port %s"
2739 % (filename, size_txt, address, port))
2740 elif dcctype == 'CHAT':
2741 dcc_text = ("CHAT for host %s, port %s"
2744 dcc_text = orig_data
2749 class DccFileReceive(DccFileReceiveBasic):
2750 """Higher-level coverage for getting a file from DCC SEND.
2752 I allow you to change the file's name and destination directory.
2753 I won't overwrite an existing file unless I've been told it's okay
2754 to do so. If passed the resumeOffset keyword argument I will attempt to
2755 resume the file from that amount of bytes.
2757 XXX: I need to let the client know when I am finished.
2758 XXX: I need to decide how to keep a progress indicator updated.
2759 XXX: Client needs a way to tell me "Do not finish until I say so."
2760 XXX: I need to make sure the client understands if the file cannot be written.
2770 def __init__(self, filename, fileSize=-1, queryData=None,
2771 destDir='.', resumeOffset=0):
2772 DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
2773 self.filename = filename
2774 self.destDir = destDir
2775 self.fileSize = fileSize
2778 self.queryData = queryData
2779 self.fromUser = self.queryData[0]
2781 def set_directory(self, directory):
2782 """Set the directory where the downloaded file will be placed.
2784 May raise OSError if the supplied directory path is not suitable.
2786 if not path.exists(directory):
2787 raise OSError(errno.ENOENT, "You see no directory there.",
2789 if not path.isdir(directory):
2790 raise OSError(errno.ENOTDIR, "You cannot put a file into "
2791 "something which is not a directory.",
2793 if not os.access(directory, os.X_OK | os.W_OK):
2794 raise OSError(errno.EACCES,
2795 "This directory is too hard to write in to.",
2797 self.destDir = directory
2799 def set_filename(self, filename):
2800 """Change the name of the file being transferred.
2802 This replaces the file name provided by the sender.
2804 self.filename = filename
2806 def set_overwrite(self, boolean):
2807 """May I overwrite existing files?
2809 self.overwrite = boolean
2812 # Protocol-level methods.
2814 def connectionMade(self):
2815 dst = path.abspath(path.join(self.destDir,self.filename))
2816 exists = path.exists(dst)
2817 if self.resume and exists:
2818 # I have been told I want to resume, and a file already
2819 # exists - Here we go
2820 self.file = open(dst, 'ab')
2821 log.msg("Attempting to resume %s - starting from %d bytes" %
2822 (self.file, self.file.tell()))
2823 elif self.overwrite or not exists:
2824 self.file = open(dst, 'wb')
2826 raise OSError(errno.EEXIST,
2827 "There's a file in the way. "
2828 "Perhaps that's why you cannot open it.",
2831 def dataReceived(self, data):
2832 self.file.write(data)
2833 DccFileReceiveBasic.dataReceived(self, data)
2835 # XXX: update a progress indicator here?
2837 def connectionLost(self, reason):
2838 """When the connection is lost, I close the file.
2841 logmsg = ("%s closed." % (self,))
2842 if self.fileSize > 0:
2843 logmsg = ("%s %d/%d bytes received"
2844 % (logmsg, self.bytesReceived, self.fileSize))
2845 if self.bytesReceived == self.fileSize:
2847 elif self.bytesReceived < self.fileSize:
2848 logmsg = ("%s (Warning: %d bytes short)"
2849 % (logmsg, self.fileSize - self.bytesReceived))
2851 logmsg = ("%s (file larger than expected)"
2854 logmsg = ("%s %d bytes received"
2855 % (logmsg, self.bytesReceived))
2857 if hasattr(self, 'file'):
2858 logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
2859 if hasattr(self.file, 'close'): self.file.close()
2861 # self.transport.log(logmsg)
2864 if not self.connected:
2865 return "<Unconnected DccFileReceive object at %x>" % (id(self),)
2866 from_ = self.transport.getPeer()
2868 from_ = "%s (%s)" % (self.fromUser, from_)
2870 s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
2874 s = ("<%s at %x: GET %s>"
2875 % (self.__class__, id(self), self.filename))
2879 # CTCP constants and helper functions
2883 def ctcpExtract(message):
2885 Extract CTCP data from a string.
2887 @return: A C{dict} containing two keys:
2888 - C{'extended'}: A list of CTCP (tag, data) tuples.
2889 - C{'normal'}: A list of strings which were not inside a CTCP delimiter.
2891 extended_messages = []
2892 normal_messages = []
2893 retval = {'extended': extended_messages,
2894 'normal': normal_messages }
2896 messages = string.split(message, X_DELIM)
2899 # X1 extended data X2 nomal data X3 extended data X4 normal...
2902 extended_messages.append(messages.pop(0))
2904 normal_messages.append(messages.pop(0))
2907 extended_messages[:] = filter(None, extended_messages)
2908 normal_messages[:] = filter(None, normal_messages)
2910 extended_messages[:] = map(ctcpDequote, extended_messages)
2911 for i in xrange(len(extended_messages)):
2912 m = string.split(extended_messages[i], SPC, 1)
2919 extended_messages[i] = (tag, data)
2931 M_QUOTE: M_QUOTE + M_QUOTE
2935 for k, v in mQuoteTable.items():
2936 mDequoteTable[v[-1]] = k
2939 mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
2942 for c in (M_QUOTE, NUL, NL, CR):
2943 s = string.replace(s, c, mQuoteTable[c])
2947 def sub(matchobj, mDequoteTable=mDequoteTable):
2948 s = matchobj.group()[1]
2950 s = mDequoteTable[s]
2955 return mEscape_re.sub(sub, s)
2960 X_DELIM: X_QUOTE + 'a',
2961 X_QUOTE: X_QUOTE + X_QUOTE
2966 for k, v in xQuoteTable.items():
2967 xDequoteTable[v[-1]] = k
2969 xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
2972 for c in (X_QUOTE, X_DELIM):
2973 s = string.replace(s, c, xQuoteTable[c])
2977 def sub(matchobj, xDequoteTable=xDequoteTable):
2978 s = matchobj.group()[1]
2980 s = xDequoteTable[s]
2985 return xEscape_re.sub(sub, s)
2987 def ctcpStringify(messages):
2989 @type messages: a list of extended messages. An extended
2990 message is a (tag, data) tuple, where 'data' may be C{None}, a
2991 string, or a list of strings to be joined with whitespace.
2996 for (tag, data) in messages:
2998 if not isinstance(data, types.StringType):
3000 # data as list-of-strings
3001 data = " ".join(map(str, data))
3003 # No? Then use it's %s representation.
3005 m = "%s %s" % (tag, data)
3009 m = "%s%s%s" % (X_DELIM, m, X_DELIM)
3010 coded_messages.append(m)
3012 line = string.join(coded_messages, '')
3016 # Constants (from RFC 2812)
3018 RPL_YOURHOST = '002'
3021 RPL_ISUPPORT = '005'
3023 RPL_USERHOST = '302'
3028 RPL_WHOISUSER = '311'
3029 RPL_WHOISSERVER = '312'
3030 RPL_WHOISOPERATOR = '313'
3031 RPL_WHOISIDLE = '317'
3032 RPL_ENDOFWHOIS = '318'
3033 RPL_WHOISCHANNELS = '319'
3034 RPL_WHOWASUSER = '314'
3035 RPL_ENDOFWHOWAS = '369'
3036 RPL_LISTSTART = '321'
3039 RPL_UNIQOPIS = '325'
3040 RPL_CHANNELMODEIS = '324'
3043 RPL_INVITING = '341'
3044 RPL_SUMMONING = '342'
3045 RPL_INVITELIST = '346'
3046 RPL_ENDOFINVITELIST = '347'
3047 RPL_EXCEPTLIST = '348'
3048 RPL_ENDOFEXCEPTLIST = '349'
3050 RPL_WHOREPLY = '352'
3051 RPL_ENDOFWHO = '315'
3052 RPL_NAMREPLY = '353'
3053 RPL_ENDOFNAMES = '366'
3055 RPL_ENDOFLINKS = '365'
3057 RPL_ENDOFBANLIST = '368'
3059 RPL_ENDOFINFO = '374'
3060 RPL_MOTDSTART = '375'
3062 RPL_ENDOFMOTD = '376'
3063 RPL_YOUREOPER = '381'
3064 RPL_REHASHING = '382'
3065 RPL_YOURESERVICE = '383'
3067 RPL_USERSSTART = '392'
3069 RPL_ENDOFUSERS = '394'
3071 RPL_TRACELINK = '200'
3072 RPL_TRACECONNECTING = '201'
3073 RPL_TRACEHANDSHAKE = '202'
3074 RPL_TRACEUNKNOWN = '203'
3075 RPL_TRACEOPERATOR = '204'
3076 RPL_TRACEUSER = '205'
3077 RPL_TRACESERVER = '206'
3078 RPL_TRACESERVICE = '207'
3079 RPL_TRACENEWTYPE = '208'
3080 RPL_TRACECLASS = '209'
3081 RPL_TRACERECONNECT = '210'
3082 RPL_TRACELOG = '261'
3083 RPL_TRACEEND = '262'
3084 RPL_STATSLINKINFO = '211'
3085 RPL_STATSCOMMANDS = '212'
3086 RPL_ENDOFSTATS = '219'
3087 RPL_STATSUPTIME = '242'
3088 RPL_STATSOLINE = '243'
3090 RPL_SERVLIST = '234'
3091 RPL_SERVLISTEND = '235'
3092 RPL_LUSERCLIENT = '251'
3094 RPL_LUSERUNKNOWN = '253'
3095 RPL_LUSERCHANNELS = '254'
3098 RPL_ADMINLOC = '257'
3099 RPL_ADMINLOC = '258'
3100 RPL_ADMINEMAIL = '259'
3101 RPL_TRYAGAIN = '263'
3102 ERR_NOSUCHNICK = '401'
3103 ERR_NOSUCHSERVER = '402'
3104 ERR_NOSUCHCHANNEL = '403'
3105 ERR_CANNOTSENDTOCHAN = '404'
3106 ERR_TOOMANYCHANNELS = '405'
3107 ERR_WASNOSUCHNICK = '406'
3108 ERR_TOOMANYTARGETS = '407'
3109 ERR_NOSUCHSERVICE = '408'
3110 ERR_NOORIGIN = '409'
3111 ERR_NORECIPIENT = '411'
3112 ERR_NOTEXTTOSEND = '412'
3113 ERR_NOTOPLEVEL = '413'
3114 ERR_WILDTOPLEVEL = '414'
3116 ERR_UNKNOWNCOMMAND = '421'
3118 ERR_NOADMININFO = '423'
3119 ERR_FILEERROR = '424'
3120 ERR_NONICKNAMEGIVEN = '431'
3121 ERR_ERRONEUSNICKNAME = '432'
3122 ERR_NICKNAMEINUSE = '433'
3123 ERR_NICKCOLLISION = '436'
3124 ERR_UNAVAILRESOURCE = '437'
3125 ERR_USERNOTINCHANNEL = '441'
3126 ERR_NOTONCHANNEL = '442'
3127 ERR_USERONCHANNEL = '443'
3129 ERR_SUMMONDISABLED = '445'
3130 ERR_USERSDISABLED = '446'
3131 ERR_NOTREGISTERED = '451'
3132 ERR_NEEDMOREPARAMS = '461'
3133 ERR_ALREADYREGISTRED = '462'
3134 ERR_NOPERMFORHOST = '463'
3135 ERR_PASSWDMISMATCH = '464'
3136 ERR_YOUREBANNEDCREEP = '465'
3137 ERR_YOUWILLBEBANNED = '466'
3139 ERR_CHANNELISFULL = '471'
3140 ERR_UNKNOWNMODE = '472'
3141 ERR_INVITEONLYCHAN = '473'
3142 ERR_BANNEDFROMCHAN = '474'
3143 ERR_BADCHANNELKEY = '475'
3144 ERR_BADCHANMASK = '476'
3145 ERR_NOCHANMODES = '477'
3146 ERR_BANLISTFULL = '478'
3147 ERR_NOPRIVILEGES = '481'
3148 ERR_CHANOPRIVSNEEDED = '482'
3149 ERR_CANTKILLSERVER = '483'
3150 ERR_RESTRICTED = '484'
3151 ERR_UNIQOPPRIVSNEEDED = '485'
3152 ERR_NOOPERHOST = '491'
3153 ERR_NOSERVICEHOST = '492'
3154 ERR_UMODEUNKNOWNFLAG = '501'
3155 ERR_USERSDONTMATCH = '502'
3157 # And hey, as long as the strings are already intern'd...
3158 symbolic_to_numeric = {
3159 "RPL_WELCOME": '001',
3160 "RPL_YOURHOST": '002',
3161 "RPL_CREATED": '003',
3162 "RPL_MYINFO": '004',
3163 "RPL_ISUPPORT": '005',
3164 "RPL_BOUNCE": '010',
3165 "RPL_USERHOST": '302',
3168 "RPL_UNAWAY": '305',
3169 "RPL_NOWAWAY": '306',
3170 "RPL_WHOISUSER": '311',
3171 "RPL_WHOISSERVER": '312',
3172 "RPL_WHOISOPERATOR": '313',
3173 "RPL_WHOISIDLE": '317',
3174 "RPL_ENDOFWHOIS": '318',
3175 "RPL_WHOISCHANNELS": '319',
3176 "RPL_WHOWASUSER": '314',
3177 "RPL_ENDOFWHOWAS": '369',
3178 "RPL_LISTSTART": '321',
3180 "RPL_LISTEND": '323',
3181 "RPL_UNIQOPIS": '325',
3182 "RPL_CHANNELMODEIS": '324',
3183 "RPL_NOTOPIC": '331',
3185 "RPL_INVITING": '341',
3186 "RPL_SUMMONING": '342',
3187 "RPL_INVITELIST": '346',
3188 "RPL_ENDOFINVITELIST": '347',
3189 "RPL_EXCEPTLIST": '348',
3190 "RPL_ENDOFEXCEPTLIST": '349',
3191 "RPL_VERSION": '351',
3192 "RPL_WHOREPLY": '352',
3193 "RPL_ENDOFWHO": '315',
3194 "RPL_NAMREPLY": '353',
3195 "RPL_ENDOFNAMES": '366',
3197 "RPL_ENDOFLINKS": '365',
3198 "RPL_BANLIST": '367',
3199 "RPL_ENDOFBANLIST": '368',
3201 "RPL_ENDOFINFO": '374',
3202 "RPL_MOTDSTART": '375',
3204 "RPL_ENDOFMOTD": '376',
3205 "RPL_YOUREOPER": '381',
3206 "RPL_REHASHING": '382',
3207 "RPL_YOURESERVICE": '383',
3209 "RPL_USERSSTART": '392',
3211 "RPL_ENDOFUSERS": '394',
3212 "RPL_NOUSERS": '395',
3213 "RPL_TRACELINK": '200',
3214 "RPL_TRACECONNECTING": '201',
3215 "RPL_TRACEHANDSHAKE": '202',
3216 "RPL_TRACEUNKNOWN": '203',
3217 "RPL_TRACEOPERATOR": '204',
3218 "RPL_TRACEUSER": '205',
3219 "RPL_TRACESERVER": '206',
3220 "RPL_TRACESERVICE": '207',
3221 "RPL_TRACENEWTYPE": '208',
3222 "RPL_TRACECLASS": '209',
3223 "RPL_TRACERECONNECT": '210',
3224 "RPL_TRACELOG": '261',
3225 "RPL_TRACEEND": '262',
3226 "RPL_STATSLINKINFO": '211',
3227 "RPL_STATSCOMMANDS": '212',
3228 "RPL_ENDOFSTATS": '219',
3229 "RPL_STATSUPTIME": '242',
3230 "RPL_STATSOLINE": '243',
3231 "RPL_UMODEIS": '221',
3232 "RPL_SERVLIST": '234',
3233 "RPL_SERVLISTEND": '235',
3234 "RPL_LUSERCLIENT": '251',
3235 "RPL_LUSEROP": '252',
3236 "RPL_LUSERUNKNOWN": '253',
3237 "RPL_LUSERCHANNELS": '254',
3238 "RPL_LUSERME": '255',
3239 "RPL_ADMINME": '256',
3240 "RPL_ADMINLOC": '257',
3241 "RPL_ADMINLOC": '258',
3242 "RPL_ADMINEMAIL": '259',
3243 "RPL_TRYAGAIN": '263',
3244 "ERR_NOSUCHNICK": '401',
3245 "ERR_NOSUCHSERVER": '402',
3246 "ERR_NOSUCHCHANNEL": '403',
3247 "ERR_CANNOTSENDTOCHAN": '404',
3248 "ERR_TOOMANYCHANNELS": '405',
3249 "ERR_WASNOSUCHNICK": '406',
3250 "ERR_TOOMANYTARGETS": '407',
3251 "ERR_NOSUCHSERVICE": '408',
3252 "ERR_NOORIGIN": '409',
3253 "ERR_NORECIPIENT": '411',
3254 "ERR_NOTEXTTOSEND": '412',
3255 "ERR_NOTOPLEVEL": '413',
3256 "ERR_WILDTOPLEVEL": '414',
3257 "ERR_BADMASK": '415',
3258 "ERR_UNKNOWNCOMMAND": '421',
3259 "ERR_NOMOTD": '422',
3260 "ERR_NOADMININFO": '423',
3261 "ERR_FILEERROR": '424',
3262 "ERR_NONICKNAMEGIVEN": '431',
3263 "ERR_ERRONEUSNICKNAME": '432',
3264 "ERR_NICKNAMEINUSE": '433',
3265 "ERR_NICKCOLLISION": '436',
3266 "ERR_UNAVAILRESOURCE": '437',
3267 "ERR_USERNOTINCHANNEL": '441',
3268 "ERR_NOTONCHANNEL": '442',
3269 "ERR_USERONCHANNEL": '443',
3270 "ERR_NOLOGIN": '444',
3271 "ERR_SUMMONDISABLED": '445',
3272 "ERR_USERSDISABLED": '446',
3273 "ERR_NOTREGISTERED": '451',
3274 "ERR_NEEDMOREPARAMS": '461',
3275 "ERR_ALREADYREGISTRED": '462',
3276 "ERR_NOPERMFORHOST": '463',
3277 "ERR_PASSWDMISMATCH": '464',
3278 "ERR_YOUREBANNEDCREEP": '465',
3279 "ERR_YOUWILLBEBANNED": '466',
3280 "ERR_KEYSET": '467',
3281 "ERR_CHANNELISFULL": '471',
3282 "ERR_UNKNOWNMODE": '472',
3283 "ERR_INVITEONLYCHAN": '473',
3284 "ERR_BANNEDFROMCHAN": '474',
3285 "ERR_BADCHANNELKEY": '475',
3286 "ERR_BADCHANMASK": '476',
3287 "ERR_NOCHANMODES": '477',
3288 "ERR_BANLISTFULL": '478',
3289 "ERR_NOPRIVILEGES": '481',
3290 "ERR_CHANOPRIVSNEEDED": '482',
3291 "ERR_CANTKILLSERVER": '483',
3292 "ERR_RESTRICTED": '484',
3293 "ERR_UNIQOPPRIVSNEEDED": '485',
3294 "ERR_NOOPERHOST": '491',
3295 "ERR_NOSERVICEHOST": '492',
3296 "ERR_UMODEUNKNOWNFLAG": '501',
3297 "ERR_USERSDONTMATCH": '502',
3300 numeric_to_symbolic = {}
3301 for k, v in symbolic_to_numeric.items():
3302 numeric_to_symbolic[v] = k