1 # -*- test-case-name: twisted.words.test.test_irc -*-
2 # Copyright (c) 2001-2005 Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 """Internet Relay Chat Protocol for client and server.
11 The way the IRCClient class works here encourages people to implement
12 IRC clients by subclassing the ephemeral protocol class, and it tends
13 to end up with way more state than it should for an object which will
14 be destroyed as soon as the TCP transport drops. Someone oughta do
15 something about that, ya know?
17 The DCC support needs to have more hooks for the client for it to be
18 able to ask the user things like \"Do you want to accept this session?\"
19 and \"Transfer #2 is 67% done.\" and otherwise manage the DCC sessions.
21 Test coverage needs to be better.
23 @author: U{Kevin Turner<mailto:acapnotic@twistedmatrix.com>}
25 @see: RFC 1459: Internet Relay Chat Protocol
26 @see: RFC 2812: Internet Relay Chat: Client Protocol
27 @see: U{The Client-To-Client-Protocol
28 <http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
31 __version__ = '$Revision$'[11:-2]
33 from twisted.internet import reactor, protocol
34 from twisted.persisted import styles
35 from twisted.protocols import basic
36 from twisted.python import log, reflect, text
61 CHANNEL_PREFIXES = '&#!+'
63 class IRCBadMessage(Exception):
66 class IRCPasswordMismatch(Exception):
70 """Breaks a message from an IRC server into its prefix, command, and arguments.
75 raise IRCBadMessage("Empty line.")
77 prefix, s = s[1:].split(' ', 1)
78 if s.find(' :') != -1:
79 s, trailing = s.split(' :', 1)
85 return prefix, command, args
88 def split(str, length = 80):
89 """I break a message into multiple lines.
91 I prefer to break at whitespace near str[length]. I also break at \\n.
93 @returns: list of strings
96 raise ValueError("Length must be a number greater than zero")
98 while len(str) > length:
99 w, n = str[:length].rfind(' '), str[:length].find('\n')
100 if w == -1 and n == -1:
101 line, str = str[:length], str[length:]
103 i = n == -1 and w or n
104 line, str = str[:i], str[i+1:]
107 r.extend(str.split('\n'))
110 class IRC(protocol.Protocol):
111 """Internet Relay Chat server protocol.
119 def connectionMade(self):
121 if self.hostname is None:
122 self.hostname = socket.getfqdn()
125 def sendLine(self, line):
126 if self.encoding is not None:
127 if isinstance(line, unicode):
128 line = line.encode(self.encoding)
129 self.transport.write("%s%s%s" % (line, CR, LF))
132 def sendMessage(self, command, *parameter_list, **prefix):
133 """Send a line formatted as an IRC message.
135 First argument is the command, all subsequent arguments
136 are parameters to that command. If a prefix is desired,
137 it may be specified with the keyword argument 'prefix'.
141 raise ValueError, "IRC message requires a command."
143 if ' ' in command or command[0] == ':':
144 # Not the ONLY way to screw up, but provides a little
145 # sanity checking to catch likely dumb mistakes.
146 raise ValueError, "Somebody screwed up, 'cuz this doesn't" \
147 " look like a command to me: %s" % command
149 line = string.join([command] + list(parameter_list))
150 if prefix.has_key('prefix'):
151 line = ":%s %s" % (prefix['prefix'], line)
154 if len(parameter_list) > 15:
155 log.msg("Message has %d parameters (RFC allows 15):\n%s" %
156 (len(parameter_list), line))
159 def dataReceived(self, data):
160 """This hack is to support mIRC, which sends LF only,
161 even though the RFC says CRLF. (Also, the flexibility
162 of LineReceiver to turn "line mode" on and off was not
165 lines = (self.buffer + data).split(LF)
166 # Put the (possibly empty) element after the last LF back in the
168 self.buffer = lines.pop()
172 # This is a blank line, at best.
176 prefix, command, params = parsemsg(line)
177 # mIRC is a big pile of doo-doo
178 command = command.upper()
179 # DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
181 self.handleCommand(command, prefix, params)
184 def handleCommand(self, command, prefix, params):
185 """Determine the function to call for the given command and call
186 it with the given arguments.
188 method = getattr(self, "irc_%s" % command, None)
190 if method is not None:
191 method(prefix, params)
193 self.irc_unknown(prefix, command, params)
198 def irc_unknown(self, prefix, command, params):
200 raise NotImplementedError(command, prefix, params)
204 def privmsg(self, sender, recip, message):
205 """Send a message to a channel or user
207 @type sender: C{str} or C{unicode}
208 @param sender: Who is sending this message. Should be of the form
209 username!ident@hostmask (unless you know better!).
211 @type recip: C{str} or C{unicode}
212 @param recip: The recipient of this message. If a channel, it
213 must start with a channel prefix.
215 @type message: C{str} or C{unicode}
216 @param message: The message being sent.
218 self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message)))
221 def notice(self, sender, recip, message):
222 """Send a \"notice\" to a channel or user.
224 Notices differ from privmsgs in that the RFC claims they are different.
225 Robots are supposed to send notices and not respond to them. Clients
226 typically display notices differently from privmsgs.
228 @type sender: C{str} or C{unicode}
229 @param sender: Who is sending this message. Should be of the form
230 username!ident@hostmask (unless you know better!).
232 @type recip: C{str} or C{unicode}
233 @param recip: The recipient of this message. If a channel, it
234 must start with a channel prefix.
236 @type message: C{str} or C{unicode}
237 @param message: The message being sent.
239 self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message))
242 def action(self, sender, recip, message):
243 """Send an action to a channel or user.
245 @type sender: C{str} or C{unicode}
246 @param sender: Who is sending this message. Should be of the form
247 username!ident@hostmask (unless you know better!).
249 @type recip: C{str} or C{unicode}
250 @param recip: The recipient of this message. If a channel, it
251 must start with a channel prefix.
253 @type message: C{str} or C{unicode}
254 @param message: The action being sent.
256 self.sendLine(":%s ACTION %s :%s" % (sender, recip, message))
259 def topic(self, user, channel, topic, author=None):
260 """Send the topic to a user.
262 @type user: C{str} or C{unicode}
263 @param user: The user receiving the topic. Only their nick name, not
266 @type channel: C{str} or C{unicode}
267 @param channel: The channel for which this is the topic.
269 @type topic: C{str} or C{unicode} or C{None}
270 @param topic: The topic string, unquoted, or None if there is
273 @type author: C{str} or C{unicode}
274 @param author: If the topic is being changed, the full username and hostmask
275 of the person changing it.
279 self.sendLine(':%s %s %s %s :%s' % (
280 self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.'))
282 self.sendLine(":%s %s %s %s :%s" % (
283 self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)))
285 self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)))
288 def topicAuthor(self, user, channel, author, date):
290 Send the author of and time at which a topic was set for the given
293 This sends a 333 reply message, which is not part of the IRC RFC.
295 @type user: C{str} or C{unicode}
296 @param user: The user receiving the topic. Only their nick name, not
299 @type channel: C{str} or C{unicode}
300 @param channel: The channel for which this information is relevant.
302 @type author: C{str} or C{unicode}
303 @param author: The nickname (without hostmask) of the user who last
307 @param date: A POSIX timestamp (number of seconds since the epoch)
308 at which the topic was last set.
310 self.sendLine(':%s %d %s %s %s %d' % (
311 self.hostname, 333, user, channel, author, date))
314 def names(self, user, channel, names):
315 """Send the names of a channel's participants to a user.
317 @type user: C{str} or C{unicode}
318 @param user: The user receiving the name list. Only their nick
319 name, not the full hostmask.
321 @type channel: C{str} or C{unicode}
322 @param channel: The channel for which this is the namelist.
324 @type names: C{list} of C{str} or C{unicode}
325 @param names: The names to send.
327 # XXX If unicode is given, these limits are not quite correct
328 prefixLength = len(channel) + len(user) + 10
329 namesLength = 512 - prefixLength
334 if count + len(n) + 1 > namesLength:
335 self.sendLine(":%s %s %s = %s :%s" % (
336 self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
343 self.sendLine(":%s %s %s = %s :%s" % (
344 self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
345 self.sendLine(":%s %s %s %s :End of /NAMES list" % (
346 self.hostname, RPL_ENDOFNAMES, user, channel))
349 def who(self, user, channel, memberInfo):
351 Send a list of users participating in a channel.
353 @type user: C{str} or C{unicode}
354 @param user: The user receiving this member information. Only their
355 nick name, not the full hostmask.
357 @type channel: C{str} or C{unicode}
358 @param channel: The channel for which this is the member
361 @type memberInfo: C{list} of C{tuples}
362 @param memberInfo: For each member of the given channel, a 7-tuple
363 containing their username, their hostmask, the server to which they
364 are connected, their nickname, the letter "H" or "G" (wtf do these
365 mean?), the hopcount from C{user} to this member, and this member's
368 for info in memberInfo:
369 (username, hostmask, server, nickname, flag, hops, realName) = info
370 assert flag in ("H", "G")
371 self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % (
372 self.hostname, RPL_WHOREPLY, user, channel,
373 username, hostmask, server, nickname, flag, hops, realName))
375 self.sendLine(":%s %s %s %s :End of /WHO list." % (
376 self.hostname, RPL_ENDOFWHO, user, channel))
379 def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels):
381 Send information about the state of a particular user.
383 @type user: C{str} or C{unicode}
384 @param user: The user receiving this information. Only their nick
385 name, not the full hostmask.
387 @type nick: C{str} or C{unicode}
388 @param nick: The nickname of the user this information describes.
390 @type username: C{str} or C{unicode}
391 @param username: The user's username (eg, ident response)
393 @type hostname: C{str}
394 @param hostname: The user's hostmask
396 @type realName: C{str} or C{unicode}
397 @param realName: The user's real name
399 @type server: C{str} or C{unicode}
400 @param server: The name of the server to which the user is connected
402 @type serverInfo: C{str} or C{unicode}
403 @param serverInfo: A descriptive string about that server
406 @param oper: Indicates whether the user is an IRC operator
409 @param idle: The number of seconds since the user last sent a message
412 @param signOn: A POSIX timestamp (number of seconds since the epoch)
413 indicating the time the user signed on
415 @type channels: C{list} of C{str} or C{unicode}
416 @param channels: A list of the channels which the user is participating in
418 self.sendLine(":%s %s %s %s %s %s * :%s" % (
419 self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName))
420 self.sendLine(":%s %s %s %s %s :%s" % (
421 self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo))
423 self.sendLine(":%s %s %s %s :is an IRC operator" % (
424 self.hostname, RPL_WHOISOPERATOR, user, nick))
425 self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % (
426 self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn))
427 self.sendLine(":%s %s %s %s :%s" % (
428 self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels)))
429 self.sendLine(":%s %s %s %s :End of WHOIS list." % (
430 self.hostname, RPL_ENDOFWHOIS, user, nick))
433 def join(self, who, where):
434 """Send a join message.
436 @type who: C{str} or C{unicode}
437 @param who: The name of the user joining. Should be of the form
438 username!ident@hostmask (unless you know better!).
440 @type where: C{str} or C{unicode}
441 @param where: The channel the user is joining.
443 self.sendLine(":%s JOIN %s" % (who, where))
446 def part(self, who, where, reason=None):
447 """Send a part message.
449 @type who: C{str} or C{unicode}
450 @param who: The name of the user joining. Should be of the form
451 username!ident@hostmask (unless you know better!).
453 @type where: C{str} or C{unicode}
454 @param where: The channel the user is joining.
456 @type reason: C{str} or C{unicode}
457 @param reason: A string describing the misery which caused
458 this poor soul to depart.
461 self.sendLine(":%s PART %s :%s" % (who, where, reason))
463 self.sendLine(":%s PART %s" % (who, where))
466 def channelMode(self, user, channel, mode, *args):
468 Send information about the mode of a channel.
470 @type user: C{str} or C{unicode}
471 @param user: The user receiving the name list. Only their nick
472 name, not the full hostmask.
474 @type channel: C{str} or C{unicode}
475 @param channel: The channel for which this is the namelist.
478 @param mode: A string describing this channel's modes.
480 @param args: Any additional arguments required by the modes.
482 self.sendLine(":%s %s %s %s %s %s" % (
483 self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args)))
486 class IRCClient(basic.LineReceiver):
487 """Internet Relay Chat client protocol, with sprinkles.
489 In addition to providing an interface for an IRC client protocol,
490 this class also contains reasonable implementations of many common
495 - Limit the length of messages sent (because the IRC server probably
497 - Add flood protection/rate limiting for my CTCP replies.
498 - NickServ cooperation. (a mix-in?)
499 - Heartbeat. The transport may die in such a way that it does not realize
500 it is dead until it is written to. Sending something (like \"PING
501 this.irc-host.net\") during idle peroids would alleviate that. If
502 you're concerned with the stability of the host as well as that of the
503 transport, you might care to watch for the corresponding PONG.
505 @ivar nickname: Nickname the client will use.
506 @ivar password: Password used to log on to the server. May be C{None}.
507 @ivar realname: Supplied to the server during login as the \"Real name\"
508 or \"ircname\". May be C{None}.
509 @ivar username: Supplied to the server during login as the \"User name\".
512 @ivar userinfo: Sent in reply to a X{USERINFO} CTCP query. If C{None}, no
513 USERINFO reply will be sent.
514 \"This is used to transmit a string which is settable by
515 the user (and never should be set by the client).\"
516 @ivar fingerReply: Sent in reply to a X{FINGER} CTCP query. If C{None}, no
517 FINGER reply will be sent.
518 @type fingerReply: Callable or String
520 @ivar versionName: CTCP VERSION reply, client name. If C{None}, no VERSION
522 @ivar versionNum: CTCP VERSION reply, client version,
523 @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
525 @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
526 client may be found. If C{None}, no SOURCE reply will be sent.
528 @ivar lineRate: Minimum delay between lines sent to the server. If
529 C{None}, no delay will be imposed.
530 @type lineRate: Number of Seconds.
538 ### Responses to various CTCP queries.
541 # fingerReply is a callable returning a string, or a str()able object.
547 sourceURL = "http://twistedmatrix.com/downloads/"
552 # If this is false, no attempt will be made to identify
553 # ourself to the server.
558 _queueEmptying = None
560 delimiter = '\n' # '\r\n' will also work (see dataReceived)
562 __pychecker__ = 'unusednames=params,prefix,channel'
565 def _reallySendLine(self, line):
566 return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
568 def sendLine(self, line):
569 if self.lineRate is None:
570 self._reallySendLine(line)
572 self._queue.append(line)
573 if not self._queueEmptying:
578 self._reallySendLine(self._queue.pop(0))
579 self._queueEmptying = reactor.callLater(self.lineRate,
582 self._queueEmptying = None
585 ### Interface level client->user output methods
587 ### You'll want to override these.
589 ### Methods relating to the server itself
591 def created(self, when):
592 """Called with creation date information about the server, usually at logon.
595 @param when: A string describing when the server was created, probably.
598 def yourHost(self, info):
599 """Called with daemon information about the server, usually at logon.
602 @param when: A string describing what software the server is running, probably.
605 def myInfo(self, servername, version, umodes, cmodes):
606 """Called with information about the server, usually at logon.
608 @type servername: C{str}
609 @param servername: The hostname of this server.
611 @type version: C{str}
612 @param version: A description of what software this server runs.
615 @param umodes: All the available user modes.
618 @param cmodes: All the available channel modes.
621 def luserClient(self, info):
622 """Called with information about the number of connections, usually at logon.
625 @param info: A description of the number of clients and servers
626 connected to the network, probably.
629 def bounce(self, info):
630 """Called with information about where the client should reconnect.
633 @param info: A plaintext description of the address that should be
637 def isupport(self, options):
638 """Called with various information about what the server supports.
640 @type options: C{list} of C{str}
641 @param options: Descriptions of features or limits of the server, possibly
642 in the form "NAME=VALUE".
645 def luserChannels(self, channels):
646 """Called with the number of channels existant on the server.
648 @type channels: C{int}
651 def luserOp(self, ops):
652 """Called with the number of ops logged on to the server.
657 def luserMe(self, info):
658 """Called with information about the server connected to.
661 @param info: A plaintext string describing the number of users and servers
662 connected to this server.
665 ### Methods involving me directly
667 def privmsg(self, user, channel, message):
668 """Called when I have a message from a user to me or a channel.
672 def joined(self, channel):
673 """Called when I finish joining a channel.
675 channel has the starting character (# or &) intact.
679 def left(self, channel):
680 """Called when I have left a channel.
682 channel has the starting character (# or &) intact.
686 def noticed(self, user, channel, message):
687 """Called when I have a notice from a user to me or a channel.
689 By default, this is equivalent to IRCClient.privmsg, but if your
690 client makes any automated replies, you must override this!
693 The difference between NOTICE and PRIVMSG is that
694 automatic replies MUST NEVER be sent in response to a
695 NOTICE message. [...] The object of this rule is to avoid
696 loops between clients automatically sending something in
697 response to something it received.
699 self.privmsg(user, channel, message)
701 def modeChanged(self, user, channel, set, modes, args):
702 """Called when a channel's modes are changed
705 @param user: The user and hostmask which instigated this change.
707 @type channel: C{str}
708 @param channel: The channel for which the modes are changing.
710 @type set: C{bool} or C{int}
711 @param set: true if the mode is being added, false if it is being
715 @param modes: The mode or modes which are being changed.
718 @param args: Any additional information required for the mode
722 def pong(self, user, secs):
723 """Called with the results of a CTCP PING query.
728 """Called after sucessfully signing on to the server.
732 def kickedFrom(self, channel, kicker, message):
733 """Called when I am kicked from a channel.
737 def nickChanged(self, nick):
738 """Called when my nick has been changed.
743 ### Things I observe other people doing in a channel.
745 def userJoined(self, user, channel):
746 """Called when I see another user joining a channel.
750 def userLeft(self, user, channel):
751 """Called when I see another user leaving a channel.
755 def userQuit(self, user, quitMessage):
756 """Called when I see another user disconnect from the network.
760 def userKicked(self, kickee, channel, kicker, message):
761 """Called when I observe someone else being kicked from a channel.
765 def action(self, user, channel, data):
766 """Called when I see a user perform an ACTION on a channel.
770 def topicUpdated(self, user, channel, newTopic):
771 """In channel, user changed the topic to newTopic.
773 Also called when first joining a channel.
777 def userRenamed(self, oldname, newname):
778 """A user changed their name from oldname to newname.
782 ### Information from the server.
784 def receivedMOTD(self, motd):
785 """I received a message-of-the-day banner from the server.
787 motd is a list of strings, where each string was sent as a seperate
788 message from the server. To display, you might want to use::
790 string.join(motd, '\\n')
792 to get a nicely formatted string.
796 ### user input commands, client->server
797 ### Your client will want to invoke these.
799 def join(self, channel, key=None):
800 if channel[0] not in '&#!+': channel = '#' + channel
802 self.sendLine("JOIN %s %s" % (channel, key))
804 self.sendLine("JOIN %s" % (channel,))
806 def leave(self, channel, reason=None):
807 if channel[0] not in '&#!+': channel = '#' + channel
809 self.sendLine("PART %s :%s" % (channel, reason))
811 self.sendLine("PART %s" % (channel,))
813 def kick(self, channel, user, reason=None):
814 if channel[0] not in '&#!+': channel = '#' + channel
816 self.sendLine("KICK %s %s :%s" % (channel, user, reason))
818 self.sendLine("KICK %s %s" % (channel, user))
822 def topic(self, channel, topic=None):
823 """Attempt to set the topic of the given channel, or ask what it is.
825 If topic is None, then I sent a topic query instead of trying to set
826 the topic. The server should respond with a TOPIC message containing
827 the current topic of the given channel.
829 # << TOPIC #xtestx :fff
830 if channel[0] not in '&#!+': channel = '#' + channel
832 self.sendLine("TOPIC %s :%s" % (channel, topic))
834 self.sendLine("TOPIC %s" % (channel,))
836 def mode(self, chan, set, modes, limit = None, user = None, mask = None):
837 """Change the modes on a user or channel."""
839 line = 'MODE %s +%s' % (chan, modes)
841 line = 'MODE %s -%s' % (chan, modes)
842 if limit is not None:
843 line = '%s %d' % (line, limit)
844 elif user is not None:
845 line = '%s %s' % (line, user)
846 elif mask is not None:
847 line = '%s %s' % (line, mask)
851 def say(self, channel, message, length = None):
852 if channel[0] not in '&#!+': channel = '#' + channel
853 self.msg(channel, message, length)
855 def msg(self, user, message, length = None):
856 """Send a message to a user or channel.
859 @param user: The username or channel name to which to direct the
862 @type message: C{str}
863 @param message: The text to send
866 @param length: The maximum number of octets to send at a time. This
867 has the effect of turning a single call to msg() into multiple
868 commands to the server. This is useful when long messages may be
869 sent that would otherwise cause the server to kick us off or silently
870 truncate the text we are sending. If None is passed, the entire
871 message is always send in one command.
874 fmt = "PRIVMSG %s :%%s" % (user,)
877 self.sendLine(fmt % (message,))
879 # NOTE: minimumLength really equals len(fmt) - 2 (for '%s') + n
880 # where n is how many bytes sendLine sends to end the line.
881 # n was magic numbered to 2, I think incorrectly
882 minimumLength = len(fmt)
883 if length <= minimumLength:
884 raise ValueError("Maximum length must exceed %d for message "
885 "to %s" % (minimumLength, user))
886 lines = split(message, length - minimumLength)
887 map(lambda line, self=self, fmt=fmt: self.sendLine(fmt % line),
890 def notice(self, user, message):
891 self.sendLine("NOTICE %s :%s" % (user, message))
893 def away(self, message=''):
894 self.sendLine("AWAY :%s" % message)
896 def register(self, nickname, hostname='foo', servername='bar'):
897 if self.password is not None:
898 self.sendLine("PASS %s" % self.password)
899 self.setNick(nickname)
900 if self.username is None:
901 self.username = nickname
902 self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname))
904 def setNick(self, nickname):
905 self.nickname = nickname
906 self.sendLine("NICK %s" % nickname)
908 def quit(self, message = ''):
909 self.sendLine("QUIT :%s" % message)
911 ### user input commands, client->client
913 def me(self, channel, action):
916 if channel[0] not in '&#!+': channel = '#' + channel
917 self.ctcpMakeQuery(channel, [('ACTION', action)])
922 def ping(self, user, text = None):
923 """Measure round-trip delay to another IRC client.
925 if self._pings is None:
929 chars = string.letters + string.digits + string.punctuation
930 key = ''.join([random.choice(chars) for i in range(12)])
933 self._pings[(user, key)] = time.time()
934 self.ctcpMakeQuery(user, [('PING', key)])
936 if len(self._pings) > self._MAX_PINGRING:
937 # Remove some of the oldest entries.
938 byValue = [(v, k) for (k, v) in self._pings.items()]
940 excess = self._MAX_PINGRING - len(self._pings)
941 for i in xrange(excess):
942 del self._pings[byValue[i][1]]
944 def dccSend(self, user, file):
945 if type(file) == types.StringType:
946 file = open(file, 'r')
948 size = fileSize(file)
950 name = getattr(file, "name", "file@%s" % (id(file),))
952 factory = DccSendFactory(file)
953 port = reactor.listenTCP(0, factory, 1)
955 raise NotImplementedError,(
956 "XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. "
957 "(and stop accepting once we've made a single connection.)")
959 my_address = struct.pack("!I", my_address)
961 args = ['SEND', name, my_address, str(port)]
963 if not (size is None):
966 args = string.join(args, ' ')
968 self.ctcpMakeQuery(user, [('DCC', args)])
970 def dccResume(self, user, fileName, port, resumePos):
971 """Send a DCC RESUME request to another user."""
972 self.ctcpMakeQuery(user, [
973 ('DCC', ['RESUME', fileName, port, resumePos])])
975 def dccAcceptResume(self, user, fileName, port, resumePos):
976 """Send a DCC ACCEPT response to clients who have requested a resume.
978 self.ctcpMakeQuery(user, [
979 ('DCC', ['ACCEPT', fileName, port, resumePos])])
981 ### server->client messages
982 ### You might want to fiddle with these,
983 ### but it is safe to leave them alone.
985 def irc_ERR_NICKNAMEINUSE(self, prefix, params):
986 self.register(self.nickname+'_')
988 def irc_ERR_PASSWDMISMATCH(self, prefix, params):
989 raise IRCPasswordMismatch("Password Incorrect.")
991 def irc_RPL_WELCOME(self, prefix, params):
994 def irc_JOIN(self, prefix, params):
995 nick = string.split(prefix,'!')[0]
997 if nick == self.nickname:
1000 self.userJoined(nick, channel)
1002 def irc_PART(self, prefix, params):
1003 nick = string.split(prefix,'!')[0]
1005 if nick == self.nickname:
1008 self.userLeft(nick, channel)
1010 def irc_QUIT(self, prefix, params):
1011 nick = string.split(prefix,'!')[0]
1012 self.userQuit(nick, params[0])
1014 def irc_MODE(self, prefix, params):
1015 channel, rest = params[0], params[1:]
1016 set = rest[0][0] == '+'
1019 self.modeChanged(prefix, channel, set, modes, tuple(args))
1021 def irc_PING(self, prefix, params):
1022 self.sendLine("PONG %s" % params[-1])
1024 def irc_PRIVMSG(self, prefix, params):
1027 message = params[-1]
1029 if not message: return # don't raise an exception if some idiot sends us a blank message
1031 if message[0]==X_DELIM:
1032 m = ctcpExtract(message)
1034 self.ctcpQuery(user, channel, m['extended'])
1039 message = string.join(m['normal'], ' ')
1041 self.privmsg(user, channel, message)
1043 def irc_NOTICE(self, prefix, params):
1046 message = params[-1]
1048 if message[0]==X_DELIM:
1049 m = ctcpExtract(message)
1051 self.ctcpReply(user, channel, m['extended'])
1056 message = string.join(m['normal'], ' ')
1058 self.noticed(user, channel, message)
1060 def irc_NICK(self, prefix, params):
1061 nick = string.split(prefix,'!', 1)[0]
1062 if nick == self.nickname:
1063 self.nickChanged(params[0])
1065 self.userRenamed(nick, params[0])
1067 def irc_KICK(self, prefix, params):
1068 """Kicked? Who? Not me, I hope.
1070 kicker = string.split(prefix,'!')[0]
1073 message = params[-1]
1074 if string.lower(kicked) == string.lower(self.nickname):
1076 self.kickedFrom(channel, kicker, message)
1078 self.userKicked(kicked, channel, kicker, message)
1080 def irc_TOPIC(self, prefix, params):
1081 """Someone in the channel set the topic.
1083 user = string.split(prefix, '!')[0]
1085 newtopic = params[1]
1086 self.topicUpdated(user, channel, newtopic)
1088 def irc_RPL_TOPIC(self, prefix, params):
1089 """I just joined the channel, and the server is telling me the current topic.
1091 user = string.split(prefix, '!')[0]
1093 newtopic = params[2]
1094 self.topicUpdated(user, channel, newtopic)
1096 def irc_RPL_NOTOPIC(self, prefix, params):
1097 user = string.split(prefix, '!')[0]
1100 self.topicUpdated(user, channel, newtopic)
1102 def irc_RPL_MOTDSTART(self, prefix, params):
1103 if params[-1].startswith("- "):
1104 params[-1] = params[-1][2:]
1105 self.motd = [params[-1]]
1107 def irc_RPL_MOTD(self, prefix, params):
1108 if params[-1].startswith("- "):
1109 params[-1] = params[-1][2:]
1110 self.motd.append(params[-1])
1112 def irc_RPL_ENDOFMOTD(self, prefix, params):
1113 self.receivedMOTD(self.motd)
1115 def irc_RPL_CREATED(self, prefix, params):
1116 self.created(params[1])
1118 def irc_RPL_YOURHOST(self, prefix, params):
1119 self.yourHost(params[1])
1121 def irc_RPL_MYINFO(self, prefix, params):
1122 info = params[1].split(None, 3)
1123 while len(info) < 4:
1127 def irc_RPL_BOUNCE(self, prefix, params):
1128 # 005 is doubly assigned. Piece of crap dirty trash protocol.
1129 if params[-1] == "are available on this server":
1130 self.isupport(params[1:-1])
1132 self.bounce(params[1])
1134 def irc_RPL_LUSERCLIENT(self, prefix, params):
1135 self.luserClient(params[1])
1137 def irc_RPL_LUSEROP(self, prefix, params):
1139 self.luserOp(int(params[1]))
1143 def irc_RPL_LUSERCHANNELS(self, prefix, params):
1145 self.luserChannels(int(params[1]))
1149 def irc_RPL_LUSERME(self, prefix, params):
1150 self.luserMe(params[1])
1152 def irc_unknown(self, prefix, command, params):
1155 ### Receiving a CTCP query from another party
1156 ### It is safe to leave these alone.
1158 def ctcpQuery(self, user, channel, messages):
1159 """Dispatch method for any CTCP queries received.
1162 method = getattr(self, "ctcpQuery_%s" % m[0], None)
1164 method(user, channel, m[1])
1166 self.ctcpUnknownQuery(user, channel, m[0], m[1])
1168 def ctcpQuery_ACTION(self, user, channel, data):
1169 self.action(user, channel, data)
1171 def ctcpQuery_PING(self, user, channel, data):
1172 nick = string.split(user,"!")[0]
1173 self.ctcpMakeReply(nick, [("PING", data)])
1175 def ctcpQuery_FINGER(self, user, channel, data):
1176 if data is not None:
1177 self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
1179 if not self.fingerReply:
1182 if callable(self.fingerReply):
1183 reply = self.fingerReply()
1185 reply = str(self.fingerReply)
1187 nick = string.split(user,"!")[0]
1188 self.ctcpMakeReply(nick, [('FINGER', reply)])
1190 def ctcpQuery_VERSION(self, user, channel, data):
1191 if data is not None:
1192 self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
1195 if self.versionName:
1196 nick = string.split(user,"!")[0]
1197 self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
1202 def ctcpQuery_SOURCE(self, user, channel, data):
1203 if data is not None:
1204 self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
1207 nick = string.split(user,"!")[0]
1208 # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
1209 # replies should be responded to with the location of an anonymous
1210 # FTP server in host:directory:file format. I'm taking the liberty
1211 # of bringing it into the 21st century by sending a URL instead.
1212 self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
1215 def ctcpQuery_USERINFO(self, user, channel, data):
1216 if data is not None:
1217 self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
1220 nick = string.split(user,"!")[0]
1221 self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
1223 def ctcpQuery_CLIENTINFO(self, user, channel, data):
1224 """A master index of what CTCP tags this client knows.
1226 If no arguments are provided, respond with a list of known tags.
1227 If an argument is provided, provide human-readable help on
1228 the usage of that tag.
1231 nick = string.split(user,"!")[0]
1233 # XXX: prefixedMethodNames gets methods from my *class*,
1234 # but it's entirely possible that this *instance* has more
1236 names = reflect.prefixedMethodNames(self.__class__,
1239 self.ctcpMakeReply(nick, [('CLIENTINFO',
1240 string.join(names, ' '))])
1242 args = string.split(data)
1243 method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
1245 self.ctcpMakeReply(nick, [('ERRMSG',
1247 "Unknown query '%s'"
1248 % (data, args[0]))])
1250 doc = getattr(method, '__doc__', '')
1251 self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
1254 def ctcpQuery_ERRMSG(self, user, channel, data):
1255 # Yeah, this seems strange, but that's what the spec says to do
1256 # when faced with an ERRMSG query (not a reply).
1257 nick = string.split(user,"!")[0]
1258 self.ctcpMakeReply(nick, [('ERRMSG',
1259 "%s :No error has occoured." % data)])
1261 def ctcpQuery_TIME(self, user, channel, data):
1262 if data is not None:
1263 self.quirkyMessage("Why did %s send '%s' with a TIME query?"
1265 nick = string.split(user,"!")[0]
1266 self.ctcpMakeReply(nick,
1268 time.asctime(time.localtime(time.time())))])
1270 def ctcpQuery_DCC(self, user, channel, data):
1271 """Initiate a Direct Client Connection
1275 dcctype = data.split(None, 1)[0].upper()
1276 handler = getattr(self, "dcc_" + dcctype, None)
1278 if self.dcc_sessions is None:
1279 self.dcc_sessions = []
1280 data = data[len(dcctype)+1:]
1281 handler(user, channel, data)
1283 nick = string.split(user,"!")[0]
1284 self.ctcpMakeReply(nick, [('ERRMSG',
1285 "DCC %s :Unknown DCC type '%s'"
1286 % (data, dcctype))])
1287 self.quirkyMessage("%s offered unknown DCC type %s"
1290 def dcc_SEND(self, user, channel, data):
1291 # Use splitQuoted for those who send files with spaces in the names.
1292 data = text.splitQuoted(data)
1294 raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
1296 (filename, address, port) = data[:3]
1298 address = dccParseAddress(address)
1302 raise IRCBadMessage, "Indecipherable port %r" % (port,)
1311 # XXX Should we bother passing this data?
1312 self.dccDoSend(user, address, port, filename, size, data)
1314 def dcc_ACCEPT(self, user, channel, data):
1315 data = text.splitQuoted(data)
1317 raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
1318 (filename, port, resumePos) = data[:3]
1321 resumePos = int(resumePos)
1325 self.dccDoAcceptResume(user, filename, port, resumePos)
1327 def dcc_RESUME(self, user, channel, data):
1328 data = text.splitQuoted(data)
1330 raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
1331 (filename, port, resumePos) = data[:3]
1334 resumePos = int(resumePos)
1337 self.dccDoResume(user, filename, port, resumePos)
1339 def dcc_CHAT(self, user, channel, data):
1340 data = text.splitQuoted(data)
1342 raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
1344 (filename, address, port) = data[:3]
1346 address = dccParseAddress(address)
1350 raise IRCBadMessage, "Indecipherable port %r" % (port,)
1352 self.dccDoChat(user, channel, address, port, data)
1354 ### The dccDo methods are the slightly higher-level siblings of
1355 ### common dcc_ methods; the arguments have been parsed for them.
1357 def dccDoSend(self, user, address, port, fileName, size, data):
1358 """Called when I receive a DCC SEND offer from a client.
1360 By default, I do nothing here."""
1361 ## filename = path.basename(arg)
1362 ## protocol = DccFileReceive(filename, size,
1363 ## (user,channel,data),self.dcc_destdir)
1364 ## reactor.clientTCP(address, port, protocol)
1365 ## self.dcc_sessions.append(protocol)
1368 def dccDoResume(self, user, file, port, resumePos):
1369 """Called when a client is trying to resume an offered file
1370 via DCC send. It should be either replied to with a DCC
1371 ACCEPT or ignored (default)."""
1374 def dccDoAcceptResume(self, user, file, port, resumePos):
1375 """Called when a client has verified and accepted a DCC resume
1376 request made by us. By default it will do nothing."""
1379 def dccDoChat(self, user, channel, address, port, data):
1381 #factory = DccChatFactory(self, queryData=(user, channel, data))
1382 #reactor.connectTCP(address, port, factory)
1383 #self.dcc_sessions.append(factory)
1385 #def ctcpQuery_SED(self, user, data):
1386 # """Simple Encryption Doodoo
1388 # Feel free to implement this, but no specification is available.
1390 # raise NotImplementedError
1392 def ctcpUnknownQuery(self, user, channel, tag, data):
1393 nick = string.split(user,"!")[0]
1394 self.ctcpMakeReply(nick, [('ERRMSG',
1395 "%s %s: Unknown query '%s'"
1396 % (tag, data, tag))])
1398 log.msg("Unknown CTCP query from %s: %s %s\n"
1399 % (user, tag, data))
1401 def ctcpMakeReply(self, user, messages):
1402 """Send one or more X{extended messages} as a CTCP reply.
1404 @type messages: a list of extended messages. An extended
1405 message is a (tag, data) tuple, where 'data' may be C{None}.
1407 self.notice(user, ctcpStringify(messages))
1409 ### client CTCP query commands
1411 def ctcpMakeQuery(self, user, messages):
1412 """Send one or more X{extended messages} as a CTCP query.
1414 @type messages: a list of extended messages. An extended
1415 message is a (tag, data) tuple, where 'data' may be C{None}.
1417 self.msg(user, ctcpStringify(messages))
1419 ### Receiving a response to a CTCP query (presumably to one we made)
1420 ### You may want to add methods here, or override UnknownReply.
1422 def ctcpReply(self, user, channel, messages):
1423 """Dispatch method for any CTCP replies received.
1426 method = getattr(self, "ctcpReply_%s" % m[0], None)
1428 method(user, channel, m[1])
1430 self.ctcpUnknownReply(user, channel, m[0], m[1])
1432 def ctcpReply_PING(self, user, channel, data):
1433 nick = user.split('!', 1)[0]
1434 if (not self._pings) or (not self._pings.has_key((nick, data))):
1435 raise IRCBadMessage,\
1436 "Bogus PING response from %s: %s" % (user, data)
1438 t0 = self._pings[(nick, data)]
1439 self.pong(user, time.time() - t0)
1441 def ctcpUnknownReply(self, user, channel, tag, data):
1442 """Called when a fitting ctcpReply_ method is not found.
1444 XXX: If the client makes arbitrary CTCP queries,
1445 this method should probably show the responses to
1446 them instead of treating them as anomolies.
1448 log.msg("Unknown CTCP reply from %s: %s %s\n"
1449 % (user, tag, data))
1452 ### You may override these with something more appropriate to your UI.
1454 def badMessage(self, line, excType, excValue, tb):
1455 """When I get a message that's so broken I can't use it.
1458 log.msg(string.join(traceback.format_exception(excType,
1462 def quirkyMessage(self, s):
1463 """This is called when I receive a message which is peculiar,
1464 but not wholly indecipherable.
1468 ### Protocool methods
1470 def connectionMade(self):
1472 if self.performLogin:
1473 self.register(self.nickname)
1475 def dataReceived(self, data):
1476 basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
1478 def lineReceived(self, line):
1479 line = lowDequote(line)
1481 prefix, command, params = parsemsg(line)
1482 if numeric_to_symbolic.has_key(command):
1483 command = numeric_to_symbolic[command]
1484 self.handleCommand(command, prefix, params)
1485 except IRCBadMessage:
1486 self.badMessage(line, *sys.exc_info())
1489 def handleCommand(self, command, prefix, params):
1490 """Determine the function to call for the given command and call
1491 it with the given arguments.
1493 method = getattr(self, "irc_%s" % command, None)
1495 if method is not None:
1496 method(prefix, params)
1498 self.irc_unknown(prefix, command, params)
1503 def __getstate__(self):
1504 dct = self.__dict__.copy()
1505 dct['dcc_sessions'] = None
1506 dct['_pings'] = None
1510 def dccParseAddress(address):
1515 address = long(address)
1517 raise IRCBadMessage,\
1518 "Indecipherable address %r" % (address,)
1521 (address >> 24) & 0xFF,
1522 (address >> 16) & 0xFF,
1523 (address >> 8) & 0xFF,
1526 address = '.'.join(map(str,address))
1530 class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
1531 """Bare protocol to receive a Direct Client Connection SEND stream.
1533 This does enough to keep the other guy talking, but you'll want to
1534 extend my dataReceived method to *do* something with the data I get.
1539 def __init__(self, resumeOffset=0):
1540 self.bytesReceived = resumeOffset
1541 self.resume = (resumeOffset != 0)
1543 def dataReceived(self, data):
1544 """Called when data is received.
1546 Warning: This just acknowledges to the remote host that the
1547 data has been received; it doesn't *do* anything with the
1548 data, so you'll want to override this.
1550 self.bytesReceived = self.bytesReceived + len(data)
1551 self.transport.write(struct.pack('!i', self.bytesReceived))
1554 class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
1555 """Protocol for an outgoing Direct Client Connection SEND.
1564 def __init__(self, file):
1565 if type(file) is types.StringType:
1566 self.file = open(file, 'r')
1568 def connectionMade(self):
1572 def dataReceived(self, data):
1573 # XXX: Do we need to check to see if len(data) != fmtsize?
1575 bytesShesGot = struct.unpack("!I", data)
1576 if bytesShesGot < self.bytesSent:
1578 # XXX? Add some checks to see if we've stalled out?
1580 elif bytesShesGot > self.bytesSent:
1581 # self.transport.log("DCC SEND %s: She says she has %d bytes "
1582 # "but I've only sent %d. I'm stopping "
1583 # "this screwy transfer."
1585 # bytesShesGot, self.bytesSent))
1586 self.transport.loseConnection()
1591 def sendBlock(self):
1592 block = self.file.read(self.blocksize)
1594 self.transport.write(block)
1595 self.bytesSent = self.bytesSent + len(block)
1597 # Nothing more to send, transfer complete.
1598 self.transport.loseConnection()
1601 def connectionLost(self, reason):
1603 if hasattr(self.file, "close"):
1607 class DccSendFactory(protocol.Factory):
1608 protocol = DccSendProtocol
1609 def __init__(self, file):
1612 def buildProtocol(self, connection):
1613 p = self.protocol(self.file)
1619 """I'll try my damndest to determine the size of this file object.
1622 if hasattr(file, "fileno"):
1623 fileno = file.fileno()
1625 stat_ = os.fstat(fileno)
1626 size = stat_[stat.ST_SIZE]
1632 if hasattr(file, "name") and path.exists(file.name):
1634 size = path.getsize(file.name)
1640 if hasattr(file, "seek") and hasattr(file, "tell"):
1654 class DccChat(basic.LineReceiver, styles.Ephemeral):
1655 """Direct Client Connection protocol type CHAT.
1657 DCC CHAT is really just your run o' the mill basic.LineReceiver
1658 protocol. This class only varies from that slightly, accepting
1659 either LF or CR LF for a line delimeter for incoming messages
1660 while always using CR LF for outgoing.
1662 The lineReceived method implemented here uses the DCC connection's
1663 'client' attribute (provided upon construction) to deliver incoming
1664 lines from the DCC chat via IRCClient's normal privmsg interface.
1665 That's something of a spoof, which you may well want to override.
1674 def __init__(self, client, queryData=None):
1675 """Initialize a new DCC CHAT session.
1677 queryData is a 3-tuple of
1678 (fromUser, targetUserOrChannel, data)
1679 as received by the CTCP query.
1681 (To be honest, fromUser is the only thing that's currently
1682 used here. targetUserOrChannel is potentially useful, while
1683 the 'data' argument is soley for informational purposes.)
1685 self.client = client
1687 self.queryData = queryData
1688 self.remoteParty = self.queryData[0]
1690 def dataReceived(self, data):
1691 self.buffer = self.buffer + data
1692 lines = string.split(self.buffer, LF)
1693 # Put the (possibly empty) element after the last LF back in the
1695 self.buffer = lines.pop()
1700 self.lineReceived(line)
1702 def lineReceived(self, line):
1703 log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
1704 self.client.privmsg(self.remoteParty,
1705 self.client.nickname, line)
1708 class DccChatFactory(protocol.ClientFactory):
1711 def __init__(self, client, queryData):
1712 self.client = client
1713 self.queryData = queryData
1715 def buildProtocol(self, addr):
1716 p = self.protocol(client=self.client, queryData=self.queryData)
1719 def clientConnectionFailed(self, unused_connector, unused_reason):
1720 self.client.dcc_sessions.remove(self)
1722 def clientConnectionLost(self, unused_connector, unused_reason):
1723 self.client.dcc_sessions.remove(self)
1726 def dccDescribe(data):
1727 """Given the data chunk from a DCC query, return a descriptive string.
1731 data = string.split(data)
1735 (dcctype, arg, address, port) = data[:4]
1741 address = long(address)
1746 (address >> 24) & 0xFF,
1747 (address >> 16) & 0xFF,
1748 (address >> 8) & 0xFF,
1751 # The mapping to 'int' is to get rid of those accursed
1752 # "L"s which python 1.5.2 puts on the end of longs.
1753 address = string.join(map(str,map(int,address)), ".")
1755 if dcctype == 'SEND':
1762 size_txt = ' of size %d bytes' % (size,)
1766 dcc_text = ("SEND for file '%s'%s at host %s, port %s"
1767 % (filename, size_txt, address, port))
1768 elif dcctype == 'CHAT':
1769 dcc_text = ("CHAT for host %s, port %s"
1772 dcc_text = orig_data
1777 class DccFileReceive(DccFileReceiveBasic):
1778 """Higher-level coverage for getting a file from DCC SEND.
1780 I allow you to change the file's name and destination directory.
1781 I won't overwrite an existing file unless I've been told it's okay
1782 to do so. If passed the resumeOffset keyword argument I will attempt to
1783 resume the file from that amount of bytes.
1785 XXX: I need to let the client know when I am finished.
1786 XXX: I need to decide how to keep a progress indicator updated.
1787 XXX: Client needs a way to tell me \"Do not finish until I say so.\"
1788 XXX: I need to make sure the client understands if the file cannot be written.
1798 def __init__(self, filename, fileSize=-1, queryData=None,
1799 destDir='.', resumeOffset=0):
1800 DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
1801 self.filename = filename
1802 self.destDir = destDir
1803 self.fileSize = fileSize
1806 self.queryData = queryData
1807 self.fromUser = self.queryData[0]
1809 def set_directory(self, directory):
1810 """Set the directory where the downloaded file will be placed.
1812 May raise OSError if the supplied directory path is not suitable.
1814 if not path.exists(directory):
1815 raise OSError(errno.ENOENT, "You see no directory there.",
1817 if not path.isdir(directory):
1818 raise OSError(errno.ENOTDIR, "You cannot put a file into "
1819 "something which is not a directory.",
1821 if not os.access(directory, os.X_OK | os.W_OK):
1822 raise OSError(errno.EACCES,
1823 "This directory is too hard to write in to.",
1825 self.destDir = directory
1827 def set_filename(self, filename):
1828 """Change the name of the file being transferred.
1830 This replaces the file name provided by the sender.
1832 self.filename = filename
1834 def set_overwrite(self, boolean):
1835 """May I overwrite existing files?
1837 self.overwrite = boolean
1840 # Protocol-level methods.
1842 def connectionMade(self):
1843 dst = path.abspath(path.join(self.destDir,self.filename))
1844 exists = path.exists(dst)
1845 if self.resume and exists:
1846 # I have been told I want to resume, and a file already
1847 # exists - Here we go
1848 self.file = open(dst, 'ab')
1849 log.msg("Attempting to resume %s - starting from %d bytes" %
1850 (self.file, self.file.tell()))
1851 elif self.overwrite or not exists:
1852 self.file = open(dst, 'wb')
1854 raise OSError(errno.EEXIST,
1855 "There's a file in the way. "
1856 "Perhaps that's why you cannot open it.",
1859 def dataReceived(self, data):
1860 self.file.write(data)
1861 DccFileReceiveBasic.dataReceived(self, data)
1863 # XXX: update a progress indicator here?
1865 def connectionLost(self, reason):
1866 """When the connection is lost, I close the file.
1869 logmsg = ("%s closed." % (self,))
1870 if self.fileSize > 0:
1871 logmsg = ("%s %d/%d bytes received"
1872 % (logmsg, self.bytesReceived, self.fileSize))
1873 if self.bytesReceived == self.fileSize:
1875 elif self.bytesReceived < self.fileSize:
1876 logmsg = ("%s (Warning: %d bytes short)"
1877 % (logmsg, self.fileSize - self.bytesReceived))
1879 logmsg = ("%s (file larger than expected)"
1882 logmsg = ("%s %d bytes received"
1883 % (logmsg, self.bytesReceived))
1885 if hasattr(self, 'file'):
1886 logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
1887 if hasattr(self.file, 'close'): self.file.close()
1889 # self.transport.log(logmsg)
1892 if not self.connected:
1893 return "<Unconnected DccFileReceive object at %x>" % (id(self),)
1894 from_ = self.transport.getPeer()
1896 from_ = "%s (%s)" % (self.fromUser, from_)
1898 s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
1902 s = ("<%s at %x: GET %s>"
1903 % (self.__class__, id(self), self.filename))
1907 # CTCP constants and helper functions
1911 def ctcpExtract(message):
1912 """Extract CTCP data from a string.
1914 Returns a dictionary with two items:
1916 - C{'extended'}: a list of CTCP (tag, data) tuples
1917 - C{'normal'}: a list of strings which were not inside a CTCP delimeter
1920 extended_messages = []
1921 normal_messages = []
1922 retval = {'extended': extended_messages,
1923 'normal': normal_messages }
1925 messages = string.split(message, X_DELIM)
1928 # X1 extended data X2 nomal data X3 extended data X4 normal...
1931 extended_messages.append(messages.pop(0))
1933 normal_messages.append(messages.pop(0))
1936 extended_messages[:] = filter(None, extended_messages)
1937 normal_messages[:] = filter(None, normal_messages)
1939 extended_messages[:] = map(ctcpDequote, extended_messages)
1940 for i in xrange(len(extended_messages)):
1941 m = string.split(extended_messages[i], SPC, 1)
1948 extended_messages[i] = (tag, data)
1960 M_QUOTE: M_QUOTE + M_QUOTE
1964 for k, v in mQuoteTable.items():
1965 mDequoteTable[v[-1]] = k
1968 mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
1971 for c in (M_QUOTE, NUL, NL, CR):
1972 s = string.replace(s, c, mQuoteTable[c])
1976 def sub(matchobj, mDequoteTable=mDequoteTable):
1977 s = matchobj.group()[1]
1979 s = mDequoteTable[s]
1984 return mEscape_re.sub(sub, s)
1989 X_DELIM: X_QUOTE + 'a',
1990 X_QUOTE: X_QUOTE + X_QUOTE
1995 for k, v in xQuoteTable.items():
1996 xDequoteTable[v[-1]] = k
1998 xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
2001 for c in (X_QUOTE, X_DELIM):
2002 s = string.replace(s, c, xQuoteTable[c])
2006 def sub(matchobj, xDequoteTable=xDequoteTable):
2007 s = matchobj.group()[1]
2009 s = xDequoteTable[s]
2014 return xEscape_re.sub(sub, s)
2016 def ctcpStringify(messages):
2018 @type messages: a list of extended messages. An extended
2019 message is a (tag, data) tuple, where 'data' may be C{None}, a
2020 string, or a list of strings to be joined with whitespace.
2025 for (tag, data) in messages:
2027 if not isinstance(data, types.StringType):
2029 # data as list-of-strings
2030 data = " ".join(map(str, data))
2032 # No? Then use it's %s representation.
2034 m = "%s %s" % (tag, data)
2038 m = "%s%s%s" % (X_DELIM, m, X_DELIM)
2039 coded_messages.append(m)
2041 line = string.join(coded_messages, '')
2045 # Constants (from RFC 2812)
2047 RPL_YOURHOST = '002'
2051 RPL_USERHOST = '302'
2056 RPL_WHOISUSER = '311'
2057 RPL_WHOISSERVER = '312'
2058 RPL_WHOISOPERATOR = '313'
2059 RPL_WHOISIDLE = '317'
2060 RPL_ENDOFWHOIS = '318'
2061 RPL_WHOISCHANNELS = '319'
2062 RPL_WHOWASUSER = '314'
2063 RPL_ENDOFWHOWAS = '369'
2064 RPL_LISTSTART = '321'
2067 RPL_UNIQOPIS = '325'
2068 RPL_CHANNELMODEIS = '324'
2071 RPL_INVITING = '341'
2072 RPL_SUMMONING = '342'
2073 RPL_INVITELIST = '346'
2074 RPL_ENDOFINVITELIST = '347'
2075 RPL_EXCEPTLIST = '348'
2076 RPL_ENDOFEXCEPTLIST = '349'
2078 RPL_WHOREPLY = '352'
2079 RPL_ENDOFWHO = '315'
2080 RPL_NAMREPLY = '353'
2081 RPL_ENDOFNAMES = '366'
2083 RPL_ENDOFLINKS = '365'
2085 RPL_ENDOFBANLIST = '368'
2087 RPL_ENDOFINFO = '374'
2088 RPL_MOTDSTART = '375'
2090 RPL_ENDOFMOTD = '376'
2091 RPL_YOUREOPER = '381'
2092 RPL_REHASHING = '382'
2093 RPL_YOURESERVICE = '383'
2095 RPL_USERSSTART = '392'
2097 RPL_ENDOFUSERS = '394'
2099 RPL_TRACELINK = '200'
2100 RPL_TRACECONNECTING = '201'
2101 RPL_TRACEHANDSHAKE = '202'
2102 RPL_TRACEUNKNOWN = '203'
2103 RPL_TRACEOPERATOR = '204'
2104 RPL_TRACEUSER = '205'
2105 RPL_TRACESERVER = '206'
2106 RPL_TRACESERVICE = '207'
2107 RPL_TRACENEWTYPE = '208'
2108 RPL_TRACECLASS = '209'
2109 RPL_TRACERECONNECT = '210'
2110 RPL_TRACELOG = '261'
2111 RPL_TRACEEND = '262'
2112 RPL_STATSLINKINFO = '211'
2113 RPL_STATSCOMMANDS = '212'
2114 RPL_ENDOFSTATS = '219'
2115 RPL_STATSUPTIME = '242'
2116 RPL_STATSOLINE = '243'
2118 RPL_SERVLIST = '234'
2119 RPL_SERVLISTEND = '235'
2120 RPL_LUSERCLIENT = '251'
2122 RPL_LUSERUNKNOWN = '253'
2123 RPL_LUSERCHANNELS = '254'
2126 RPL_ADMINLOC = '257'
2127 RPL_ADMINLOC = '258'
2128 RPL_ADMINEMAIL = '259'
2129 RPL_TRYAGAIN = '263'
2130 ERR_NOSUCHNICK = '401'
2131 ERR_NOSUCHSERVER = '402'
2132 ERR_NOSUCHCHANNEL = '403'
2133 ERR_CANNOTSENDTOCHAN = '404'
2134 ERR_TOOMANYCHANNELS = '405'
2135 ERR_WASNOSUCHNICK = '406'
2136 ERR_TOOMANYTARGETS = '407'
2137 ERR_NOSUCHSERVICE = '408'
2138 ERR_NOORIGIN = '409'
2139 ERR_NORECIPIENT = '411'
2140 ERR_NOTEXTTOSEND = '412'
2141 ERR_NOTOPLEVEL = '413'
2142 ERR_WILDTOPLEVEL = '414'
2144 ERR_UNKNOWNCOMMAND = '421'
2146 ERR_NOADMININFO = '423'
2147 ERR_FILEERROR = '424'
2148 ERR_NONICKNAMEGIVEN = '431'
2149 ERR_ERRONEUSNICKNAME = '432'
2150 ERR_NICKNAMEINUSE = '433'
2151 ERR_NICKCOLLISION = '436'
2152 ERR_UNAVAILRESOURCE = '437'
2153 ERR_USERNOTINCHANNEL = '441'
2154 ERR_NOTONCHANNEL = '442'
2155 ERR_USERONCHANNEL = '443'
2157 ERR_SUMMONDISABLED = '445'
2158 ERR_USERSDISABLED = '446'
2159 ERR_NOTREGISTERED = '451'
2160 ERR_NEEDMOREPARAMS = '461'
2161 ERR_ALREADYREGISTRED = '462'
2162 ERR_NOPERMFORHOST = '463'
2163 ERR_PASSWDMISMATCH = '464'
2164 ERR_YOUREBANNEDCREEP = '465'
2165 ERR_YOUWILLBEBANNED = '466'
2167 ERR_CHANNELISFULL = '471'
2168 ERR_UNKNOWNMODE = '472'
2169 ERR_INVITEONLYCHAN = '473'
2170 ERR_BANNEDFROMCHAN = '474'
2171 ERR_BADCHANNELKEY = '475'
2172 ERR_BADCHANMASK = '476'
2173 ERR_NOCHANMODES = '477'
2174 ERR_BANLISTFULL = '478'
2175 ERR_NOPRIVILEGES = '481'
2176 ERR_CHANOPRIVSNEEDED = '482'
2177 ERR_CANTKILLSERVER = '483'
2178 ERR_RESTRICTED = '484'
2179 ERR_UNIQOPPRIVSNEEDED = '485'
2180 ERR_NOOPERHOST = '491'
2181 ERR_NOSERVICEHOST = '492'
2182 ERR_UMODEUNKNOWNFLAG = '501'
2183 ERR_USERSDONTMATCH = '502'
2185 # And hey, as long as the strings are already intern'd...
2186 symbolic_to_numeric = {
2187 "RPL_WELCOME": '001',
2188 "RPL_YOURHOST": '002',
2189 "RPL_CREATED": '003',
2190 "RPL_MYINFO": '004',
2191 "RPL_BOUNCE": '005',
2192 "RPL_USERHOST": '302',
2195 "RPL_UNAWAY": '305',
2196 "RPL_NOWAWAY": '306',
2197 "RPL_WHOISUSER": '311',
2198 "RPL_WHOISSERVER": '312',
2199 "RPL_WHOISOPERATOR": '313',
2200 "RPL_WHOISIDLE": '317',
2201 "RPL_ENDOFWHOIS": '318',
2202 "RPL_WHOISCHANNELS": '319',
2203 "RPL_WHOWASUSER": '314',
2204 "RPL_ENDOFWHOWAS": '369',
2205 "RPL_LISTSTART": '321',
2207 "RPL_LISTEND": '323',
2208 "RPL_UNIQOPIS": '325',
2209 "RPL_CHANNELMODEIS": '324',
2210 "RPL_NOTOPIC": '331',
2212 "RPL_INVITING": '341',
2213 "RPL_SUMMONING": '342',
2214 "RPL_INVITELIST": '346',
2215 "RPL_ENDOFINVITELIST": '347',
2216 "RPL_EXCEPTLIST": '348',
2217 "RPL_ENDOFEXCEPTLIST": '349',
2218 "RPL_VERSION": '351',
2219 "RPL_WHOREPLY": '352',
2220 "RPL_ENDOFWHO": '315',
2221 "RPL_NAMREPLY": '353',
2222 "RPL_ENDOFNAMES": '366',
2224 "RPL_ENDOFLINKS": '365',
2225 "RPL_BANLIST": '367',
2226 "RPL_ENDOFBANLIST": '368',
2228 "RPL_ENDOFINFO": '374',
2229 "RPL_MOTDSTART": '375',
2231 "RPL_ENDOFMOTD": '376',
2232 "RPL_YOUREOPER": '381',
2233 "RPL_REHASHING": '382',
2234 "RPL_YOURESERVICE": '383',
2236 "RPL_USERSSTART": '392',
2238 "RPL_ENDOFUSERS": '394',
2239 "RPL_NOUSERS": '395',
2240 "RPL_TRACELINK": '200',
2241 "RPL_TRACECONNECTING": '201',
2242 "RPL_TRACEHANDSHAKE": '202',
2243 "RPL_TRACEUNKNOWN": '203',
2244 "RPL_TRACEOPERATOR": '204',
2245 "RPL_TRACEUSER": '205',
2246 "RPL_TRACESERVER": '206',
2247 "RPL_TRACESERVICE": '207',
2248 "RPL_TRACENEWTYPE": '208',
2249 "RPL_TRACECLASS": '209',
2250 "RPL_TRACERECONNECT": '210',
2251 "RPL_TRACELOG": '261',
2252 "RPL_TRACEEND": '262',
2253 "RPL_STATSLINKINFO": '211',
2254 "RPL_STATSCOMMANDS": '212',
2255 "RPL_ENDOFSTATS": '219',
2256 "RPL_STATSUPTIME": '242',
2257 "RPL_STATSOLINE": '243',
2258 "RPL_UMODEIS": '221',
2259 "RPL_SERVLIST": '234',
2260 "RPL_SERVLISTEND": '235',
2261 "RPL_LUSERCLIENT": '251',
2262 "RPL_LUSEROP": '252',
2263 "RPL_LUSERUNKNOWN": '253',
2264 "RPL_LUSERCHANNELS": '254',
2265 "RPL_LUSERME": '255',
2266 "RPL_ADMINME": '256',
2267 "RPL_ADMINLOC": '257',
2268 "RPL_ADMINLOC": '258',
2269 "RPL_ADMINEMAIL": '259',
2270 "RPL_TRYAGAIN": '263',
2271 "ERR_NOSUCHNICK": '401',
2272 "ERR_NOSUCHSERVER": '402',
2273 "ERR_NOSUCHCHANNEL": '403',
2274 "ERR_CANNOTSENDTOCHAN": '404',
2275 "ERR_TOOMANYCHANNELS": '405',
2276 "ERR_WASNOSUCHNICK": '406',
2277 "ERR_TOOMANYTARGETS": '407',
2278 "ERR_NOSUCHSERVICE": '408',
2279 "ERR_NOORIGIN": '409',
2280 "ERR_NORECIPIENT": '411',
2281 "ERR_NOTEXTTOSEND": '412',
2282 "ERR_NOTOPLEVEL": '413',
2283 "ERR_WILDTOPLEVEL": '414',
2284 "ERR_BADMASK": '415',
2285 "ERR_UNKNOWNCOMMAND": '421',
2286 "ERR_NOMOTD": '422',
2287 "ERR_NOADMININFO": '423',
2288 "ERR_FILEERROR": '424',
2289 "ERR_NONICKNAMEGIVEN": '431',
2290 "ERR_ERRONEUSNICKNAME": '432',
2291 "ERR_NICKNAMEINUSE": '433',
2292 "ERR_NICKCOLLISION": '436',
2293 "ERR_UNAVAILRESOURCE": '437',
2294 "ERR_USERNOTINCHANNEL": '441',
2295 "ERR_NOTONCHANNEL": '442',
2296 "ERR_USERONCHANNEL": '443',
2297 "ERR_NOLOGIN": '444',
2298 "ERR_SUMMONDISABLED": '445',
2299 "ERR_USERSDISABLED": '446',
2300 "ERR_NOTREGISTERED": '451',
2301 "ERR_NEEDMOREPARAMS": '461',
2302 "ERR_ALREADYREGISTRED": '462',
2303 "ERR_NOPERMFORHOST": '463',
2304 "ERR_PASSWDMISMATCH": '464',
2305 "ERR_YOUREBANNEDCREEP": '465',
2306 "ERR_YOUWILLBEBANNED": '466',
2307 "ERR_KEYSET": '467',
2308 "ERR_CHANNELISFULL": '471',
2309 "ERR_UNKNOWNMODE": '472',
2310 "ERR_INVITEONLYCHAN": '473',
2311 "ERR_BANNEDFROMCHAN": '474',
2312 "ERR_BADCHANNELKEY": '475',
2313 "ERR_BADCHANMASK": '476',
2314 "ERR_NOCHANMODES": '477',
2315 "ERR_BANLISTFULL": '478',
2316 "ERR_NOPRIVILEGES": '481',
2317 "ERR_CHANOPRIVSNEEDED": '482',
2318 "ERR_CANTKILLSERVER": '483',
2319 "ERR_RESTRICTED": '484',
2320 "ERR_UNIQOPPRIVSNEEDED": '485',
2321 "ERR_NOOPERHOST": '491',
2322 "ERR_NOSERVICEHOST": '492',
2323 "ERR_UMODEUNKNOWNFLAG": '501',
2324 "ERR_USERSDONTMATCH": '502',
2327 numeric_to_symbolic = {}
2328 for k, v in symbolic_to_numeric.items():
2329 numeric_to_symbolic[v] = k