| 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
|
|
|