diff options
Diffstat (limited to 'lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/words/protocols/irc.py')
-rwxr-xr-x | lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/words/protocols/irc.py | 3302 |
1 files changed, 0 insertions, 3302 deletions
diff --git a/lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/words/protocols/irc.py b/lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/words/protocols/irc.py deleted file mode 100755 index 65daa7a9..00000000 --- a/lib/python2.7/site-packages/Twisted-12.2.0-py2.7-linux-x86_64.egg/twisted/words/protocols/irc.py +++ /dev/null @@ -1,3302 +0,0 @@ -# -*- test-case-name: twisted.words.test.test_irc -*- -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. - -""" -Internet Relay Chat Protocol for client and server. - -Future Plans -============ - -The way the IRCClient class works here encourages people to implement -IRC clients by subclassing the ephemeral protocol class, and it tends -to end up with way more state than it should for an object which will -be destroyed as soon as the TCP transport drops. Someone oughta do -something about that, ya know? - -The DCC support needs to have more hooks for the client for it to be -able to ask the user things like "Do you want to accept this session?" -and "Transfer #2 is 67% done." and otherwise manage the DCC sessions. - -Test coverage needs to be better. - -@var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC - 2812 section 2.3. - -@author: Kevin Turner - -@see: RFC 1459: Internet Relay Chat Protocol -@see: RFC 2812: Internet Relay Chat: Client Protocol -@see: U{The Client-To-Client-Protocol -<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>} -""" - -import errno, os, random, re, stat, struct, sys, time, types, traceback -import string, socket -import warnings -import textwrap -from os import path - -from twisted.internet import reactor, protocol, task -from twisted.persisted import styles -from twisted.protocols import basic -from twisted.python import log, reflect, text -from twisted.python.compat import set - -NUL = chr(0) -CR = chr(015) -NL = chr(012) -LF = NL -SPC = chr(040) - -# This includes the CRLF terminator characters. -MAX_COMMAND_LENGTH = 512 - -CHANNEL_PREFIXES = '&#!+' - -class IRCBadMessage(Exception): - pass - -class IRCPasswordMismatch(Exception): - pass - - - -class IRCBadModes(ValueError): - """ - A malformed mode was encountered while attempting to parse a mode string. - """ - - - -def parsemsg(s): - """Breaks a message from an IRC server into its prefix, command, and arguments. - """ - prefix = '' - trailing = [] - if not s: - raise IRCBadMessage("Empty line.") - if s[0] == ':': - prefix, s = s[1:].split(' ', 1) - if s.find(' :') != -1: - s, trailing = s.split(' :', 1) - args = s.split() - args.append(trailing) - else: - args = s.split() - command = args.pop(0) - return prefix, command, args - - - -def split(str, length=80): - """ - Split a string into multiple lines. - - Whitespace near C{str[length]} will be preferred as a breaking point. - C{"\\n"} will also be used as a breaking point. - - @param str: The string to split. - @type str: C{str} - - @param length: The maximum length which will be allowed for any string in - the result. - @type length: C{int} - - @return: C{list} of C{str} - """ - return [chunk - for line in str.split('\n') - for chunk in textwrap.wrap(line, length)] - - -def _intOrDefault(value, default=None): - """ - Convert a value to an integer if possible. - - @rtype: C{int} or type of L{default} - @return: An integer when C{value} can be converted to an integer, - otherwise return C{default} - """ - if value: - try: - return int(value) - except (TypeError, ValueError): - pass - return default - - - -class UnhandledCommand(RuntimeError): - """ - A command dispatcher could not locate an appropriate command handler. - """ - - - -class _CommandDispatcherMixin(object): - """ - Dispatch commands to handlers based on their name. - - Command handler names should be of the form C{prefix_commandName}, - where C{prefix} is the value specified by L{prefix}, and must - accept the parameters as given to L{dispatch}. - - Attempting to mix this in more than once for a single class will cause - strange behaviour, due to L{prefix} being overwritten. - - @type prefix: C{str} - @ivar prefix: Command handler prefix, used to locate handler attributes - """ - prefix = None - - def dispatch(self, commandName, *args): - """ - Perform actual command dispatch. - """ - def _getMethodName(command): - return '%s_%s' % (self.prefix, command) - - def _getMethod(name): - return getattr(self, _getMethodName(name), None) - - method = _getMethod(commandName) - if method is not None: - return method(*args) - - method = _getMethod('unknown') - if method is None: - raise UnhandledCommand("No handler for %r could be found" % (_getMethodName(commandName),)) - return method(commandName, *args) - - - - - -def parseModes(modes, params, paramModes=('', '')): - """ - Parse an IRC mode string. - - The mode string is parsed into two lists of mode changes (added and - removed), with each mode change represented as C{(mode, param)} where mode - is the mode character, and param is the parameter passed for that mode, or - C{None} if no parameter is required. - - @type modes: C{str} - @param modes: Modes string to parse. - - @type params: C{list} - @param params: Parameters specified along with L{modes}. - - @type paramModes: C{(str, str)} - @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take - parameters when added or removed. - - @returns: Two lists of mode changes, one for modes added and the other for - modes removed respectively, mode changes in each list are represented as - C{(mode, param)}. - """ - if len(modes) == 0: - raise IRCBadModes('Empty mode string') - - if modes[0] not in '+-': - raise IRCBadModes('Malformed modes string: %r' % (modes,)) - - changes = ([], []) - - direction = None - count = -1 - for ch in modes: - if ch in '+-': - if count == 0: - raise IRCBadModes('Empty mode sequence: %r' % (modes,)) - direction = '+-'.index(ch) - count = 0 - else: - param = None - if ch in paramModes[direction]: - try: - param = params.pop(0) - except IndexError: - raise IRCBadModes('Not enough parameters: %r' % (ch,)) - changes[direction].append((ch, param)) - count += 1 - - if len(params) > 0: - raise IRCBadModes('Too many parameters: %r %r' % (modes, params)) - - if count == 0: - raise IRCBadModes('Empty mode sequence: %r' % (modes,)) - - return changes - - - -class IRC(protocol.Protocol): - """ - Internet Relay Chat server protocol. - """ - - buffer = "" - hostname = None - - encoding = None - - def connectionMade(self): - self.channels = [] - if self.hostname is None: - self.hostname = socket.getfqdn() - - - def sendLine(self, line): - if self.encoding is not None: - if isinstance(line, unicode): - line = line.encode(self.encoding) - self.transport.write("%s%s%s" % (line, CR, LF)) - - - def sendMessage(self, command, *parameter_list, **prefix): - """ - Send a line formatted as an IRC message. - - First argument is the command, all subsequent arguments are parameters - to that command. If a prefix is desired, it may be specified with the - keyword argument 'prefix'. - """ - - if not command: - raise ValueError, "IRC message requires a command." - - if ' ' in command or command[0] == ':': - # Not the ONLY way to screw up, but provides a little - # sanity checking to catch likely dumb mistakes. - raise ValueError, "Somebody screwed up, 'cuz this doesn't" \ - " look like a command to me: %s" % command - - line = string.join([command] + list(parameter_list)) - if 'prefix' in prefix: - line = ":%s %s" % (prefix['prefix'], line) - self.sendLine(line) - - if len(parameter_list) > 15: - log.msg("Message has %d parameters (RFC allows 15):\n%s" % - (len(parameter_list), line)) - - - def dataReceived(self, data): - """ - This hack is to support mIRC, which sends LF only, even though the RFC - says CRLF. (Also, the flexibility of LineReceiver to turn "line mode" - on and off was not required.) - """ - lines = (self.buffer + data).split(LF) - # Put the (possibly empty) element after the last LF back in the - # buffer - self.buffer = lines.pop() - - for line in lines: - if len(line) <= 2: - # This is a blank line, at best. - continue - if line[-1] == CR: - line = line[:-1] - prefix, command, params = parsemsg(line) - # mIRC is a big pile of doo-doo - command = command.upper() - # DEBUG: log.msg( "%s %s %s" % (prefix, command, params)) - - self.handleCommand(command, prefix, params) - - - def handleCommand(self, command, prefix, params): - """ - Determine the function to call for the given command and call it with - the given arguments. - """ - method = getattr(self, "irc_%s" % command, None) - try: - if method is not None: - method(prefix, params) - else: - self.irc_unknown(prefix, command, params) - except: - log.deferr() - - - def irc_unknown(self, prefix, command, params): - """ - Called by L{handleCommand} on a command that doesn't have a defined - handler. Subclasses should override this method. - """ - raise NotImplementedError(command, prefix, params) - - - # Helper methods - def privmsg(self, sender, recip, message): - """ - Send a message to a channel or user - - @type sender: C{str} or C{unicode} - @param sender: Who is sending this message. Should be of the form - username!ident@hostmask (unless you know better!). - - @type recip: C{str} or C{unicode} - @param recip: The recipient of this message. If a channel, it must - start with a channel prefix. - - @type message: C{str} or C{unicode} - @param message: The message being sent. - """ - self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message))) - - - def notice(self, sender, recip, message): - """ - Send a "notice" to a channel or user. - - Notices differ from privmsgs in that the RFC claims they are different. - Robots are supposed to send notices and not respond to them. Clients - typically display notices differently from privmsgs. - - @type sender: C{str} or C{unicode} - @param sender: Who is sending this message. Should be of the form - username!ident@hostmask (unless you know better!). - - @type recip: C{str} or C{unicode} - @param recip: The recipient of this message. If a channel, it must - start with a channel prefix. - - @type message: C{str} or C{unicode} - @param message: The message being sent. - """ - self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message)) - - - def action(self, sender, recip, message): - """ - Send an action to a channel or user. - - @type sender: C{str} or C{unicode} - @param sender: Who is sending this message. Should be of the form - username!ident@hostmask (unless you know better!). - - @type recip: C{str} or C{unicode} - @param recip: The recipient of this message. If a channel, it must - start with a channel prefix. - - @type message: C{str} or C{unicode} - @param message: The action being sent. - """ - self.sendLine(":%s ACTION %s :%s" % (sender, recip, message)) - - - def topic(self, user, channel, topic, author=None): - """ - Send the topic to a user. - - @type user: C{str} or C{unicode} - @param user: The user receiving the topic. Only their nick name, not - the full hostmask. - - @type channel: C{str} or C{unicode} - @param channel: The channel for which this is the topic. - - @type topic: C{str} or C{unicode} or C{None} - @param topic: The topic string, unquoted, or None if there is no topic. - - @type author: C{str} or C{unicode} - @param author: If the topic is being changed, the full username and - hostmask of the person changing it. - """ - if author is None: - if topic is None: - self.sendLine(':%s %s %s %s :%s' % ( - self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.')) - else: - self.sendLine(":%s %s %s %s :%s" % ( - self.hostname, RPL_TOPIC, user, channel, lowQuote(topic))) - else: - self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic))) - - - def topicAuthor(self, user, channel, author, date): - """ - Send the author of and time at which a topic was set for the given - channel. - - This sends a 333 reply message, which is not part of the IRC RFC. - - @type user: C{str} or C{unicode} - @param user: The user receiving the topic. Only their nick name, not - the full hostmask. - - @type channel: C{str} or C{unicode} - @param channel: The channel for which this information is relevant. - - @type author: C{str} or C{unicode} - @param author: The nickname (without hostmask) of the user who last set - the topic. - - @type date: C{int} - @param date: A POSIX timestamp (number of seconds since the epoch) at - which the topic was last set. - """ - self.sendLine(':%s %d %s %s %s %d' % ( - self.hostname, 333, user, channel, author, date)) - - - def names(self, user, channel, names): - """ - Send the names of a channel's participants to a user. - - @type user: C{str} or C{unicode} - @param user: The user receiving the name list. Only their nick name, - not the full hostmask. - - @type channel: C{str} or C{unicode} - @param channel: The channel for which this is the namelist. - - @type names: C{list} of C{str} or C{unicode} - @param names: The names to send. - """ - # XXX If unicode is given, these limits are not quite correct - prefixLength = len(channel) + len(user) + 10 - namesLength = 512 - prefixLength - - L = [] - count = 0 - for n in names: - if count + len(n) + 1 > namesLength: - self.sendLine(":%s %s %s = %s :%s" % ( - self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L))) - L = [n] - count = len(n) - else: - L.append(n) - count += len(n) + 1 - if L: - self.sendLine(":%s %s %s = %s :%s" % ( - self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L))) - self.sendLine(":%s %s %s %s :End of /NAMES list" % ( - self.hostname, RPL_ENDOFNAMES, user, channel)) - - - def who(self, user, channel, memberInfo): - """ - Send a list of users participating in a channel. - - @type user: C{str} or C{unicode} - @param user: The user receiving this member information. Only their - nick name, not the full hostmask. - - @type channel: C{str} or C{unicode} - @param channel: The channel for which this is the member information. - - @type memberInfo: C{list} of C{tuples} - @param memberInfo: For each member of the given channel, a 7-tuple - containing their username, their hostmask, the server to which they - are connected, their nickname, the letter "H" or "G" (standing for - "Here" or "Gone"), the hopcount from C{user} to this member, and - this member's real name. - """ - for info in memberInfo: - (username, hostmask, server, nickname, flag, hops, realName) = info - assert flag in ("H", "G") - self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % ( - self.hostname, RPL_WHOREPLY, user, channel, - username, hostmask, server, nickname, flag, hops, realName)) - - self.sendLine(":%s %s %s %s :End of /WHO list." % ( - self.hostname, RPL_ENDOFWHO, user, channel)) - - - def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels): - """ - Send information about the state of a particular user. - - @type user: C{str} or C{unicode} - @param user: The user receiving this information. Only their nick name, - not the full hostmask. - - @type nick: C{str} or C{unicode} - @param nick: The nickname of the user this information describes. - - @type username: C{str} or C{unicode} - @param username: The user's username (eg, ident response) - - @type hostname: C{str} - @param hostname: The user's hostmask - - @type realName: C{str} or C{unicode} - @param realName: The user's real name - - @type server: C{str} or C{unicode} - @param server: The name of the server to which the user is connected - - @type serverInfo: C{str} or C{unicode} - @param serverInfo: A descriptive string about that server - - @type oper: C{bool} - @param oper: Indicates whether the user is an IRC operator - - @type idle: C{int} - @param idle: The number of seconds since the user last sent a message - - @type signOn: C{int} - @param signOn: A POSIX timestamp (number of seconds since the epoch) - indicating the time the user signed on - - @type channels: C{list} of C{str} or C{unicode} - @param channels: A list of the channels which the user is participating in - """ - self.sendLine(":%s %s %s %s %s %s * :%s" % ( - self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName)) - self.sendLine(":%s %s %s %s %s :%s" % ( - self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo)) - if oper: - self.sendLine(":%s %s %s %s :is an IRC operator" % ( - self.hostname, RPL_WHOISOPERATOR, user, nick)) - self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % ( - self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn)) - self.sendLine(":%s %s %s %s :%s" % ( - self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels))) - self.sendLine(":%s %s %s %s :End of WHOIS list." % ( - self.hostname, RPL_ENDOFWHOIS, user, nick)) - - - def join(self, who, where): - """ - Send a join message. - - @type who: C{str} or C{unicode} - @param who: The name of the user joining. Should be of the form - username!ident@hostmask (unless you know better!). - - @type where: C{str} or C{unicode} - @param where: The channel the user is joining. - """ - self.sendLine(":%s JOIN %s" % (who, where)) - - - def part(self, who, where, reason=None): - """ - Send a part message. - - @type who: C{str} or C{unicode} - @param who: The name of the user joining. Should be of the form - username!ident@hostmask (unless you know better!). - - @type where: C{str} or C{unicode} - @param where: The channel the user is joining. - - @type reason: C{str} or C{unicode} - @param reason: A string describing the misery which caused this poor - soul to depart. - """ - if reason: - self.sendLine(":%s PART %s :%s" % (who, where, reason)) - else: - self.sendLine(":%s PART %s" % (who, where)) - - - def channelMode(self, user, channel, mode, *args): - """ - Send information about the mode of a channel. - - @type user: C{str} or C{unicode} - @param user: The user receiving the name list. Only their nick name, - not the full hostmask. - - @type channel: C{str} or C{unicode} - @param channel: The channel for which this is the namelist. - - @type mode: C{str} - @param mode: A string describing this channel's modes. - - @param args: Any additional arguments required by the modes. - """ - self.sendLine(":%s %s %s %s %s %s" % ( - self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args))) - - - -class ServerSupportedFeatures(_CommandDispatcherMixin): - """ - Handle ISUPPORT messages. - - Feature names match those in the ISUPPORT RFC draft identically. - - Information regarding the specifics of ISUPPORT was gleaned from - <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>. - """ - prefix = 'isupport' - - def __init__(self): - self._features = { - 'CHANNELLEN': 200, - 'CHANTYPES': tuple('#&'), - 'MODES': 3, - 'NICKLEN': 9, - 'PREFIX': self._parsePrefixParam('(ovh)@+%'), - # The ISUPPORT draft explicitly says that there is no default for - # CHANMODES, but we're defaulting it here to handle the case where - # the IRC server doesn't send us any ISUPPORT information, since - # IRCClient.getChannelModeParams relies on this value. - 'CHANMODES': self._parseChanModesParam(['b', '', 'lk'])} - - - def _splitParamArgs(cls, params, valueProcessor=None): - """ - Split ISUPPORT parameter arguments. - - Values can optionally be processed by C{valueProcessor}. - - For example:: - - >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2']) - (('A', '1'), ('B', '2')) - - @type params: C{iterable} of C{str} - - @type valueProcessor: C{callable} taking {str} - @param valueProcessor: Callable to process argument values, or C{None} - to perform no processing - - @rtype: C{list} of C{(str, object)} - @return: Sequence of C{(name, processedValue)} - """ - if valueProcessor is None: - valueProcessor = lambda x: x - - def _parse(): - for param in params: - if ':' not in param: - param += ':' - a, b = param.split(':', 1) - yield a, valueProcessor(b) - return list(_parse()) - _splitParamArgs = classmethod(_splitParamArgs) - - - def _unescapeParamValue(cls, value): - """ - Unescape an ISUPPORT parameter. - - The only form of supported escape is C{\\xHH}, where HH must be a valid - 2-digit hexadecimal number. - - @rtype: C{str} - """ - def _unescape(): - parts = value.split('\\x') - # The first part can never be preceeded by the escape. - yield parts.pop(0) - for s in parts: - octet, rest = s[:2], s[2:] - try: - octet = int(octet, 16) - except ValueError: - raise ValueError('Invalid hex octet: %r' % (octet,)) - yield chr(octet) + rest - - if '\\x' not in value: - return value - return ''.join(_unescape()) - _unescapeParamValue = classmethod(_unescapeParamValue) - - - def _splitParam(cls, param): - """ - Split an ISUPPORT parameter. - - @type param: C{str} - - @rtype: C{(str, list)} - @return C{(key, arguments)} - """ - if '=' not in param: - param += '=' - key, value = param.split('=', 1) - return key, map(cls._unescapeParamValue, value.split(',')) - _splitParam = classmethod(_splitParam) - - - def _parsePrefixParam(cls, prefix): - """ - Parse the ISUPPORT "PREFIX" parameter. - - The order in which the parameter arguments appear is significant, the - earlier a mode appears the more privileges it gives. - - @rtype: C{dict} mapping C{str} to C{(str, int)} - @return: A dictionary mapping a mode character to a two-tuple of - C({symbol, priority)}, the lower a priority (the lowest being - C{0}) the more privileges it gives - """ - if not prefix: - return None - if prefix[0] != '(' and ')' not in prefix: - raise ValueError('Malformed PREFIX parameter') - modes, symbols = prefix.split(')', 1) - symbols = zip(symbols, xrange(len(symbols))) - modes = modes[1:] - return dict(zip(modes, symbols)) - _parsePrefixParam = classmethod(_parsePrefixParam) - - - def _parseChanModesParam(self, params): - """ - Parse the ISUPPORT "CHANMODES" parameter. - - See L{isupport_CHANMODES} for a detailed explanation of this parameter. - """ - names = ('addressModes', 'param', 'setParam', 'noParam') - if len(params) > len(names): - raise ValueError( - 'Expecting a maximum of %d channel mode parameters, got %d' % ( - len(names), len(params))) - items = map(lambda key, value: (key, value or ''), names, params) - return dict(items) - _parseChanModesParam = classmethod(_parseChanModesParam) - - - def getFeature(self, feature, default=None): - """ - Get a server supported feature's value. - - A feature with the value C{None} is equivalent to the feature being - unsupported. - - @type feature: C{str} - @param feature: Feature name - - @type default: C{object} - @param default: The value to default to, assuming that C{feature} - is not supported - - @return: Feature value - """ - return self._features.get(feature, default) - - - def hasFeature(self, feature): - """ - Determine whether a feature is supported or not. - - @rtype: C{bool} - """ - return self.getFeature(feature) is not None - - - def parse(self, params): - """ - Parse ISUPPORT parameters. - - If an unknown parameter is encountered, it is simply added to the - dictionary, keyed by its name, as a tuple of the parameters provided. - - @type params: C{iterable} of C{str} - @param params: Iterable of ISUPPORT parameters to parse - """ - for param in params: - key, value = self._splitParam(param) - if key.startswith('-'): - self._features.pop(key[1:], None) - else: - self._features[key] = self.dispatch(key, value) - - - def isupport_unknown(self, command, params): - """ - Unknown ISUPPORT parameter. - """ - return tuple(params) - - - def isupport_CHANLIMIT(self, params): - """ - The maximum number of each channel type a user may join. - """ - return self._splitParamArgs(params, _intOrDefault) - - - def isupport_CHANMODES(self, params): - """ - Available channel modes. - - There are 4 categories of channel mode:: - - addressModes - Modes that add or remove an address to or from a - list, these modes always take a parameter. - - param - Modes that change a setting on a channel, these modes - always take a parameter. - - setParam - Modes that change a setting on a channel, these modes - only take a parameter when being set. - - noParam - Modes that change a setting on a channel, these modes - never take a parameter. - """ - try: - return self._parseChanModesParam(params) - except ValueError: - return self.getFeature('CHANMODES') - - - def isupport_CHANNELLEN(self, params): - """ - Maximum length of a channel name a client may create. - """ - return _intOrDefault(params[0], self.getFeature('CHANNELLEN')) - - - def isupport_CHANTYPES(self, params): - """ - Valid channel prefixes. - """ - return tuple(params[0]) - - - def isupport_EXCEPTS(self, params): - """ - Mode character for "ban exceptions". - - The presence of this parameter indicates that the server supports - this functionality. - """ - return params[0] or 'e' - - - def isupport_IDCHAN(self, params): - """ - Safe channel identifiers. - - The presence of this parameter indicates that the server supports - this functionality. - """ - return self._splitParamArgs(params) - - - def isupport_INVEX(self, params): - """ - Mode character for "invite exceptions". - - The presence of this parameter indicates that the server supports - this functionality. - """ - return params[0] or 'I' - - - def isupport_KICKLEN(self, params): - """ - Maximum length of a kick message a client may provide. - """ - return _intOrDefault(params[0]) - - - def isupport_MAXLIST(self, params): - """ - Maximum number of "list modes" a client may set on a channel at once. - - List modes are identified by the "addressModes" key in CHANMODES. - """ - return self._splitParamArgs(params, _intOrDefault) - - - def isupport_MODES(self, params): - """ - Maximum number of modes accepting parameters that may be sent, by a - client, in a single MODE command. - """ - return _intOrDefault(params[0]) - - - def isupport_NETWORK(self, params): - """ - IRC network name. - """ - return params[0] - - - def isupport_NICKLEN(self, params): - """ - Maximum length of a nickname the client may use. - """ - return _intOrDefault(params[0], self.getFeature('NICKLEN')) - - - def isupport_PREFIX(self, params): - """ - Mapping of channel modes that clients may have to status flags. - """ - try: - return self._parsePrefixParam(params[0]) - except ValueError: - return self.getFeature('PREFIX') - - - def isupport_SAFELIST(self, params): - """ - Flag indicating that a client may request a LIST without being - disconnected due to the large amount of data generated. - """ - return True - - - def isupport_STATUSMSG(self, params): - """ - The server supports sending messages to only to clients on a channel - with a specific status. - """ - return params[0] - - - def isupport_TARGMAX(self, params): - """ - Maximum number of targets allowable for commands that accept multiple - targets. - """ - return dict(self._splitParamArgs(params, _intOrDefault)) - - - def isupport_TOPICLEN(self, params): - """ - Maximum length of a topic that may be set. - """ - return _intOrDefault(params[0]) - - - -class IRCClient(basic.LineReceiver): - """ - Internet Relay Chat client protocol, with sprinkles. - - In addition to providing an interface for an IRC client protocol, - this class also contains reasonable implementations of many common - CTCP methods. - - TODO - ==== - - Limit the length of messages sent (because the IRC server probably - does). - - Add flood protection/rate limiting for my CTCP replies. - - NickServ cooperation. (a mix-in?) - - @ivar nickname: Nickname the client will use. - @ivar password: Password used to log on to the server. May be C{None}. - @ivar realname: Supplied to the server during login as the "Real name" - or "ircname". May be C{None}. - @ivar username: Supplied to the server during login as the "User name". - May be C{None} - - @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query. If C{None}, no - USERINFO reply will be sent. - "This is used to transmit a string which is settable by - the user (and never should be set by the client)." - @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query. If C{None}, no - FINGER reply will be sent. - @type fingerReply: Callable or String - - @ivar versionName: CTCP VERSION reply, client name. If C{None}, no VERSION - reply will be sent. - @type versionName: C{str}, or None. - @ivar versionNum: CTCP VERSION reply, client version. - @type versionNum: C{str}, or None. - @ivar versionEnv: CTCP VERSION reply, environment the client is running in. - @type versionEnv: C{str}, or None. - - @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this - client may be found. If C{None}, no SOURCE reply will be sent. - - @ivar lineRate: Minimum delay between lines sent to the server. If - C{None}, no delay will be imposed. - @type lineRate: Number of Seconds. - - @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and - I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content - of an I{RPL_MOTD} message. - - @ivar erroneousNickFallback: Default nickname assigned when an unregistered - client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register - with an illegal nickname. - @type erroneousNickFallback: C{str} - - @ivar _registered: Whether or not the user is registered. It becomes True - once a welcome has been received from the server. - @type _registered: C{bool} - - @ivar _attemptedNick: The nickname that will try to get registered. It may - change if it is illegal or already taken. L{nickname} becomes the - L{_attemptedNick} that is successfully registered. - @type _attemptedNick: C{str} - - @type supported: L{ServerSupportedFeatures} - @ivar supported: Available ISUPPORT features on the server - - @type hostname: C{str} - @ivar hostname: Host name of the IRC server the client is connected to. - Initially the host name is C{None} and later is set to the host name - from which the I{RPL_WELCOME} message is received. - - @type _heartbeat: L{task.LoopingCall} - @ivar _heartbeat: Looping call to perform the keepalive by calling - L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or - C{None} if there is no heartbeat. - - @type heartbeatInterval: C{float} - @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to - the server as a form of keepalive, defaults to 120 seconds. Use C{None} - to disable the heartbeat. - """ - hostname = None - motd = None - nickname = 'irc' - password = None - realname = None - username = None - ### Responses to various CTCP queries. - - userinfo = None - # fingerReply is a callable returning a string, or a str()able object. - fingerReply = None - versionName = None - versionNum = None - versionEnv = None - - sourceURL = "http://twistedmatrix.com/downloads/" - - dcc_destdir = '.' - dcc_sessions = None - - # If this is false, no attempt will be made to identify - # ourself to the server. - performLogin = 1 - - lineRate = None - _queue = None - _queueEmptying = None - - delimiter = '\n' # '\r\n' will also work (see dataReceived) - - __pychecker__ = 'unusednames=params,prefix,channel' - - _registered = False - _attemptedNick = '' - erroneousNickFallback = 'defaultnick' - - _heartbeat = None - heartbeatInterval = 120 - - - def _reallySendLine(self, line): - return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r') - - def sendLine(self, line): - if self.lineRate is None: - self._reallySendLine(line) - else: - self._queue.append(line) - if not self._queueEmptying: - self._sendLine() - - def _sendLine(self): - if self._queue: - self._reallySendLine(self._queue.pop(0)) - self._queueEmptying = reactor.callLater(self.lineRate, - self._sendLine) - else: - self._queueEmptying = None - - - def connectionLost(self, reason): - basic.LineReceiver.connectionLost(self, reason) - self.stopHeartbeat() - - - def _createHeartbeat(self): - """ - Create the heartbeat L{LoopingCall}. - """ - return task.LoopingCall(self._sendHeartbeat) - - - def _sendHeartbeat(self): - """ - Send a I{PING} message to the IRC server as a form of keepalive. - """ - self.sendLine('PING ' + self.hostname) - - - def stopHeartbeat(self): - """ - Stop sending I{PING} messages to keep the connection to the server - alive. - - @since: 11.1 - """ - if self._heartbeat is not None: - self._heartbeat.stop() - self._heartbeat = None - - - def startHeartbeat(self): - """ - Start sending I{PING} messages every L{IRCClient.heartbeatInterval} - seconds to keep the connection to the server alive during periods of no - activity. - - @since: 11.1 - """ - self.stopHeartbeat() - if self.heartbeatInterval is None: - return - self._heartbeat = self._createHeartbeat() - self._heartbeat.start(self.heartbeatInterval, now=False) - - - ### Interface level client->user output methods - ### - ### You'll want to override these. - - ### Methods relating to the server itself - - def created(self, when): - """ - Called with creation date information about the server, usually at logon. - - @type when: C{str} - @param when: A string describing when the server was created, probably. - """ - - def yourHost(self, info): - """ - Called with daemon information about the server, usually at logon. - - @type info: C{str} - @param when: A string describing what software the server is running, probably. - """ - - def myInfo(self, servername, version, umodes, cmodes): - """ - Called with information about the server, usually at logon. - - @type servername: C{str} - @param servername: The hostname of this server. - - @type version: C{str} - @param version: A description of what software this server runs. - - @type umodes: C{str} - @param umodes: All the available user modes. - - @type cmodes: C{str} - @param cmodes: All the available channel modes. - """ - - def luserClient(self, info): - """ - Called with information about the number of connections, usually at logon. - - @type info: C{str} - @param info: A description of the number of clients and servers - connected to the network, probably. - """ - - def bounce(self, info): - """ - Called with information about where the client should reconnect. - - @type info: C{str} - @param info: A plaintext description of the address that should be - connected to. - """ - - def isupport(self, options): - """ - Called with various information about what the server supports. - - @type options: C{list} of C{str} - @param options: Descriptions of features or limits of the server, possibly - in the form "NAME=VALUE". - """ - - def luserChannels(self, channels): - """ - Called with the number of channels existant on the server. - - @type channels: C{int} - """ - - def luserOp(self, ops): - """ - Called with the number of ops logged on to the server. - - @type ops: C{int} - """ - - def luserMe(self, info): - """ - Called with information about the server connected to. - - @type info: C{str} - @param info: A plaintext string describing the number of users and servers - connected to this server. - """ - - ### Methods involving me directly - - def privmsg(self, user, channel, message): - """ - Called when I have a message from a user to me or a channel. - """ - pass - - def joined(self, channel): - """ - Called when I finish joining a channel. - - channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'}) - intact. - """ - - def left(self, channel): - """ - Called when I have left a channel. - - channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'}) - intact. - """ - - - def noticed(self, user, channel, message): - """ - Called when I have a notice from a user to me or a channel. - - If the client makes any automated replies, it must not do so in - response to a NOTICE message, per the RFC:: - - The difference between NOTICE and PRIVMSG is that - automatic replies MUST NEVER be sent in response to a - NOTICE message. [...] The object of this rule is to avoid - loops between clients automatically sending something in - response to something it received. - """ - - - def modeChanged(self, user, channel, set, modes, args): - """ - Called when users or channel's modes are changed. - - @type user: C{str} - @param user: The user and hostmask which instigated this change. - - @type channel: C{str} - @param channel: The channel where the modes are changed. If args is - empty the channel for which the modes are changing. If the changes are - at server level it could be equal to C{user}. - - @type set: C{bool} or C{int} - @param set: True if the mode(s) is being added, False if it is being - removed. If some modes are added and others removed at the same time - this function will be called twice, the first time with all the added - modes, the second with the removed ones. (To change this behaviour - override the irc_MODE method) - - @type modes: C{str} - @param modes: The mode or modes which are being changed. - - @type args: C{tuple} - @param args: Any additional information required for the mode - change. - """ - - def pong(self, user, secs): - """ - Called with the results of a CTCP PING query. - """ - pass - - def signedOn(self): - """ - Called after sucessfully signing on to the server. - """ - pass - - def kickedFrom(self, channel, kicker, message): - """ - Called when I am kicked from a channel. - """ - pass - - def nickChanged(self, nick): - """ - Called when my nick has been changed. - """ - self.nickname = nick - - - ### Things I observe other people doing in a channel. - - def userJoined(self, user, channel): - """ - Called when I see another user joining a channel. - """ - pass - - def userLeft(self, user, channel): - """ - Called when I see another user leaving a channel. - """ - pass - - def userQuit(self, user, quitMessage): - """ - Called when I see another user disconnect from the network. - """ - pass - - def userKicked(self, kickee, channel, kicker, message): - """ - Called when I observe someone else being kicked from a channel. - """ - pass - - def action(self, user, channel, data): - """ - Called when I see a user perform an ACTION on a channel. - """ - pass - - def topicUpdated(self, user, channel, newTopic): - """ - In channel, user changed the topic to newTopic. - - Also called when first joining a channel. - """ - pass - - def userRenamed(self, oldname, newname): - """ - A user changed their name from oldname to newname. - """ - pass - - ### Information from the server. - - def receivedMOTD(self, motd): - """ - I received a message-of-the-day banner from the server. - - motd is a list of strings, where each string was sent as a seperate - message from the server. To display, you might want to use:: - - '\\n'.join(motd) - - to get a nicely formatted string. - """ - pass - - ### user input commands, client->server - ### Your client will want to invoke these. - - def join(self, channel, key=None): - """ - Join a channel. - - @type channel: C{str} - @param channel: The name of the channel to join. If it has no prefix, - C{'#'} will be prepended to it. - @type key: C{str} - @param key: If specified, the key used to join the channel. - """ - if channel[0] not in CHANNEL_PREFIXES: - channel = '#' + channel - if key: - self.sendLine("JOIN %s %s" % (channel, key)) - else: - self.sendLine("JOIN %s" % (channel,)) - - def leave(self, channel, reason=None): - """ - Leave a channel. - - @type channel: C{str} - @param channel: The name of the channel to leave. If it has no prefix, - C{'#'} will be prepended to it. - @type reason: C{str} - @param reason: If given, the reason for leaving. - """ - if channel[0] not in CHANNEL_PREFIXES: - channel = '#' + channel - if reason: - self.sendLine("PART %s :%s" % (channel, reason)) - else: - self.sendLine("PART %s" % (channel,)) - - def kick(self, channel, user, reason=None): - """ - Attempt to kick a user from a channel. - - @type channel: C{str} - @param channel: The name of the channel to kick the user from. If it has - no prefix, C{'#'} will be prepended to it. - @type user: C{str} - @param user: The nick of the user to kick. - @type reason: C{str} - @param reason: If given, the reason for kicking the user. - """ - if channel[0] not in CHANNEL_PREFIXES: - channel = '#' + channel - if reason: - self.sendLine("KICK %s %s :%s" % (channel, user, reason)) - else: - self.sendLine("KICK %s %s" % (channel, user)) - - part = leave - - - def invite(self, user, channel): - """ - Attempt to invite user to channel - - @type user: C{str} - @param user: The user to invite - @type channel: C{str} - @param channel: The channel to invite the user too - - @since: 11.0 - """ - if channel[0] not in CHANNEL_PREFIXES: - channel = '#' + channel - self.sendLine("INVITE %s %s" % (user, channel)) - - - def topic(self, channel, topic=None): - """ - Attempt to set the topic of the given channel, or ask what it is. - - If topic is None, then I sent a topic query instead of trying to set the - topic. The server should respond with a TOPIC message containing the - current topic of the given channel. - - @type channel: C{str} - @param channel: The name of the channel to change the topic on. If it - has no prefix, C{'#'} will be prepended to it. - @type topic: C{str} - @param topic: If specified, what to set the topic to. - """ - # << TOPIC #xtestx :fff - if channel[0] not in CHANNEL_PREFIXES: - channel = '#' + channel - if topic != None: - self.sendLine("TOPIC %s :%s" % (channel, topic)) - else: - self.sendLine("TOPIC %s" % (channel,)) - - - def mode(self, chan, set, modes, limit = None, user = None, mask = None): - """ - Change the modes on a user or channel. - - The C{limit}, C{user}, and C{mask} parameters are mutually exclusive. - - @type chan: C{str} - @param chan: The name of the channel to operate on. - @type set: C{bool} - @param set: True to give the user or channel permissions and False to - remove them. - @type modes: C{str} - @param modes: The mode flags to set on the user or channel. - @type limit: C{int} - @param limit: In conjuction with the C{'l'} mode flag, limits the - number of users on the channel. - @type user: C{str} - @param user: The user to change the mode on. - @type mask: C{str} - @param mask: In conjuction with the C{'b'} mode flag, sets a mask of - users to be banned from the channel. - """ - if set: - line = 'MODE %s +%s' % (chan, modes) - else: - line = 'MODE %s -%s' % (chan, modes) - if limit is not None: - line = '%s %d' % (line, limit) - elif user is not None: - line = '%s %s' % (line, user) - elif mask is not None: - line = '%s %s' % (line, mask) - self.sendLine(line) - - - def say(self, channel, message, length=None): - """ - Send a message to a channel - - @type channel: C{str} - @param channel: The channel to say the message on. If it has no prefix, - C{'#'} will be prepended to it. - @type message: C{str} - @param message: The message to say. - @type length: C{int} - @param length: The maximum number of octets to send at a time. This has - the effect of turning a single call to C{msg()} into multiple - commands to the server. This is useful when long messages may be - sent that would otherwise cause the server to kick us off or - silently truncate the text we are sending. If None is passed, the - entire message is always send in one command. - """ - if channel[0] not in CHANNEL_PREFIXES: - channel = '#' + channel - self.msg(channel, message, length) - - - def _safeMaximumLineLength(self, command): - """ - Estimate a safe maximum line length for the given command. - - This is done by assuming the maximum values for nickname length, - realname and hostname combined with the command that needs to be sent - and some guessing. A theoretical maximum value is used because it is - possible that our nickname, username or hostname changes (on the server - side) while the length is still being calculated. - """ - # :nickname!realname@hostname COMMAND ... - theoretical = ':%s!%s@%s %s' % ( - 'a' * self.supported.getFeature('NICKLEN'), - # This value is based on observation. - 'b' * 10, - # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>. - 'c' * 63, - command) - # Fingers crossed. - fudge = 10 - return MAX_COMMAND_LENGTH - len(theoretical) - fudge - - - def msg(self, user, message, length=None): - """ - Send a message to a user or channel. - - The message will be split into multiple commands to the server if: - - The message contains any newline characters - - Any span between newline characters is longer than the given - line-length. - - @param user: Username or channel name to which to direct the - message. - @type user: C{str} - - @param message: Text to send. - @type message: C{str} - - @param length: Maximum number of octets to send in a single - command, including the IRC protocol framing. If C{None} is given - then L{IRCClient._safeMaximumLineLength} is used to determine a - value. - @type length: C{int} - """ - fmt = 'PRIVMSG %s :' % (user,) - - if length is None: - length = self._safeMaximumLineLength(fmt) - - # Account for the line terminator. - minimumLength = len(fmt) + 2 - if length <= minimumLength: - raise ValueError("Maximum length must exceed %d for message " - "to %s" % (minimumLength, user)) - for line in split(message, length - minimumLength): - self.sendLine(fmt + line) - - - def notice(self, user, message): - """ - Send a notice to a user. - - Notices are like normal message, but should never get automated - replies. - - @type user: C{str} - @param user: The user to send a notice to. - @type message: C{str} - @param message: The contents of the notice to send. - """ - self.sendLine("NOTICE %s :%s" % (user, message)) - - - def away(self, message=''): - """ - Mark this client as away. - - @type message: C{str} - @param message: If specified, the away message. - """ - self.sendLine("AWAY :%s" % message) - - - def back(self): - """ - Clear the away status. - """ - # An empty away marks us as back - self.away() - - - def whois(self, nickname, server=None): - """ - Retrieve user information about the given nick name. - - @type nickname: C{str} - @param nickname: The nick name about which to retrieve information. - - @since: 8.2 - """ - if server is None: - self.sendLine('WHOIS ' + nickname) - else: - self.sendLine('WHOIS %s %s' % (server, nickname)) - - - def register(self, nickname, hostname='foo', servername='bar'): - """ - Login to the server. - - @type nickname: C{str} - @param nickname: The nickname to register. - @type hostname: C{str} - @param hostname: If specified, the hostname to logon as. - @type servername: C{str} - @param servername: If specified, the servername to logon as. - """ - if self.password is not None: - self.sendLine("PASS %s" % self.password) - self.setNick(nickname) - if self.username is None: - self.username = nickname - self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname)) - - - def setNick(self, nickname): - """ - Set this client's nickname. - - @type nickname: C{str} - @param nickname: The nickname to change to. - """ - self._attemptedNick = nickname - self.sendLine("NICK %s" % nickname) - - - def quit(self, message = ''): - """ - Disconnect from the server - - @type message: C{str} - - @param message: If specified, the message to give when quitting the - server. - """ - self.sendLine("QUIT :%s" % message) - - ### user input commands, client->client - - def describe(self, channel, action): - """ - Strike a pose. - - @type channel: C{str} - @param channel: The name of the channel to have an action on. If it - has no prefix, it is sent to the user of that name. - @type action: C{str} - @param action: The action to preform. - @since: 9.0 - """ - self.ctcpMakeQuery(channel, [('ACTION', action)]) - - - _pings = None - _MAX_PINGRING = 12 - - def ping(self, user, text = None): - """ - Measure round-trip delay to another IRC client. - """ - if self._pings is None: - self._pings = {} - - if text is None: - chars = string.letters + string.digits + string.punctuation - key = ''.join([random.choice(chars) for i in range(12)]) - else: - key = str(text) - self._pings[(user, key)] = time.time() - self.ctcpMakeQuery(user, [('PING', key)]) - - if len(self._pings) > self._MAX_PINGRING: - # Remove some of the oldest entries. - byValue = [(v, k) for (k, v) in self._pings.items()] - byValue.sort() - excess = self._MAX_PINGRING - len(self._pings) - for i in xrange(excess): - del self._pings[byValue[i][1]] - - - def dccSend(self, user, file): - if type(file) == types.StringType: - file = open(file, 'r') - - size = fileSize(file) - - name = getattr(file, "name", "file@%s" % (id(file),)) - - factory = DccSendFactory(file) - port = reactor.listenTCP(0, factory, 1) - - raise NotImplementedError,( - "XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. " - "(and stop accepting once we've made a single connection.)") - - my_address = struct.pack("!I", my_address) - - args = ['SEND', name, my_address, str(port)] - - if not (size is None): - args.append(size) - - args = string.join(args, ' ') - - self.ctcpMakeQuery(user, [('DCC', args)]) - - - def dccResume(self, user, fileName, port, resumePos): - """ - Send a DCC RESUME request to another user. - """ - self.ctcpMakeQuery(user, [ - ('DCC', ['RESUME', fileName, port, resumePos])]) - - - def dccAcceptResume(self, user, fileName, port, resumePos): - """ - Send a DCC ACCEPT response to clients who have requested a resume. - """ - self.ctcpMakeQuery(user, [ - ('DCC', ['ACCEPT', fileName, port, resumePos])]) - - ### server->client messages - ### You might want to fiddle with these, - ### but it is safe to leave them alone. - - def irc_ERR_NICKNAMEINUSE(self, prefix, params): - """ - Called when we try to register or change to a nickname that is already - taken. - """ - self._attemptedNick = self.alterCollidedNick(self._attemptedNick) - self.setNick(self._attemptedNick) - - - def alterCollidedNick(self, nickname): - """ - Generate an altered version of a nickname that caused a collision in an - effort to create an unused related name for subsequent registration. - - @param nickname: The nickname a user is attempting to register. - @type nickname: C{str} - - @returns: A string that is in some way different from the nickname. - @rtype: C{str} - """ - return nickname + '_' - - - def irc_ERR_ERRONEUSNICKNAME(self, prefix, params): - """ - Called when we try to register or change to an illegal nickname. - - The server should send this reply when the nickname contains any - disallowed characters. The bot will stall, waiting for RPL_WELCOME, if - we don't handle this during sign-on. - - @note: The method uses the spelling I{erroneus}, as it appears in - the RFC, section 6.1. - """ - if not self._registered: - self.setNick(self.erroneousNickFallback) - - - def irc_ERR_PASSWDMISMATCH(self, prefix, params): - """ - Called when the login was incorrect. - """ - raise IRCPasswordMismatch("Password Incorrect.") - - - def irc_RPL_WELCOME(self, prefix, params): - """ - Called when we have received the welcome from the server. - """ - self.hostname = prefix - self._registered = True - self.nickname = self._attemptedNick - self.signedOn() - self.startHeartbeat() - - - def irc_JOIN(self, prefix, params): - """ - Called when a user joins a channel. - """ - nick = string.split(prefix,'!')[0] - channel = params[-1] - if nick == self.nickname: - self.joined(channel) - else: - self.userJoined(nick, channel) - - def irc_PART(self, prefix, params): - """ - Called when a user leaves a channel. - """ - nick = string.split(prefix,'!')[0] - channel = params[0] - if nick == self.nickname: - self.left(channel) - else: - self.userLeft(nick, channel) - - def irc_QUIT(self, prefix, params): - """ - Called when a user has quit. - """ - nick = string.split(prefix,'!')[0] - self.userQuit(nick, params[0]) - - - def irc_MODE(self, user, params): - """ - Parse a server mode change message. - """ - channel, modes, args = params[0], params[1], params[2:] - - if modes[0] not in '-+': - modes = '+' + modes - - if channel == self.nickname: - # This is a mode change to our individual user, not a channel mode - # that involves us. - paramModes = self.getUserModeParams() - else: - paramModes = self.getChannelModeParams() - - try: - added, removed = parseModes(modes, args, paramModes) - except IRCBadModes: - log.err(None, 'An error occured while parsing the following ' - 'MODE message: MODE %s' % (' '.join(params),)) - else: - if added: - modes, params = zip(*added) - self.modeChanged(user, channel, True, ''.join(modes), params) - - if removed: - modes, params = zip(*removed) - self.modeChanged(user, channel, False, ''.join(modes), params) - - - def irc_PING(self, prefix, params): - """ - Called when some has pinged us. - """ - self.sendLine("PONG %s" % params[-1]) - - def irc_PRIVMSG(self, prefix, params): - """ - Called when we get a message. - """ - user = prefix - channel = params[0] - message = params[-1] - - if not message: - # Don't raise an exception if we get blank message. - return - - if message[0] == X_DELIM: - m = ctcpExtract(message) - if m['extended']: - self.ctcpQuery(user, channel, m['extended']) - - if not m['normal']: - return - - message = string.join(m['normal'], ' ') - - self.privmsg(user, channel, message) - - def irc_NOTICE(self, prefix, params): - """ - Called when a user gets a notice. - """ - user = prefix - channel = params[0] - message = params[-1] - - if message[0]==X_DELIM: - m = ctcpExtract(message) - if m['extended']: - self.ctcpReply(user, channel, m['extended']) - - if not m['normal']: - return - - message = string.join(m['normal'], ' ') - - self.noticed(user, channel, message) - - def irc_NICK(self, prefix, params): - """ - Called when a user changes their nickname. - """ - nick = string.split(prefix,'!', 1)[0] - if nick == self.nickname: - self.nickChanged(params[0]) - else: - self.userRenamed(nick, params[0]) - - def irc_KICK(self, prefix, params): - """ - Called when a user is kicked from a channel. - """ - kicker = string.split(prefix,'!')[0] - channel = params[0] - kicked = params[1] - message = params[-1] - if string.lower(kicked) == string.lower(self.nickname): - # Yikes! - self.kickedFrom(channel, kicker, message) - else: - self.userKicked(kicked, channel, kicker, message) - - def irc_TOPIC(self, prefix, params): - """ - Someone in the channel set the topic. - """ - user = string.split(prefix, '!')[0] - channel = params[0] - newtopic = params[1] - self.topicUpdated(user, channel, newtopic) - - def irc_RPL_TOPIC(self, prefix, params): - """ - Called when the topic for a channel is initially reported or when it - subsequently changes. - """ - user = string.split(prefix, '!')[0] - channel = params[1] - newtopic = params[2] - self.topicUpdated(user, channel, newtopic) - - def irc_RPL_NOTOPIC(self, prefix, params): - user = string.split(prefix, '!')[0] - channel = params[1] - newtopic = "" - self.topicUpdated(user, channel, newtopic) - - def irc_RPL_MOTDSTART(self, prefix, params): - if params[-1].startswith("- "): - params[-1] = params[-1][2:] - self.motd = [params[-1]] - - def irc_RPL_MOTD(self, prefix, params): - if params[-1].startswith("- "): - params[-1] = params[-1][2:] - if self.motd is None: - self.motd = [] - self.motd.append(params[-1]) - - - def irc_RPL_ENDOFMOTD(self, prefix, params): - """ - I{RPL_ENDOFMOTD} indicates the end of the message of the day - messages. Deliver the accumulated lines to C{receivedMOTD}. - """ - motd = self.motd - self.motd = None - self.receivedMOTD(motd) - - - def irc_RPL_CREATED(self, prefix, params): - self.created(params[1]) - - def irc_RPL_YOURHOST(self, prefix, params): - self.yourHost(params[1]) - - def irc_RPL_MYINFO(self, prefix, params): - info = params[1].split(None, 3) - while len(info) < 4: - info.append(None) - self.myInfo(*info) - - def irc_RPL_BOUNCE(self, prefix, params): - self.bounce(params[1]) - - def irc_RPL_ISUPPORT(self, prefix, params): - args = params[1:-1] - # Several ISUPPORT messages, in no particular order, may be sent - # to the client at any given point in time (usually only on connect, - # though.) For this reason, ServerSupportedFeatures.parse is intended - # to mutate the supported feature list. - self.supported.parse(args) - self.isupport(args) - - def irc_RPL_LUSERCLIENT(self, prefix, params): - self.luserClient(params[1]) - - def irc_RPL_LUSEROP(self, prefix, params): - try: - self.luserOp(int(params[1])) - except ValueError: - pass - - def irc_RPL_LUSERCHANNELS(self, prefix, params): - try: - self.luserChannels(int(params[1])) - except ValueError: - pass - - def irc_RPL_LUSERME(self, prefix, params): - self.luserMe(params[1]) - - def irc_unknown(self, prefix, command, params): - pass - - ### Receiving a CTCP query from another party - ### It is safe to leave these alone. - - - def ctcpQuery(self, user, channel, messages): - """ - Dispatch method for any CTCP queries received. - - Duplicated CTCP queries are ignored and no dispatch is - made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}. - """ - seen = set() - for tag, data in messages: - method = getattr(self, 'ctcpQuery_%s' % tag, None) - if tag not in seen: - if method is not None: - method(user, channel, data) - else: - self.ctcpUnknownQuery(user, channel, tag, data) - seen.add(tag) - - - def ctcpUnknownQuery(self, user, channel, tag, data): - """ - Fallback handler for unrecognized CTCP queries. - - No CTCP I{ERRMSG} reply is made to remove a potential denial of service - avenue. - """ - log.msg('Unknown CTCP query from %r: %r %r' % (user, tag, data)) - - - def ctcpQuery_ACTION(self, user, channel, data): - self.action(user, channel, data) - - def ctcpQuery_PING(self, user, channel, data): - nick = string.split(user,"!")[0] - self.ctcpMakeReply(nick, [("PING", data)]) - - def ctcpQuery_FINGER(self, user, channel, data): - if data is not None: - self.quirkyMessage("Why did %s send '%s' with a FINGER query?" - % (user, data)) - if not self.fingerReply: - return - - if callable(self.fingerReply): - reply = self.fingerReply() - else: - reply = str(self.fingerReply) - - nick = string.split(user,"!")[0] - self.ctcpMakeReply(nick, [('FINGER', reply)]) - - def ctcpQuery_VERSION(self, user, channel, data): - if data is not None: - self.quirkyMessage("Why did %s send '%s' with a VERSION query?" - % (user, data)) - - if self.versionName: - nick = string.split(user,"!")[0] - self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' % - (self.versionName, - self.versionNum or '', - self.versionEnv or ''))]) - - def ctcpQuery_SOURCE(self, user, channel, data): - if data is not None: - self.quirkyMessage("Why did %s send '%s' with a SOURCE query?" - % (user, data)) - if self.sourceURL: - nick = string.split(user,"!")[0] - # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE - # replies should be responded to with the location of an anonymous - # FTP server in host:directory:file format. I'm taking the liberty - # of bringing it into the 21st century by sending a URL instead. - self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL), - ('SOURCE', None)]) - - def ctcpQuery_USERINFO(self, user, channel, data): - if data is not None: - self.quirkyMessage("Why did %s send '%s' with a USERINFO query?" - % (user, data)) - if self.userinfo: - nick = string.split(user,"!")[0] - self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)]) - - def ctcpQuery_CLIENTINFO(self, user, channel, data): - """ - A master index of what CTCP tags this client knows. - - If no arguments are provided, respond with a list of known tags. - If an argument is provided, provide human-readable help on - the usage of that tag. - """ - - nick = string.split(user,"!")[0] - if not data: - # XXX: prefixedMethodNames gets methods from my *class*, - # but it's entirely possible that this *instance* has more - # methods. - names = reflect.prefixedMethodNames(self.__class__, - 'ctcpQuery_') - - self.ctcpMakeReply(nick, [('CLIENTINFO', - string.join(names, ' '))]) - else: - args = string.split(data) - method = getattr(self, 'ctcpQuery_%s' % (args[0],), None) - if not method: - self.ctcpMakeReply(nick, [('ERRMSG', - "CLIENTINFO %s :" - "Unknown query '%s'" - % (data, args[0]))]) - return - doc = getattr(method, '__doc__', '') - self.ctcpMakeReply(nick, [('CLIENTINFO', doc)]) - - - def ctcpQuery_ERRMSG(self, user, channel, data): - # Yeah, this seems strange, but that's what the spec says to do - # when faced with an ERRMSG query (not a reply). - nick = string.split(user,"!")[0] - self.ctcpMakeReply(nick, [('ERRMSG', - "%s :No error has occoured." % data)]) - - def ctcpQuery_TIME(self, user, channel, data): - if data is not None: - self.quirkyMessage("Why did %s send '%s' with a TIME query?" - % (user, data)) - nick = string.split(user,"!")[0] - self.ctcpMakeReply(nick, - [('TIME', ':%s' % - time.asctime(time.localtime(time.time())))]) - - def ctcpQuery_DCC(self, user, channel, data): - """Initiate a Direct Client Connection - """ - - if not data: return - dcctype = data.split(None, 1)[0].upper() - handler = getattr(self, "dcc_" + dcctype, None) - if handler: - if self.dcc_sessions is None: - self.dcc_sessions = [] - data = data[len(dcctype)+1:] - handler(user, channel, data) - else: - nick = string.split(user,"!")[0] - self.ctcpMakeReply(nick, [('ERRMSG', - "DCC %s :Unknown DCC type '%s'" - % (data, dcctype))]) - self.quirkyMessage("%s offered unknown DCC type %s" - % (user, dcctype)) - - def dcc_SEND(self, user, channel, data): - # Use splitQuoted for those who send files with spaces in the names. - data = text.splitQuoted(data) - if len(data) < 3: - raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,) - - (filename, address, port) = data[:3] - - address = dccParseAddress(address) - try: - port = int(port) - except ValueError: - raise IRCBadMessage, "Indecipherable port %r" % (port,) - - size = -1 - if len(data) >= 4: - try: - size = int(data[3]) - except ValueError: - pass - - # XXX Should we bother passing this data? - self.dccDoSend(user, address, port, filename, size, data) - - def dcc_ACCEPT(self, user, channel, data): - data = text.splitQuoted(data) - if len(data) < 3: - raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,) - (filename, port, resumePos) = data[:3] - try: - port = int(port) - resumePos = int(resumePos) - except ValueError: - return - - self.dccDoAcceptResume(user, filename, port, resumePos) - - def dcc_RESUME(self, user, channel, data): - data = text.splitQuoted(data) - if len(data) < 3: - raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,) - (filename, port, resumePos) = data[:3] - try: - port = int(port) - resumePos = int(resumePos) - except ValueError: - return - self.dccDoResume(user, filename, port, resumePos) - - def dcc_CHAT(self, user, channel, data): - data = text.splitQuoted(data) - if len(data) < 3: - raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,) - - (filename, address, port) = data[:3] - - address = dccParseAddress(address) - try: - port = int(port) - except ValueError: - raise IRCBadMessage, "Indecipherable port %r" % (port,) - - self.dccDoChat(user, channel, address, port, data) - - ### The dccDo methods are the slightly higher-level siblings of - ### common dcc_ methods; the arguments have been parsed for them. - - def dccDoSend(self, user, address, port, fileName, size, data): - """Called when I receive a DCC SEND offer from a client. - - By default, I do nothing here.""" - ## filename = path.basename(arg) - ## protocol = DccFileReceive(filename, size, - ## (user,channel,data),self.dcc_destdir) - ## reactor.clientTCP(address, port, protocol) - ## self.dcc_sessions.append(protocol) - pass - - def dccDoResume(self, user, file, port, resumePos): - """Called when a client is trying to resume an offered file - via DCC send. It should be either replied to with a DCC - ACCEPT or ignored (default).""" - pass - - def dccDoAcceptResume(self, user, file, port, resumePos): - """Called when a client has verified and accepted a DCC resume - request made by us. By default it will do nothing.""" - pass - - def dccDoChat(self, user, channel, address, port, data): - pass - #factory = DccChatFactory(self, queryData=(user, channel, data)) - #reactor.connectTCP(address, port, factory) - #self.dcc_sessions.append(factory) - - #def ctcpQuery_SED(self, user, data): - # """Simple Encryption Doodoo - # - # Feel free to implement this, but no specification is available. - # """ - # raise NotImplementedError - - - def ctcpMakeReply(self, user, messages): - """ - Send one or more C{extended messages} as a CTCP reply. - - @type messages: a list of extended messages. An extended - message is a (tag, data) tuple, where 'data' may be C{None}. - """ - self.notice(user, ctcpStringify(messages)) - - ### client CTCP query commands - - def ctcpMakeQuery(self, user, messages): - """ - Send one or more C{extended messages} as a CTCP query. - - @type messages: a list of extended messages. An extended - message is a (tag, data) tuple, where 'data' may be C{None}. - """ - self.msg(user, ctcpStringify(messages)) - - ### Receiving a response to a CTCP query (presumably to one we made) - ### You may want to add methods here, or override UnknownReply. - - def ctcpReply(self, user, channel, messages): - """ - Dispatch method for any CTCP replies received. - """ - for m in messages: - method = getattr(self, "ctcpReply_%s" % m[0], None) - if method: - method(user, channel, m[1]) - else: - self.ctcpUnknownReply(user, channel, m[0], m[1]) - - def ctcpReply_PING(self, user, channel, data): - nick = user.split('!', 1)[0] - if (not self._pings) or (not self._pings.has_key((nick, data))): - raise IRCBadMessage,\ - "Bogus PING response from %s: %s" % (user, data) - - t0 = self._pings[(nick, data)] - self.pong(user, time.time() - t0) - - def ctcpUnknownReply(self, user, channel, tag, data): - """Called when a fitting ctcpReply_ method is not found. - - XXX: If the client makes arbitrary CTCP queries, - this method should probably show the responses to - them instead of treating them as anomolies. - """ - log.msg("Unknown CTCP reply from %s: %s %s\n" - % (user, tag, data)) - - ### Error handlers - ### You may override these with something more appropriate to your UI. - - def badMessage(self, line, excType, excValue, tb): - """When I get a message that's so broken I can't use it. - """ - log.msg(line) - log.msg(string.join(traceback.format_exception(excType, - excValue, - tb),'')) - - def quirkyMessage(self, s): - """This is called when I receive a message which is peculiar, - but not wholly indecipherable. - """ - log.msg(s + '\n') - - ### Protocool methods - - def connectionMade(self): - self.supported = ServerSupportedFeatures() - self._queue = [] - if self.performLogin: - self.register(self.nickname) - - def dataReceived(self, data): - basic.LineReceiver.dataReceived(self, data.replace('\r', '')) - - def lineReceived(self, line): - line = lowDequote(line) - try: - prefix, command, params = parsemsg(line) - if command in numeric_to_symbolic: - command = numeric_to_symbolic[command] - self.handleCommand(command, prefix, params) - except IRCBadMessage: - self.badMessage(line, *sys.exc_info()) - - - def getUserModeParams(self): - """ - Get user modes that require parameters for correct parsing. - - @rtype: C{[str, str]} - @return C{[add, remove]} - """ - return ['', ''] - - - def getChannelModeParams(self): - """ - Get channel modes that require parameters for correct parsing. - - @rtype: C{[str, str]} - @return C{[add, remove]} - """ - # PREFIX modes are treated as "type B" CHANMODES, they always take - # parameter. - params = ['', ''] - prefixes = self.supported.getFeature('PREFIX', {}) - params[0] = params[1] = ''.join(prefixes.iterkeys()) - - chanmodes = self.supported.getFeature('CHANMODES') - if chanmodes is not None: - params[0] += chanmodes.get('addressModes', '') - params[0] += chanmodes.get('param', '') - params[1] = params[0] - params[0] += chanmodes.get('setParam', '') - return params - - - def handleCommand(self, command, prefix, params): - """Determine the function to call for the given command and call - it with the given arguments. - """ - method = getattr(self, "irc_%s" % command, None) - try: - if method is not None: - method(prefix, params) - else: - self.irc_unknown(prefix, command, params) - except: - log.deferr() - - - def __getstate__(self): - dct = self.__dict__.copy() - dct['dcc_sessions'] = None - dct['_pings'] = None - return dct - - -def dccParseAddress(address): - if '.' in address: - pass - else: - try: - address = long(address) - except ValueError: - raise IRCBadMessage,\ - "Indecipherable address %r" % (address,) - else: - address = ( - (address >> 24) & 0xFF, - (address >> 16) & 0xFF, - (address >> 8) & 0xFF, - address & 0xFF, - ) - address = '.'.join(map(str,address)) - return address - - -class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral): - """Bare protocol to receive a Direct Client Connection SEND stream. - - This does enough to keep the other guy talking, but you'll want to - extend my dataReceived method to *do* something with the data I get. - """ - - bytesReceived = 0 - - def __init__(self, resumeOffset=0): - self.bytesReceived = resumeOffset - self.resume = (resumeOffset != 0) - - def dataReceived(self, data): - """Called when data is received. - - Warning: This just acknowledges to the remote host that the - data has been received; it doesn't *do* anything with the - data, so you'll want to override this. - """ - self.bytesReceived = self.bytesReceived + len(data) - self.transport.write(struct.pack('!i', self.bytesReceived)) - - -class DccSendProtocol(protocol.Protocol, styles.Ephemeral): - """Protocol for an outgoing Direct Client Connection SEND. - """ - - blocksize = 1024 - file = None - bytesSent = 0 - completed = 0 - connected = 0 - - def __init__(self, file): - if type(file) is types.StringType: - self.file = open(file, 'r') - - def connectionMade(self): - self.connected = 1 - self.sendBlock() - - def dataReceived(self, data): - # XXX: Do we need to check to see if len(data) != fmtsize? - - bytesShesGot = struct.unpack("!I", data) - if bytesShesGot < self.bytesSent: - # Wait for her. - # XXX? Add some checks to see if we've stalled out? - return - elif bytesShesGot > self.bytesSent: - # self.transport.log("DCC SEND %s: She says she has %d bytes " - # "but I've only sent %d. I'm stopping " - # "this screwy transfer." - # % (self.file, - # bytesShesGot, self.bytesSent)) - self.transport.loseConnection() - return - - self.sendBlock() - - def sendBlock(self): - block = self.file.read(self.blocksize) - if block: - self.transport.write(block) - self.bytesSent = self.bytesSent + len(block) - else: - # Nothing more to send, transfer complete. - self.transport.loseConnection() - self.completed = 1 - - def connectionLost(self, reason): - self.connected = 0 - if hasattr(self.file, "close"): - self.file.close() - - -class DccSendFactory(protocol.Factory): - protocol = DccSendProtocol - def __init__(self, file): - self.file = file - - def buildProtocol(self, connection): - p = self.protocol(self.file) - p.factory = self - return p - - -def fileSize(file): - """I'll try my damndest to determine the size of this file object. - """ - size = None - if hasattr(file, "fileno"): - fileno = file.fileno() - try: - stat_ = os.fstat(fileno) - size = stat_[stat.ST_SIZE] - except: - pass - else: - return size - - if hasattr(file, "name") and path.exists(file.name): - try: - size = path.getsize(file.name) - except: - pass - else: - return size - - if hasattr(file, "seek") and hasattr(file, "tell"): - try: - try: - file.seek(0, 2) - size = file.tell() - finally: - file.seek(0, 0) - except: - pass - else: - return size - - return size - -class DccChat(basic.LineReceiver, styles.Ephemeral): - """Direct Client Connection protocol type CHAT. - - DCC CHAT is really just your run o' the mill basic.LineReceiver - protocol. This class only varies from that slightly, accepting - either LF or CR LF for a line delimeter for incoming messages - while always using CR LF for outgoing. - - The lineReceived method implemented here uses the DCC connection's - 'client' attribute (provided upon construction) to deliver incoming - lines from the DCC chat via IRCClient's normal privmsg interface. - That's something of a spoof, which you may well want to override. - """ - - queryData = None - delimiter = CR + NL - client = None - remoteParty = None - buffer = "" - - def __init__(self, client, queryData=None): - """Initialize a new DCC CHAT session. - - queryData is a 3-tuple of - (fromUser, targetUserOrChannel, data) - as received by the CTCP query. - - (To be honest, fromUser is the only thing that's currently - used here. targetUserOrChannel is potentially useful, while - the 'data' argument is soley for informational purposes.) - """ - self.client = client - if queryData: - self.queryData = queryData - self.remoteParty = self.queryData[0] - - def dataReceived(self, data): - self.buffer = self.buffer + data - lines = string.split(self.buffer, LF) - # Put the (possibly empty) element after the last LF back in the - # buffer - self.buffer = lines.pop() - - for line in lines: - if line[-1] == CR: - line = line[:-1] - self.lineReceived(line) - - def lineReceived(self, line): - log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line)) - self.client.privmsg(self.remoteParty, - self.client.nickname, line) - - -class DccChatFactory(protocol.ClientFactory): - protocol = DccChat - noisy = 0 - def __init__(self, client, queryData): - self.client = client - self.queryData = queryData - - - def buildProtocol(self, addr): - p = self.protocol(client=self.client, queryData=self.queryData) - p.factory = self - return p - - - def clientConnectionFailed(self, unused_connector, unused_reason): - self.client.dcc_sessions.remove(self) - - def clientConnectionLost(self, unused_connector, unused_reason): - self.client.dcc_sessions.remove(self) - - -def dccDescribe(data): - """Given the data chunk from a DCC query, return a descriptive string. - """ - - orig_data = data - data = string.split(data) - if len(data) < 4: - return orig_data - - (dcctype, arg, address, port) = data[:4] - - if '.' in address: - pass - else: - try: - address = long(address) - except ValueError: - pass - else: - address = ( - (address >> 24) & 0xFF, - (address >> 16) & 0xFF, - (address >> 8) & 0xFF, - address & 0xFF, - ) - # The mapping to 'int' is to get rid of those accursed - # "L"s which python 1.5.2 puts on the end of longs. - address = string.join(map(str,map(int,address)), ".") - - if dcctype == 'SEND': - filename = arg - - size_txt = '' - if len(data) >= 5: - try: - size = int(data[4]) - size_txt = ' of size %d bytes' % (size,) - except ValueError: - pass - - dcc_text = ("SEND for file '%s'%s at host %s, port %s" - % (filename, size_txt, address, port)) - elif dcctype == 'CHAT': - dcc_text = ("CHAT for host %s, port %s" - % (address, port)) - else: - dcc_text = orig_data - - return dcc_text - - -class DccFileReceive(DccFileReceiveBasic): - """Higher-level coverage for getting a file from DCC SEND. - - I allow you to change the file's name and destination directory. - I won't overwrite an existing file unless I've been told it's okay - to do so. If passed the resumeOffset keyword argument I will attempt to - resume the file from that amount of bytes. - - XXX: I need to let the client know when I am finished. - XXX: I need to decide how to keep a progress indicator updated. - XXX: Client needs a way to tell me "Do not finish until I say so." - XXX: I need to make sure the client understands if the file cannot be written. - """ - - filename = 'dcc' - fileSize = -1 - destDir = '.' - overwrite = 0 - fromUser = None - queryData = None - - def __init__(self, filename, fileSize=-1, queryData=None, - destDir='.', resumeOffset=0): - DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset) - self.filename = filename - self.destDir = destDir - self.fileSize = fileSize - - if queryData: - self.queryData = queryData - self.fromUser = self.queryData[0] - - def set_directory(self, directory): - """Set the directory where the downloaded file will be placed. - - May raise OSError if the supplied directory path is not suitable. - """ - if not path.exists(directory): - raise OSError(errno.ENOENT, "You see no directory there.", - directory) - if not path.isdir(directory): - raise OSError(errno.ENOTDIR, "You cannot put a file into " - "something which is not a directory.", - directory) - if not os.access(directory, os.X_OK | os.W_OK): - raise OSError(errno.EACCES, - "This directory is too hard to write in to.", - directory) - self.destDir = directory - - def set_filename(self, filename): - """Change the name of the file being transferred. - - This replaces the file name provided by the sender. - """ - self.filename = filename - - def set_overwrite(self, boolean): - """May I overwrite existing files? - """ - self.overwrite = boolean - - - # Protocol-level methods. - - def connectionMade(self): - dst = path.abspath(path.join(self.destDir,self.filename)) - exists = path.exists(dst) - if self.resume and exists: - # I have been told I want to resume, and a file already - # exists - Here we go - self.file = open(dst, 'ab') - log.msg("Attempting to resume %s - starting from %d bytes" % - (self.file, self.file.tell())) - elif self.overwrite or not exists: - self.file = open(dst, 'wb') - else: - raise OSError(errno.EEXIST, - "There's a file in the way. " - "Perhaps that's why you cannot open it.", - dst) - - def dataReceived(self, data): - self.file.write(data) - DccFileReceiveBasic.dataReceived(self, data) - - # XXX: update a progress indicator here? - - def connectionLost(self, reason): - """When the connection is lost, I close the file. - """ - self.connected = 0 - logmsg = ("%s closed." % (self,)) - if self.fileSize > 0: - logmsg = ("%s %d/%d bytes received" - % (logmsg, self.bytesReceived, self.fileSize)) - if self.bytesReceived == self.fileSize: - pass # Hooray! - elif self.bytesReceived < self.fileSize: - logmsg = ("%s (Warning: %d bytes short)" - % (logmsg, self.fileSize - self.bytesReceived)) - else: - logmsg = ("%s (file larger than expected)" - % (logmsg,)) - else: - logmsg = ("%s %d bytes received" - % (logmsg, self.bytesReceived)) - - if hasattr(self, 'file'): - logmsg = "%s and written to %s.\n" % (logmsg, self.file.name) - if hasattr(self.file, 'close'): self.file.close() - - # self.transport.log(logmsg) - - def __str__(self): - if not self.connected: - return "<Unconnected DccFileReceive object at %x>" % (id(self),) - from_ = self.transport.getPeer() - if self.fromUser: - from_ = "%s (%s)" % (self.fromUser, from_) - - s = ("DCC transfer of '%s' from %s" % (self.filename, from_)) - return s - - def __repr__(self): - s = ("<%s at %x: GET %s>" - % (self.__class__, id(self), self.filename)) - return s - - -# CTCP constants and helper functions - -X_DELIM = chr(001) - -def ctcpExtract(message): - """ - Extract CTCP data from a string. - - @return: A C{dict} containing two keys: - - C{'extended'}: A list of CTCP (tag, data) tuples. - - C{'normal'}: A list of strings which were not inside a CTCP delimiter. - """ - extended_messages = [] - normal_messages = [] - retval = {'extended': extended_messages, - 'normal': normal_messages } - - messages = string.split(message, X_DELIM) - odd = 0 - - # X1 extended data X2 nomal data X3 extended data X4 normal... - while messages: - if odd: - extended_messages.append(messages.pop(0)) - else: - normal_messages.append(messages.pop(0)) - odd = not odd - - extended_messages[:] = filter(None, extended_messages) - normal_messages[:] = filter(None, normal_messages) - - extended_messages[:] = map(ctcpDequote, extended_messages) - for i in xrange(len(extended_messages)): - m = string.split(extended_messages[i], SPC, 1) - tag = m[0] - if len(m) > 1: - data = m[1] - else: - data = None - - extended_messages[i] = (tag, data) - - return retval - -# CTCP escaping - -M_QUOTE= chr(020) - -mQuoteTable = { - NUL: M_QUOTE + '0', - NL: M_QUOTE + 'n', - CR: M_QUOTE + 'r', - M_QUOTE: M_QUOTE + M_QUOTE - } - -mDequoteTable = {} -for k, v in mQuoteTable.items(): - mDequoteTable[v[-1]] = k -del k, v - -mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL) - -def lowQuote(s): - for c in (M_QUOTE, NUL, NL, CR): - s = string.replace(s, c, mQuoteTable[c]) - return s - -def lowDequote(s): - def sub(matchobj, mDequoteTable=mDequoteTable): - s = matchobj.group()[1] - try: - s = mDequoteTable[s] - except KeyError: - s = s - return s - - return mEscape_re.sub(sub, s) - -X_QUOTE = '\\' - -xQuoteTable = { - X_DELIM: X_QUOTE + 'a', - X_QUOTE: X_QUOTE + X_QUOTE - } - -xDequoteTable = {} - -for k, v in xQuoteTable.items(): - xDequoteTable[v[-1]] = k - -xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL) - -def ctcpQuote(s): - for c in (X_QUOTE, X_DELIM): - s = string.replace(s, c, xQuoteTable[c]) - return s - -def ctcpDequote(s): - def sub(matchobj, xDequoteTable=xDequoteTable): - s = matchobj.group()[1] - try: - s = xDequoteTable[s] - except KeyError: - s = s - return s - - return xEscape_re.sub(sub, s) - -def ctcpStringify(messages): - """ - @type messages: a list of extended messages. An extended - message is a (tag, data) tuple, where 'data' may be C{None}, a - string, or a list of strings to be joined with whitespace. - - @returns: String - """ - coded_messages = [] - for (tag, data) in messages: - if data: - if not isinstance(data, types.StringType): - try: - # data as list-of-strings - data = " ".join(map(str, data)) - except TypeError: - # No? Then use it's %s representation. - pass - m = "%s %s" % (tag, data) - else: - m = str(tag) - m = ctcpQuote(m) - m = "%s%s%s" % (X_DELIM, m, X_DELIM) - coded_messages.append(m) - - line = string.join(coded_messages, '') - return line - - -# Constants (from RFC 2812) -RPL_WELCOME = '001' -RPL_YOURHOST = '002' -RPL_CREATED = '003' -RPL_MYINFO = '004' -RPL_ISUPPORT = '005' -RPL_BOUNCE = '010' -RPL_USERHOST = '302' -RPL_ISON = '303' -RPL_AWAY = '301' -RPL_UNAWAY = '305' -RPL_NOWAWAY = '306' -RPL_WHOISUSER = '311' -RPL_WHOISSERVER = '312' -RPL_WHOISOPERATOR = '313' -RPL_WHOISIDLE = '317' -RPL_ENDOFWHOIS = '318' -RPL_WHOISCHANNELS = '319' -RPL_WHOWASUSER = '314' -RPL_ENDOFWHOWAS = '369' -RPL_LISTSTART = '321' -RPL_LIST = '322' -RPL_LISTEND = '323' -RPL_UNIQOPIS = '325' -RPL_CHANNELMODEIS = '324' -RPL_NOTOPIC = '331' -RPL_TOPIC = '332' -RPL_INVITING = '341' -RPL_SUMMONING = '342' -RPL_INVITELIST = '346' -RPL_ENDOFINVITELIST = '347' -RPL_EXCEPTLIST = '348' -RPL_ENDOFEXCEPTLIST = '349' -RPL_VERSION = '351' -RPL_WHOREPLY = '352' -RPL_ENDOFWHO = '315' -RPL_NAMREPLY = '353' -RPL_ENDOFNAMES = '366' -RPL_LINKS = '364' -RPL_ENDOFLINKS = '365' -RPL_BANLIST = '367' -RPL_ENDOFBANLIST = '368' -RPL_INFO = '371' -RPL_ENDOFINFO = '374' -RPL_MOTDSTART = '375' -RPL_MOTD = '372' -RPL_ENDOFMOTD = '376' -RPL_YOUREOPER = '381' -RPL_REHASHING = '382' -RPL_YOURESERVICE = '383' -RPL_TIME = '391' -RPL_USERSSTART = '392' -RPL_USERS = '393' -RPL_ENDOFUSERS = '394' -RPL_NOUSERS = '395' -RPL_TRACELINK = '200' -RPL_TRACECONNECTING = '201' -RPL_TRACEHANDSHAKE = '202' -RPL_TRACEUNKNOWN = '203' -RPL_TRACEOPERATOR = '204' -RPL_TRACEUSER = '205' -RPL_TRACESERVER = '206' -RPL_TRACESERVICE = '207' -RPL_TRACENEWTYPE = '208' -RPL_TRACECLASS = '209' -RPL_TRACERECONNECT = '210' -RPL_TRACELOG = '261' -RPL_TRACEEND = '262' -RPL_STATSLINKINFO = '211' -RPL_STATSCOMMANDS = '212' -RPL_ENDOFSTATS = '219' -RPL_STATSUPTIME = '242' -RPL_STATSOLINE = '243' -RPL_UMODEIS = '221' -RPL_SERVLIST = '234' -RPL_SERVLISTEND = '235' -RPL_LUSERCLIENT = '251' -RPL_LUSEROP = '252' -RPL_LUSERUNKNOWN = '253' -RPL_LUSERCHANNELS = '254' -RPL_LUSERME = '255' -RPL_ADMINME = '256' -RPL_ADMINLOC = '257' -RPL_ADMINLOC = '258' -RPL_ADMINEMAIL = '259' -RPL_TRYAGAIN = '263' -ERR_NOSUCHNICK = '401' -ERR_NOSUCHSERVER = '402' -ERR_NOSUCHCHANNEL = '403' -ERR_CANNOTSENDTOCHAN = '404' -ERR_TOOMANYCHANNELS = '405' -ERR_WASNOSUCHNICK = '406' -ERR_TOOMANYTARGETS = '407' -ERR_NOSUCHSERVICE = '408' -ERR_NOORIGIN = '409' -ERR_NORECIPIENT = '411' -ERR_NOTEXTTOSEND = '412' -ERR_NOTOPLEVEL = '413' -ERR_WILDTOPLEVEL = '414' -ERR_BADMASK = '415' -ERR_UNKNOWNCOMMAND = '421' -ERR_NOMOTD = '422' -ERR_NOADMININFO = '423' -ERR_FILEERROR = '424' -ERR_NONICKNAMEGIVEN = '431' -ERR_ERRONEUSNICKNAME = '432' -ERR_NICKNAMEINUSE = '433' -ERR_NICKCOLLISION = '436' -ERR_UNAVAILRESOURCE = '437' -ERR_USERNOTINCHANNEL = '441' -ERR_NOTONCHANNEL = '442' -ERR_USERONCHANNEL = '443' -ERR_NOLOGIN = '444' -ERR_SUMMONDISABLED = '445' -ERR_USERSDISABLED = '446' -ERR_NOTREGISTERED = '451' -ERR_NEEDMOREPARAMS = '461' -ERR_ALREADYREGISTRED = '462' -ERR_NOPERMFORHOST = '463' -ERR_PASSWDMISMATCH = '464' -ERR_YOUREBANNEDCREEP = '465' -ERR_YOUWILLBEBANNED = '466' -ERR_KEYSET = '467' -ERR_CHANNELISFULL = '471' -ERR_UNKNOWNMODE = '472' -ERR_INVITEONLYCHAN = '473' -ERR_BANNEDFROMCHAN = '474' -ERR_BADCHANNELKEY = '475' -ERR_BADCHANMASK = '476' -ERR_NOCHANMODES = '477' -ERR_BANLISTFULL = '478' -ERR_NOPRIVILEGES = '481' -ERR_CHANOPRIVSNEEDED = '482' -ERR_CANTKILLSERVER = '483' -ERR_RESTRICTED = '484' -ERR_UNIQOPPRIVSNEEDED = '485' -ERR_NOOPERHOST = '491' -ERR_NOSERVICEHOST = '492' -ERR_UMODEUNKNOWNFLAG = '501' -ERR_USERSDONTMATCH = '502' - -# And hey, as long as the strings are already intern'd... -symbolic_to_numeric = { - "RPL_WELCOME": '001', - "RPL_YOURHOST": '002', - "RPL_CREATED": '003', - "RPL_MYINFO": '004', - "RPL_ISUPPORT": '005', - "RPL_BOUNCE": '010', - "RPL_USERHOST": '302', - "RPL_ISON": '303', - "RPL_AWAY": '301', - "RPL_UNAWAY": '305', - "RPL_NOWAWAY": '306', - "RPL_WHOISUSER": '311', - "RPL_WHOISSERVER": '312', - "RPL_WHOISOPERATOR": '313', - "RPL_WHOISIDLE": '317', - "RPL_ENDOFWHOIS": '318', - "RPL_WHOISCHANNELS": '319', - "RPL_WHOWASUSER": '314', - "RPL_ENDOFWHOWAS": '369', - "RPL_LISTSTART": '321', - "RPL_LIST": '322', - "RPL_LISTEND": '323', - "RPL_UNIQOPIS": '325', - "RPL_CHANNELMODEIS": '324', - "RPL_NOTOPIC": '331', - "RPL_TOPIC": '332', - "RPL_INVITING": '341', - "RPL_SUMMONING": '342', - "RPL_INVITELIST": '346', - "RPL_ENDOFINVITELIST": '347', - "RPL_EXCEPTLIST": '348', - "RPL_ENDOFEXCEPTLIST": '349', - "RPL_VERSION": '351', - "RPL_WHOREPLY": '352', - "RPL_ENDOFWHO": '315', - "RPL_NAMREPLY": '353', - "RPL_ENDOFNAMES": '366', - "RPL_LINKS": '364', - "RPL_ENDOFLINKS": '365', - "RPL_BANLIST": '367', - "RPL_ENDOFBANLIST": '368', - "RPL_INFO": '371', - "RPL_ENDOFINFO": '374', - "RPL_MOTDSTART": '375', - "RPL_MOTD": '372', - "RPL_ENDOFMOTD": '376', - "RPL_YOUREOPER": '381', - "RPL_REHASHING": '382', - "RPL_YOURESERVICE": '383', - "RPL_TIME": '391', - "RPL_USERSSTART": '392', - "RPL_USERS": '393', - "RPL_ENDOFUSERS": '394', - "RPL_NOUSERS": '395', - "RPL_TRACELINK": '200', - "RPL_TRACECONNECTING": '201', - "RPL_TRACEHANDSHAKE": '202', - "RPL_TRACEUNKNOWN": '203', - "RPL_TRACEOPERATOR": '204', - "RPL_TRACEUSER": '205', - "RPL_TRACESERVER": '206', - "RPL_TRACESERVICE": '207', - "RPL_TRACENEWTYPE": '208', - "RPL_TRACECLASS": '209', - "RPL_TRACERECONNECT": '210', - "RPL_TRACELOG": '261', - "RPL_TRACEEND": '262', - "RPL_STATSLINKINFO": '211', - "RPL_STATSCOMMANDS": '212', - "RPL_ENDOFSTATS": '219', - "RPL_STATSUPTIME": '242', - "RPL_STATSOLINE": '243', - "RPL_UMODEIS": '221', - "RPL_SERVLIST": '234', - "RPL_SERVLISTEND": '235', - "RPL_LUSERCLIENT": '251', - "RPL_LUSEROP": '252', - "RPL_LUSERUNKNOWN": '253', - "RPL_LUSERCHANNELS": '254', - "RPL_LUSERME": '255', - "RPL_ADMINME": '256', - "RPL_ADMINLOC": '257', - "RPL_ADMINLOC": '258', - "RPL_ADMINEMAIL": '259', - "RPL_TRYAGAIN": '263', - "ERR_NOSUCHNICK": '401', - "ERR_NOSUCHSERVER": '402', - "ERR_NOSUCHCHANNEL": '403', - "ERR_CANNOTSENDTOCHAN": '404', - "ERR_TOOMANYCHANNELS": '405', - "ERR_WASNOSUCHNICK": '406', - "ERR_TOOMANYTARGETS": '407', - "ERR_NOSUCHSERVICE": '408', - "ERR_NOORIGIN": '409', - "ERR_NORECIPIENT": '411', - "ERR_NOTEXTTOSEND": '412', - "ERR_NOTOPLEVEL": '413', - "ERR_WILDTOPLEVEL": '414', - "ERR_BADMASK": '415', - "ERR_UNKNOWNCOMMAND": '421', - "ERR_NOMOTD": '422', - "ERR_NOADMININFO": '423', - "ERR_FILEERROR": '424', - "ERR_NONICKNAMEGIVEN": '431', - "ERR_ERRONEUSNICKNAME": '432', - "ERR_NICKNAMEINUSE": '433', - "ERR_NICKCOLLISION": '436', - "ERR_UNAVAILRESOURCE": '437', - "ERR_USERNOTINCHANNEL": '441', - "ERR_NOTONCHANNEL": '442', - "ERR_USERONCHANNEL": '443', - "ERR_NOLOGIN": '444', - "ERR_SUMMONDISABLED": '445', - "ERR_USERSDISABLED": '446', - "ERR_NOTREGISTERED": '451', - "ERR_NEEDMOREPARAMS": '461', - "ERR_ALREADYREGISTRED": '462', - "ERR_NOPERMFORHOST": '463', - "ERR_PASSWDMISMATCH": '464', - "ERR_YOUREBANNEDCREEP": '465', - "ERR_YOUWILLBEBANNED": '466', - "ERR_KEYSET": '467', - "ERR_CHANNELISFULL": '471', - "ERR_UNKNOWNMODE": '472', - "ERR_INVITEONLYCHAN": '473', - "ERR_BANNEDFROMCHAN": '474', - "ERR_BADCHANNELKEY": '475', - "ERR_BADCHANMASK": '476', - "ERR_NOCHANMODES": '477', - "ERR_BANLISTFULL": '478', - "ERR_NOPRIVILEGES": '481', - "ERR_CHANOPRIVSNEEDED": '482', - "ERR_CANTKILLSERVER": '483', - "ERR_RESTRICTED": '484', - "ERR_UNIQOPPRIVSNEEDED": '485', - "ERR_NOOPERHOST": '491', - "ERR_NOSERVICEHOST": '492', - "ERR_UMODEUNKNOWNFLAG": '501', - "ERR_USERSDONTMATCH": '502', -} - -numeric_to_symbolic = {} -for k, v in symbolic_to_numeric.items(): - numeric_to_symbolic[v] = k |