Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(55)

Unified Diff: third_party/twisted_8_1/twisted/words/protocols/irc.py

Issue 12261012: Remove third_party/twisted_8_1 (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: third_party/twisted_8_1/twisted/words/protocols/irc.py
diff --git a/third_party/twisted_8_1/twisted/words/protocols/irc.py b/third_party/twisted_8_1/twisted/words/protocols/irc.py
deleted file mode 100644
index fb38339b30e26ac67c292994afe6900cc35517c5..0000000000000000000000000000000000000000
--- a/third_party/twisted_8_1/twisted/words/protocols/irc.py
+++ /dev/null
@@ -1,2329 +0,0 @@
-# -*- test-case-name: twisted.words.test.test_irc -*-
-# Copyright (c) 2001-2005 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.
-
-@author: U{Kevin Turner<mailto:acapnotic@twistedmatrix.com>}
-
-@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>}
-"""
-
-__version__ = '$Revision: 1.94 $'[11:-2]
-
-from twisted.internet import reactor, protocol
-from twisted.persisted import styles
-from twisted.protocols import basic
-from twisted.python import log, reflect, text
-
-# System Imports
-
-import errno
-import os
-import random
-import re
-import stat
-import string
-import struct
-import sys
-import time
-import types
-import traceback
-import socket
-
-from os import path
-
-NUL = chr(0)
-CR = chr(015)
-NL = chr(012)
-LF = NL
-SPC = chr(040)
-
-CHANNEL_PREFIXES = '&#!+'
-
-class IRCBadMessage(Exception):
- pass
-
-class IRCPasswordMismatch(Exception):
- pass
-
-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):
- """I break a message into multiple lines.
-
- I prefer to break at whitespace near str[length]. I also break at \\n.
-
- @returns: list of strings
- """
- if length <= 0:
- raise ValueError("Length must be a number greater than zero")
- r = []
- while len(str) > length:
- w, n = str[:length].rfind(' '), str[:length].find('\n')
- if w == -1 and n == -1:
- line, str = str[:length], str[length:]
- else:
- i = n == -1 and w or n
- line, str = str[:i], str[i+1:]
- r.append(line)
- if len(str):
- r.extend(str.split('\n'))
- return r
-
-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.has_key('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):
- """Implement me!"""
- 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" (wtf do these
- mean?), 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 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?)
- - Heartbeat. The transport may die in such a way that it does not realize
- it is dead until it is written to. Sending something (like \"PING
- this.irc-host.net\") during idle peroids would alleviate that. If
- you're concerned with the stability of the host as well as that of the
- transport, you might care to watch for the corresponding PONG.
-
- @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 X{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 X{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.
- @ivar versionNum: CTCP VERSION reply, client version,
- @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
-
- @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.
- """
-
- motd = ""
- 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'
-
-
- 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
-
-
- ### 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 (# or &) intact.
- """
- pass
-
- def left(self, channel):
- """Called when I have left a channel.
-
- channel has the starting character (# or &) intact.
- """
- pass
-
- def noticed(self, user, channel, message):
- """Called when I have a notice from a user to me or a channel.
-
- By default, this is equivalent to IRCClient.privmsg, but if your
- client makes any automated replies, you must override this!
- From 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.
- """
- self.privmsg(user, channel, message)
-
- def modeChanged(self, user, channel, set, modes, args):
- """Called when a 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 for which the modes are changing.
-
- @type set: C{bool} or C{int}
- @param set: true if the mode is being added, false if it is being
- removed.
-
- @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::
-
- string.join(motd, '\\n')
-
- 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):
- if channel[0] not in '&#!+': channel = '#' + channel
- if key:
- self.sendLine("JOIN %s %s" % (channel, key))
- else:
- self.sendLine("JOIN %s" % (channel,))
-
- def leave(self, channel, reason=None):
- if channel[0] not in '&#!+': channel = '#' + channel
- if reason:
- self.sendLine("PART %s :%s" % (channel, reason))
- else:
- self.sendLine("PART %s" % (channel,))
-
- def kick(self, channel, user, reason=None):
- if channel[0] not in '&#!+': channel = '#' + channel
- if reason:
- self.sendLine("KICK %s %s :%s" % (channel, user, reason))
- else:
- self.sendLine("KICK %s %s" % (channel, user))
-
- part = leave
-
- 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.
- """
- # << TOPIC #xtestx :fff
- if channel[0] not in '&#!+': 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."""
- 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):
- if channel[0] not in '&#!+': channel = '#' + channel
- self.msg(channel, message, length)
-
- def msg(self, user, message, length = None):
- """Send a message to a user or channel.
-
- @type user: C{str}
- @param user: The username or channel name to which to direct the
- message.
-
- @type message: C{str}
- @param message: The text to send
-
- @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 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.
- """
-
- fmt = "PRIVMSG %s :%%s" % (user,)
-
- if length is None:
- self.sendLine(fmt % (message,))
- else:
- # NOTE: minimumLength really equals len(fmt) - 2 (for '%s') + n
- # where n is how many bytes sendLine sends to end the line.
- # n was magic numbered to 2, I think incorrectly
- minimumLength = len(fmt)
- if length <= minimumLength:
- raise ValueError("Maximum length must exceed %d for message "
- "to %s" % (minimumLength, user))
- lines = split(message, length - minimumLength)
- map(lambda line, self=self, fmt=fmt: self.sendLine(fmt % line),
- lines)
-
- def notice(self, user, message):
- self.sendLine("NOTICE %s :%s" % (user, message))
-
- def away(self, message=''):
- self.sendLine("AWAY :%s" % message)
-
- def register(self, nickname, hostname='foo', servername='bar'):
- 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):
- self.nickname = nickname
- self.sendLine("NICK %s" % nickname)
-
- def quit(self, message = ''):
- self.sendLine("QUIT :%s" % message)
-
- ### user input commands, client->client
-
- def me(self, channel, action):
- """Strike a pose.
- """
- if channel[0] not in '&#!+': channel = '#' + channel
- 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):
- self.register(self.nickname+'_')
-
- def irc_ERR_PASSWDMISMATCH(self, prefix, params):
- raise IRCPasswordMismatch("Password Incorrect.")
-
- def irc_RPL_WELCOME(self, prefix, params):
- self.signedOn()
-
- def irc_JOIN(self, prefix, params):
- 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):
- 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):
- nick = string.split(prefix,'!')[0]
- self.userQuit(nick, params[0])
-
- def irc_MODE(self, prefix, params):
- channel, rest = params[0], params[1:]
- set = rest[0][0] == '+'
- modes = rest[0][1:]
- args = rest[1:]
- self.modeChanged(prefix, channel, set, modes, tuple(args))
-
- def irc_PING(self, prefix, params):
- self.sendLine("PONG %s" % params[-1])
-
- def irc_PRIVMSG(self, prefix, params):
- user = prefix
- channel = params[0]
- message = params[-1]
-
- if not message: return # don't raise an exception if some idiot sends us a blank message
-
- 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):
- 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):
- 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):
- """Kicked? Who? Not me, I hope.
- """
- 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):
- """I just joined the channel, and the server is telling me the current topic.
- """
- 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:]
- self.motd.append(params[-1])
-
- def irc_RPL_ENDOFMOTD(self, prefix, params):
- self.receivedMOTD(self.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):
- # 005 is doubly assigned. Piece of crap dirty trash protocol.
- if params[-1] == "are available on this server":
- self.isupport(params[1:-1])
- else:
- self.bounce(params[1])
-
- 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.
- """
- for m in messages:
- method = getattr(self, "ctcpQuery_%s" % m[0], None)
- if method:
- method(user, channel, m[1])
- else:
- self.ctcpUnknownQuery(user, channel, m[0], m[1])
-
- 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,
- self.versionEnv))])
-
- 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 ctcpUnknownQuery(self, user, channel, tag, data):
- nick = string.split(user,"!")[0]
- self.ctcpMakeReply(nick, [('ERRMSG',
- "%s %s: Unknown query '%s'"
- % (tag, data, tag))])
-
- log.msg("Unknown CTCP query from %s: %s %s\n"
- % (user, tag, data))
-
- def ctcpMakeReply(self, user, messages):
- """Send one or more X{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 X{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._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 numeric_to_symbolic.has_key(command):
- command = numeric_to_symbolic[command]
- self.handleCommand(command, prefix, params)
- except IRCBadMessage:
- self.badMessage(line, *sys.exc_info())
-
-
- 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
-
- 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.
-
- Returns a dictionary with two items:
-
- - C{'extended'}: a list of CTCP (tag, data) tuples
- - C{'normal'}: a list of strings which were not inside a CTCP delimeter
- """
-
- 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_BOUNCE = '005'
-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_BOUNCE": '005',
- "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

Powered by Google App Engine
This is Rietveld 408576698