OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.words.test.test_irc -*- | |
2 # Copyright (c) 2001-2005 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """Internet Relay Chat Protocol for client and server. | |
7 | |
8 Future Plans | |
9 ============ | |
10 | |
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? | |
16 | |
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. | |
20 | |
21 Test coverage needs to be better. | |
22 | |
23 @author: U{Kevin Turner<mailto:acapnotic@twistedmatrix.com>} | |
24 | |
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>} | |
29 """ | |
30 | |
31 __version__ = '$Revision: 1.94 $'[11:-2] | |
32 | |
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 | |
37 | |
38 # System Imports | |
39 | |
40 import errno | |
41 import os | |
42 import random | |
43 import re | |
44 import stat | |
45 import string | |
46 import struct | |
47 import sys | |
48 import time | |
49 import types | |
50 import traceback | |
51 import socket | |
52 | |
53 from os import path | |
54 | |
55 NUL = chr(0) | |
56 CR = chr(015) | |
57 NL = chr(012) | |
58 LF = NL | |
59 SPC = chr(040) | |
60 | |
61 CHANNEL_PREFIXES = '&#!+' | |
62 | |
63 class IRCBadMessage(Exception): | |
64 pass | |
65 | |
66 class IRCPasswordMismatch(Exception): | |
67 pass | |
68 | |
69 def parsemsg(s): | |
70 """Breaks a message from an IRC server into its prefix, command, and argumen
ts. | |
71 """ | |
72 prefix = '' | |
73 trailing = [] | |
74 if not s: | |
75 raise IRCBadMessage("Empty line.") | |
76 if s[0] == ':': | |
77 prefix, s = s[1:].split(' ', 1) | |
78 if s.find(' :') != -1: | |
79 s, trailing = s.split(' :', 1) | |
80 args = s.split() | |
81 args.append(trailing) | |
82 else: | |
83 args = s.split() | |
84 command = args.pop(0) | |
85 return prefix, command, args | |
86 | |
87 | |
88 def split(str, length = 80): | |
89 """I break a message into multiple lines. | |
90 | |
91 I prefer to break at whitespace near str[length]. I also break at \\n. | |
92 | |
93 @returns: list of strings | |
94 """ | |
95 if length <= 0: | |
96 raise ValueError("Length must be a number greater than zero") | |
97 r = [] | |
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:] | |
102 else: | |
103 i = n == -1 and w or n | |
104 line, str = str[:i], str[i+1:] | |
105 r.append(line) | |
106 if len(str): | |
107 r.extend(str.split('\n')) | |
108 return r | |
109 | |
110 class IRC(protocol.Protocol): | |
111 """Internet Relay Chat server protocol. | |
112 """ | |
113 | |
114 buffer = "" | |
115 hostname = None | |
116 | |
117 encoding = None | |
118 | |
119 def connectionMade(self): | |
120 self.channels = [] | |
121 if self.hostname is None: | |
122 self.hostname = socket.getfqdn() | |
123 | |
124 | |
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)) | |
130 | |
131 | |
132 def sendMessage(self, command, *parameter_list, **prefix): | |
133 """Send a line formatted as an IRC message. | |
134 | |
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'. | |
138 """ | |
139 | |
140 if not command: | |
141 raise ValueError, "IRC message requires a command." | |
142 | |
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 | |
148 | |
149 line = string.join([command] + list(parameter_list)) | |
150 if prefix.has_key('prefix'): | |
151 line = ":%s %s" % (prefix['prefix'], line) | |
152 self.sendLine(line) | |
153 | |
154 if len(parameter_list) > 15: | |
155 log.msg("Message has %d parameters (RFC allows 15):\n%s" % | |
156 (len(parameter_list), line)) | |
157 | |
158 | |
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 | |
163 required.) | |
164 """ | |
165 lines = (self.buffer + data).split(LF) | |
166 # Put the (possibly empty) element after the last LF back in the | |
167 # buffer | |
168 self.buffer = lines.pop() | |
169 | |
170 for line in lines: | |
171 if len(line) <= 2: | |
172 # This is a blank line, at best. | |
173 continue | |
174 if line[-1] == CR: | |
175 line = line[:-1] | |
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)) | |
180 | |
181 self.handleCommand(command, prefix, params) | |
182 | |
183 | |
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. | |
187 """ | |
188 method = getattr(self, "irc_%s" % command, None) | |
189 try: | |
190 if method is not None: | |
191 method(prefix, params) | |
192 else: | |
193 self.irc_unknown(prefix, command, params) | |
194 except: | |
195 log.deferr() | |
196 | |
197 | |
198 def irc_unknown(self, prefix, command, params): | |
199 """Implement me!""" | |
200 raise NotImplementedError(command, prefix, params) | |
201 | |
202 | |
203 # Helper methods | |
204 def privmsg(self, sender, recip, message): | |
205 """Send a message to a channel or user | |
206 | |
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!). | |
210 | |
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. | |
214 | |
215 @type message: C{str} or C{unicode} | |
216 @param message: The message being sent. | |
217 """ | |
218 self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message))) | |
219 | |
220 | |
221 def notice(self, sender, recip, message): | |
222 """Send a \"notice\" to a channel or user. | |
223 | |
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. | |
227 | |
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!). | |
231 | |
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. | |
235 | |
236 @type message: C{str} or C{unicode} | |
237 @param message: The message being sent. | |
238 """ | |
239 self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message)) | |
240 | |
241 | |
242 def action(self, sender, recip, message): | |
243 """Send an action to a channel or user. | |
244 | |
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!). | |
248 | |
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. | |
252 | |
253 @type message: C{str} or C{unicode} | |
254 @param message: The action being sent. | |
255 """ | |
256 self.sendLine(":%s ACTION %s :%s" % (sender, recip, message)) | |
257 | |
258 | |
259 def topic(self, user, channel, topic, author=None): | |
260 """Send the topic to a user. | |
261 | |
262 @type user: C{str} or C{unicode} | |
263 @param user: The user receiving the topic. Only their nick name, not | |
264 the full hostmask. | |
265 | |
266 @type channel: C{str} or C{unicode} | |
267 @param channel: The channel for which this is the topic. | |
268 | |
269 @type topic: C{str} or C{unicode} or C{None} | |
270 @param topic: The topic string, unquoted, or None if there is | |
271 no topic. | |
272 | |
273 @type author: C{str} or C{unicode} | |
274 @param author: If the topic is being changed, the full username and host
mask | |
275 of the person changing it. | |
276 """ | |
277 if author is None: | |
278 if topic is None: | |
279 self.sendLine(':%s %s %s %s :%s' % ( | |
280 self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.
')) | |
281 else: | |
282 self.sendLine(":%s %s %s %s :%s" % ( | |
283 self.hostname, RPL_TOPIC, user, channel, lowQuote(topic))) | |
284 else: | |
285 self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)
)) | |
286 | |
287 | |
288 def topicAuthor(self, user, channel, author, date): | |
289 """ | |
290 Send the author of and time at which a topic was set for the given | |
291 channel. | |
292 | |
293 This sends a 333 reply message, which is not part of the IRC RFC. | |
294 | |
295 @type user: C{str} or C{unicode} | |
296 @param user: The user receiving the topic. Only their nick name, not | |
297 the full hostmask. | |
298 | |
299 @type channel: C{str} or C{unicode} | |
300 @param channel: The channel for which this information is relevant. | |
301 | |
302 @type author: C{str} or C{unicode} | |
303 @param author: The nickname (without hostmask) of the user who last | |
304 set the topic. | |
305 | |
306 @type date: C{int} | |
307 @param date: A POSIX timestamp (number of seconds since the epoch) | |
308 at which the topic was last set. | |
309 """ | |
310 self.sendLine(':%s %d %s %s %s %d' % ( | |
311 self.hostname, 333, user, channel, author, date)) | |
312 | |
313 | |
314 def names(self, user, channel, names): | |
315 """Send the names of a channel's participants to a user. | |
316 | |
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. | |
320 | |
321 @type channel: C{str} or C{unicode} | |
322 @param channel: The channel for which this is the namelist. | |
323 | |
324 @type names: C{list} of C{str} or C{unicode} | |
325 @param names: The names to send. | |
326 """ | |
327 # XXX If unicode is given, these limits are not quite correct | |
328 prefixLength = len(channel) + len(user) + 10 | |
329 namesLength = 512 - prefixLength | |
330 | |
331 L = [] | |
332 count = 0 | |
333 for n in names: | |
334 if count + len(n) + 1 > namesLength: | |
335 self.sendLine(":%s %s %s = %s :%s" % ( | |
336 self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L))) | |
337 L = [n] | |
338 count = len(n) | |
339 else: | |
340 L.append(n) | |
341 count += len(n) + 1 | |
342 if 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)) | |
347 | |
348 | |
349 def who(self, user, channel, memberInfo): | |
350 """ | |
351 Send a list of users participating in a channel. | |
352 | |
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. | |
356 | |
357 @type channel: C{str} or C{unicode} | |
358 @param channel: The channel for which this is the member | |
359 information. | |
360 | |
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 | |
366 real name. | |
367 """ | |
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)) | |
374 | |
375 self.sendLine(":%s %s %s %s :End of /WHO list." % ( | |
376 self.hostname, RPL_ENDOFWHO, user, channel)) | |
377 | |
378 | |
379 def whois(self, user, nick, username, hostname, realName, server, serverInfo
, oper, idle, signOn, channels): | |
380 """ | |
381 Send information about the state of a particular user. | |
382 | |
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. | |
386 | |
387 @type nick: C{str} or C{unicode} | |
388 @param nick: The nickname of the user this information describes. | |
389 | |
390 @type username: C{str} or C{unicode} | |
391 @param username: The user's username (eg, ident response) | |
392 | |
393 @type hostname: C{str} | |
394 @param hostname: The user's hostmask | |
395 | |
396 @type realName: C{str} or C{unicode} | |
397 @param realName: The user's real name | |
398 | |
399 @type server: C{str} or C{unicode} | |
400 @param server: The name of the server to which the user is connected | |
401 | |
402 @type serverInfo: C{str} or C{unicode} | |
403 @param serverInfo: A descriptive string about that server | |
404 | |
405 @type oper: C{bool} | |
406 @param oper: Indicates whether the user is an IRC operator | |
407 | |
408 @type idle: C{int} | |
409 @param idle: The number of seconds since the user last sent a message | |
410 | |
411 @type signOn: C{int} | |
412 @param signOn: A POSIX timestamp (number of seconds since the epoch) | |
413 indicating the time the user signed on | |
414 | |
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 | |
417 """ | |
418 self.sendLine(":%s %s %s %s %s %s * :%s" % ( | |
419 self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realNa
me)) | |
420 self.sendLine(":%s %s %s %s %s :%s" % ( | |
421 self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo)) | |
422 if oper: | |
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)) | |
431 | |
432 | |
433 def join(self, who, where): | |
434 """Send a join message. | |
435 | |
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!). | |
439 | |
440 @type where: C{str} or C{unicode} | |
441 @param where: The channel the user is joining. | |
442 """ | |
443 self.sendLine(":%s JOIN %s" % (who, where)) | |
444 | |
445 | |
446 def part(self, who, where, reason=None): | |
447 """Send a part message. | |
448 | |
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!). | |
452 | |
453 @type where: C{str} or C{unicode} | |
454 @param where: The channel the user is joining. | |
455 | |
456 @type reason: C{str} or C{unicode} | |
457 @param reason: A string describing the misery which caused | |
458 this poor soul to depart. | |
459 """ | |
460 if reason: | |
461 self.sendLine(":%s PART %s :%s" % (who, where, reason)) | |
462 else: | |
463 self.sendLine(":%s PART %s" % (who, where)) | |
464 | |
465 | |
466 def channelMode(self, user, channel, mode, *args): | |
467 """ | |
468 Send information about the mode of a channel. | |
469 | |
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. | |
473 | |
474 @type channel: C{str} or C{unicode} | |
475 @param channel: The channel for which this is the namelist. | |
476 | |
477 @type mode: C{str} | |
478 @param mode: A string describing this channel's modes. | |
479 | |
480 @param args: Any additional arguments required by the modes. | |
481 """ | |
482 self.sendLine(":%s %s %s %s %s %s" % ( | |
483 self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args
))) | |
484 | |
485 | |
486 class IRCClient(basic.LineReceiver): | |
487 """Internet Relay Chat client protocol, with sprinkles. | |
488 | |
489 In addition to providing an interface for an IRC client protocol, | |
490 this class also contains reasonable implementations of many common | |
491 CTCP methods. | |
492 | |
493 TODO | |
494 ==== | |
495 - Limit the length of messages sent (because the IRC server probably | |
496 does). | |
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. | |
504 | |
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\". | |
510 May be C{None} | |
511 | |
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 | |
519 | |
520 @ivar versionName: CTCP VERSION reply, client name. If C{None}, no VERSION | |
521 reply will be sent. | |
522 @ivar versionNum: CTCP VERSION reply, client version, | |
523 @ivar versionEnv: CTCP VERSION reply, environment the client is running in. | |
524 | |
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. | |
527 | |
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. | |
531 """ | |
532 | |
533 motd = "" | |
534 nickname = 'irc' | |
535 password = None | |
536 realname = None | |
537 username = None | |
538 ### Responses to various CTCP queries. | |
539 | |
540 userinfo = None | |
541 # fingerReply is a callable returning a string, or a str()able object. | |
542 fingerReply = None | |
543 versionName = None | |
544 versionNum = None | |
545 versionEnv = None | |
546 | |
547 sourceURL = "http://twistedmatrix.com/downloads/" | |
548 | |
549 dcc_destdir = '.' | |
550 dcc_sessions = None | |
551 | |
552 # If this is false, no attempt will be made to identify | |
553 # ourself to the server. | |
554 performLogin = 1 | |
555 | |
556 lineRate = None | |
557 _queue = None | |
558 _queueEmptying = None | |
559 | |
560 delimiter = '\n' # '\r\n' will also work (see dataReceived) | |
561 | |
562 __pychecker__ = 'unusednames=params,prefix,channel' | |
563 | |
564 | |
565 def _reallySendLine(self, line): | |
566 return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r') | |
567 | |
568 def sendLine(self, line): | |
569 if self.lineRate is None: | |
570 self._reallySendLine(line) | |
571 else: | |
572 self._queue.append(line) | |
573 if not self._queueEmptying: | |
574 self._sendLine() | |
575 | |
576 def _sendLine(self): | |
577 if self._queue: | |
578 self._reallySendLine(self._queue.pop(0)) | |
579 self._queueEmptying = reactor.callLater(self.lineRate, | |
580 self._sendLine) | |
581 else: | |
582 self._queueEmptying = None | |
583 | |
584 | |
585 ### Interface level client->user output methods | |
586 ### | |
587 ### You'll want to override these. | |
588 | |
589 ### Methods relating to the server itself | |
590 | |
591 def created(self, when): | |
592 """Called with creation date information about the server, usually at lo
gon. | |
593 | |
594 @type when: C{str} | |
595 @param when: A string describing when the server was created, probably. | |
596 """ | |
597 | |
598 def yourHost(self, info): | |
599 """Called with daemon information about the server, usually at logon. | |
600 | |
601 @type info: C{str} | |
602 @param when: A string describing what software the server is running, pr
obably. | |
603 """ | |
604 | |
605 def myInfo(self, servername, version, umodes, cmodes): | |
606 """Called with information about the server, usually at logon. | |
607 | |
608 @type servername: C{str} | |
609 @param servername: The hostname of this server. | |
610 | |
611 @type version: C{str} | |
612 @param version: A description of what software this server runs. | |
613 | |
614 @type umodes: C{str} | |
615 @param umodes: All the available user modes. | |
616 | |
617 @type cmodes: C{str} | |
618 @param cmodes: All the available channel modes. | |
619 """ | |
620 | |
621 def luserClient(self, info): | |
622 """Called with information about the number of connections, usually at l
ogon. | |
623 | |
624 @type info: C{str} | |
625 @param info: A description of the number of clients and servers | |
626 connected to the network, probably. | |
627 """ | |
628 | |
629 def bounce(self, info): | |
630 """Called with information about where the client should reconnect. | |
631 | |
632 @type info: C{str} | |
633 @param info: A plaintext description of the address that should be | |
634 connected to. | |
635 """ | |
636 | |
637 def isupport(self, options): | |
638 """Called with various information about what the server supports. | |
639 | |
640 @type options: C{list} of C{str} | |
641 @param options: Descriptions of features or limits of the server, possib
ly | |
642 in the form "NAME=VALUE". | |
643 """ | |
644 | |
645 def luserChannels(self, channels): | |
646 """Called with the number of channels existant on the server. | |
647 | |
648 @type channels: C{int} | |
649 """ | |
650 | |
651 def luserOp(self, ops): | |
652 """Called with the number of ops logged on to the server. | |
653 | |
654 @type ops: C{int} | |
655 """ | |
656 | |
657 def luserMe(self, info): | |
658 """Called with information about the server connected to. | |
659 | |
660 @type info: C{str} | |
661 @param info: A plaintext string describing the number of users and serve
rs | |
662 connected to this server. | |
663 """ | |
664 | |
665 ### Methods involving me directly | |
666 | |
667 def privmsg(self, user, channel, message): | |
668 """Called when I have a message from a user to me or a channel. | |
669 """ | |
670 pass | |
671 | |
672 def joined(self, channel): | |
673 """Called when I finish joining a channel. | |
674 | |
675 channel has the starting character (# or &) intact. | |
676 """ | |
677 pass | |
678 | |
679 def left(self, channel): | |
680 """Called when I have left a channel. | |
681 | |
682 channel has the starting character (# or &) intact. | |
683 """ | |
684 pass | |
685 | |
686 def noticed(self, user, channel, message): | |
687 """Called when I have a notice from a user to me or a channel. | |
688 | |
689 By default, this is equivalent to IRCClient.privmsg, but if your | |
690 client makes any automated replies, you must override this! | |
691 From the RFC:: | |
692 | |
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. | |
698 """ | |
699 self.privmsg(user, channel, message) | |
700 | |
701 def modeChanged(self, user, channel, set, modes, args): | |
702 """Called when a channel's modes are changed | |
703 | |
704 @type user: C{str} | |
705 @param user: The user and hostmask which instigated this change. | |
706 | |
707 @type channel: C{str} | |
708 @param channel: The channel for which the modes are changing. | |
709 | |
710 @type set: C{bool} or C{int} | |
711 @param set: true if the mode is being added, false if it is being | |
712 removed. | |
713 | |
714 @type modes: C{str} | |
715 @param modes: The mode or modes which are being changed. | |
716 | |
717 @type args: C{tuple} | |
718 @param args: Any additional information required for the mode | |
719 change. | |
720 """ | |
721 | |
722 def pong(self, user, secs): | |
723 """Called with the results of a CTCP PING query. | |
724 """ | |
725 pass | |
726 | |
727 def signedOn(self): | |
728 """Called after sucessfully signing on to the server. | |
729 """ | |
730 pass | |
731 | |
732 def kickedFrom(self, channel, kicker, message): | |
733 """Called when I am kicked from a channel. | |
734 """ | |
735 pass | |
736 | |
737 def nickChanged(self, nick): | |
738 """Called when my nick has been changed. | |
739 """ | |
740 self.nickname = nick | |
741 | |
742 | |
743 ### Things I observe other people doing in a channel. | |
744 | |
745 def userJoined(self, user, channel): | |
746 """Called when I see another user joining a channel. | |
747 """ | |
748 pass | |
749 | |
750 def userLeft(self, user, channel): | |
751 """Called when I see another user leaving a channel. | |
752 """ | |
753 pass | |
754 | |
755 def userQuit(self, user, quitMessage): | |
756 """Called when I see another user disconnect from the network. | |
757 """ | |
758 pass | |
759 | |
760 def userKicked(self, kickee, channel, kicker, message): | |
761 """Called when I observe someone else being kicked from a channel. | |
762 """ | |
763 pass | |
764 | |
765 def action(self, user, channel, data): | |
766 """Called when I see a user perform an ACTION on a channel. | |
767 """ | |
768 pass | |
769 | |
770 def topicUpdated(self, user, channel, newTopic): | |
771 """In channel, user changed the topic to newTopic. | |
772 | |
773 Also called when first joining a channel. | |
774 """ | |
775 pass | |
776 | |
777 def userRenamed(self, oldname, newname): | |
778 """A user changed their name from oldname to newname. | |
779 """ | |
780 pass | |
781 | |
782 ### Information from the server. | |
783 | |
784 def receivedMOTD(self, motd): | |
785 """I received a message-of-the-day banner from the server. | |
786 | |
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:: | |
789 | |
790 string.join(motd, '\\n') | |
791 | |
792 to get a nicely formatted string. | |
793 """ | |
794 pass | |
795 | |
796 ### user input commands, client->server | |
797 ### Your client will want to invoke these. | |
798 | |
799 def join(self, channel, key=None): | |
800 if channel[0] not in '&#!+': channel = '#' + channel | |
801 if key: | |
802 self.sendLine("JOIN %s %s" % (channel, key)) | |
803 else: | |
804 self.sendLine("JOIN %s" % (channel,)) | |
805 | |
806 def leave(self, channel, reason=None): | |
807 if channel[0] not in '&#!+': channel = '#' + channel | |
808 if reason: | |
809 self.sendLine("PART %s :%s" % (channel, reason)) | |
810 else: | |
811 self.sendLine("PART %s" % (channel,)) | |
812 | |
813 def kick(self, channel, user, reason=None): | |
814 if channel[0] not in '&#!+': channel = '#' + channel | |
815 if reason: | |
816 self.sendLine("KICK %s %s :%s" % (channel, user, reason)) | |
817 else: | |
818 self.sendLine("KICK %s %s" % (channel, user)) | |
819 | |
820 part = leave | |
821 | |
822 def topic(self, channel, topic=None): | |
823 """Attempt to set the topic of the given channel, or ask what it is. | |
824 | |
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. | |
828 """ | |
829 # << TOPIC #xtestx :fff | |
830 if channel[0] not in '&#!+': channel = '#' + channel | |
831 if topic != None: | |
832 self.sendLine("TOPIC %s :%s" % (channel, topic)) | |
833 else: | |
834 self.sendLine("TOPIC %s" % (channel,)) | |
835 | |
836 def mode(self, chan, set, modes, limit = None, user = None, mask = None): | |
837 """Change the modes on a user or channel.""" | |
838 if set: | |
839 line = 'MODE %s +%s' % (chan, modes) | |
840 else: | |
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) | |
848 self.sendLine(line) | |
849 | |
850 | |
851 def say(self, channel, message, length = None): | |
852 if channel[0] not in '&#!+': channel = '#' + channel | |
853 self.msg(channel, message, length) | |
854 | |
855 def msg(self, user, message, length = None): | |
856 """Send a message to a user or channel. | |
857 | |
858 @type user: C{str} | |
859 @param user: The username or channel name to which to direct the | |
860 message. | |
861 | |
862 @type message: C{str} | |
863 @param message: The text to send | |
864 | |
865 @type length: C{int} | |
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. | |
872 """ | |
873 | |
874 fmt = "PRIVMSG %s :%%s" % (user,) | |
875 | |
876 if length is None: | |
877 self.sendLine(fmt % (message,)) | |
878 else: | |
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), | |
888 lines) | |
889 | |
890 def notice(self, user, message): | |
891 self.sendLine("NOTICE %s :%s" % (user, message)) | |
892 | |
893 def away(self, message=''): | |
894 self.sendLine("AWAY :%s" % message) | |
895 | |
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)) | |
903 | |
904 def setNick(self, nickname): | |
905 self.nickname = nickname | |
906 self.sendLine("NICK %s" % nickname) | |
907 | |
908 def quit(self, message = ''): | |
909 self.sendLine("QUIT :%s" % message) | |
910 | |
911 ### user input commands, client->client | |
912 | |
913 def me(self, channel, action): | |
914 """Strike a pose. | |
915 """ | |
916 if channel[0] not in '&#!+': channel = '#' + channel | |
917 self.ctcpMakeQuery(channel, [('ACTION', action)]) | |
918 | |
919 _pings = None | |
920 _MAX_PINGRING = 12 | |
921 | |
922 def ping(self, user, text = None): | |
923 """Measure round-trip delay to another IRC client. | |
924 """ | |
925 if self._pings is None: | |
926 self._pings = {} | |
927 | |
928 if text is None: | |
929 chars = string.letters + string.digits + string.punctuation | |
930 key = ''.join([random.choice(chars) for i in range(12)]) | |
931 else: | |
932 key = str(text) | |
933 self._pings[(user, key)] = time.time() | |
934 self.ctcpMakeQuery(user, [('PING', key)]) | |
935 | |
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()] | |
939 byValue.sort() | |
940 excess = self._MAX_PINGRING - len(self._pings) | |
941 for i in xrange(excess): | |
942 del self._pings[byValue[i][1]] | |
943 | |
944 def dccSend(self, user, file): | |
945 if type(file) == types.StringType: | |
946 file = open(file, 'r') | |
947 | |
948 size = fileSize(file) | |
949 | |
950 name = getattr(file, "name", "file@%s" % (id(file),)) | |
951 | |
952 factory = DccSendFactory(file) | |
953 port = reactor.listenTCP(0, factory, 1) | |
954 | |
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.)") | |
958 | |
959 my_address = struct.pack("!I", my_address) | |
960 | |
961 args = ['SEND', name, my_address, str(port)] | |
962 | |
963 if not (size is None): | |
964 args.append(size) | |
965 | |
966 args = string.join(args, ' ') | |
967 | |
968 self.ctcpMakeQuery(user, [('DCC', args)]) | |
969 | |
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])]) | |
974 | |
975 def dccAcceptResume(self, user, fileName, port, resumePos): | |
976 """Send a DCC ACCEPT response to clients who have requested a resume. | |
977 """ | |
978 self.ctcpMakeQuery(user, [ | |
979 ('DCC', ['ACCEPT', fileName, port, resumePos])]) | |
980 | |
981 ### server->client messages | |
982 ### You might want to fiddle with these, | |
983 ### but it is safe to leave them alone. | |
984 | |
985 def irc_ERR_NICKNAMEINUSE(self, prefix, params): | |
986 self.register(self.nickname+'_') | |
987 | |
988 def irc_ERR_PASSWDMISMATCH(self, prefix, params): | |
989 raise IRCPasswordMismatch("Password Incorrect.") | |
990 | |
991 def irc_RPL_WELCOME(self, prefix, params): | |
992 self.signedOn() | |
993 | |
994 def irc_JOIN(self, prefix, params): | |
995 nick = string.split(prefix,'!')[0] | |
996 channel = params[-1] | |
997 if nick == self.nickname: | |
998 self.joined(channel) | |
999 else: | |
1000 self.userJoined(nick, channel) | |
1001 | |
1002 def irc_PART(self, prefix, params): | |
1003 nick = string.split(prefix,'!')[0] | |
1004 channel = params[0] | |
1005 if nick == self.nickname: | |
1006 self.left(channel) | |
1007 else: | |
1008 self.userLeft(nick, channel) | |
1009 | |
1010 def irc_QUIT(self, prefix, params): | |
1011 nick = string.split(prefix,'!')[0] | |
1012 self.userQuit(nick, params[0]) | |
1013 | |
1014 def irc_MODE(self, prefix, params): | |
1015 channel, rest = params[0], params[1:] | |
1016 set = rest[0][0] == '+' | |
1017 modes = rest[0][1:] | |
1018 args = rest[1:] | |
1019 self.modeChanged(prefix, channel, set, modes, tuple(args)) | |
1020 | |
1021 def irc_PING(self, prefix, params): | |
1022 self.sendLine("PONG %s" % params[-1]) | |
1023 | |
1024 def irc_PRIVMSG(self, prefix, params): | |
1025 user = prefix | |
1026 channel = params[0] | |
1027 message = params[-1] | |
1028 | |
1029 if not message: return # don't raise an exception if some idiot sends us
a blank message | |
1030 | |
1031 if message[0]==X_DELIM: | |
1032 m = ctcpExtract(message) | |
1033 if m['extended']: | |
1034 self.ctcpQuery(user, channel, m['extended']) | |
1035 | |
1036 if not m['normal']: | |
1037 return | |
1038 | |
1039 message = string.join(m['normal'], ' ') | |
1040 | |
1041 self.privmsg(user, channel, message) | |
1042 | |
1043 def irc_NOTICE(self, prefix, params): | |
1044 user = prefix | |
1045 channel = params[0] | |
1046 message = params[-1] | |
1047 | |
1048 if message[0]==X_DELIM: | |
1049 m = ctcpExtract(message) | |
1050 if m['extended']: | |
1051 self.ctcpReply(user, channel, m['extended']) | |
1052 | |
1053 if not m['normal']: | |
1054 return | |
1055 | |
1056 message = string.join(m['normal'], ' ') | |
1057 | |
1058 self.noticed(user, channel, message) | |
1059 | |
1060 def irc_NICK(self, prefix, params): | |
1061 nick = string.split(prefix,'!', 1)[0] | |
1062 if nick == self.nickname: | |
1063 self.nickChanged(params[0]) | |
1064 else: | |
1065 self.userRenamed(nick, params[0]) | |
1066 | |
1067 def irc_KICK(self, prefix, params): | |
1068 """Kicked? Who? Not me, I hope. | |
1069 """ | |
1070 kicker = string.split(prefix,'!')[0] | |
1071 channel = params[0] | |
1072 kicked = params[1] | |
1073 message = params[-1] | |
1074 if string.lower(kicked) == string.lower(self.nickname): | |
1075 # Yikes! | |
1076 self.kickedFrom(channel, kicker, message) | |
1077 else: | |
1078 self.userKicked(kicked, channel, kicker, message) | |
1079 | |
1080 def irc_TOPIC(self, prefix, params): | |
1081 """Someone in the channel set the topic. | |
1082 """ | |
1083 user = string.split(prefix, '!')[0] | |
1084 channel = params[0] | |
1085 newtopic = params[1] | |
1086 self.topicUpdated(user, channel, newtopic) | |
1087 | |
1088 def irc_RPL_TOPIC(self, prefix, params): | |
1089 """I just joined the channel, and the server is telling me the current t
opic. | |
1090 """ | |
1091 user = string.split(prefix, '!')[0] | |
1092 channel = params[1] | |
1093 newtopic = params[2] | |
1094 self.topicUpdated(user, channel, newtopic) | |
1095 | |
1096 def irc_RPL_NOTOPIC(self, prefix, params): | |
1097 user = string.split(prefix, '!')[0] | |
1098 channel = params[1] | |
1099 newtopic = "" | |
1100 self.topicUpdated(user, channel, newtopic) | |
1101 | |
1102 def irc_RPL_MOTDSTART(self, prefix, params): | |
1103 if params[-1].startswith("- "): | |
1104 params[-1] = params[-1][2:] | |
1105 self.motd = [params[-1]] | |
1106 | |
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]) | |
1111 | |
1112 def irc_RPL_ENDOFMOTD(self, prefix, params): | |
1113 self.receivedMOTD(self.motd) | |
1114 | |
1115 def irc_RPL_CREATED(self, prefix, params): | |
1116 self.created(params[1]) | |
1117 | |
1118 def irc_RPL_YOURHOST(self, prefix, params): | |
1119 self.yourHost(params[1]) | |
1120 | |
1121 def irc_RPL_MYINFO(self, prefix, params): | |
1122 info = params[1].split(None, 3) | |
1123 while len(info) < 4: | |
1124 info.append(None) | |
1125 self.myInfo(*info) | |
1126 | |
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]) | |
1131 else: | |
1132 self.bounce(params[1]) | |
1133 | |
1134 def irc_RPL_LUSERCLIENT(self, prefix, params): | |
1135 self.luserClient(params[1]) | |
1136 | |
1137 def irc_RPL_LUSEROP(self, prefix, params): | |
1138 try: | |
1139 self.luserOp(int(params[1])) | |
1140 except ValueError: | |
1141 pass | |
1142 | |
1143 def irc_RPL_LUSERCHANNELS(self, prefix, params): | |
1144 try: | |
1145 self.luserChannels(int(params[1])) | |
1146 except ValueError: | |
1147 pass | |
1148 | |
1149 def irc_RPL_LUSERME(self, prefix, params): | |
1150 self.luserMe(params[1]) | |
1151 | |
1152 def irc_unknown(self, prefix, command, params): | |
1153 pass | |
1154 | |
1155 ### Receiving a CTCP query from another party | |
1156 ### It is safe to leave these alone. | |
1157 | |
1158 def ctcpQuery(self, user, channel, messages): | |
1159 """Dispatch method for any CTCP queries received. | |
1160 """ | |
1161 for m in messages: | |
1162 method = getattr(self, "ctcpQuery_%s" % m[0], None) | |
1163 if method: | |
1164 method(user, channel, m[1]) | |
1165 else: | |
1166 self.ctcpUnknownQuery(user, channel, m[0], m[1]) | |
1167 | |
1168 def ctcpQuery_ACTION(self, user, channel, data): | |
1169 self.action(user, channel, data) | |
1170 | |
1171 def ctcpQuery_PING(self, user, channel, data): | |
1172 nick = string.split(user,"!")[0] | |
1173 self.ctcpMakeReply(nick, [("PING", data)]) | |
1174 | |
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?" | |
1178 % (user, data)) | |
1179 if not self.fingerReply: | |
1180 return | |
1181 | |
1182 if callable(self.fingerReply): | |
1183 reply = self.fingerReply() | |
1184 else: | |
1185 reply = str(self.fingerReply) | |
1186 | |
1187 nick = string.split(user,"!")[0] | |
1188 self.ctcpMakeReply(nick, [('FINGER', reply)]) | |
1189 | |
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?" | |
1193 % (user, data)) | |
1194 | |
1195 if self.versionName: | |
1196 nick = string.split(user,"!")[0] | |
1197 self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' % | |
1198 (self.versionName, | |
1199 self.versionNum, | |
1200 self.versionEnv))]) | |
1201 | |
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?" | |
1205 % (user, data)) | |
1206 if self.sourceURL: | |
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), | |
1213 ('SOURCE', None)]) | |
1214 | |
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?" | |
1218 % (user, data)) | |
1219 if self.userinfo: | |
1220 nick = string.split(user,"!")[0] | |
1221 self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)]) | |
1222 | |
1223 def ctcpQuery_CLIENTINFO(self, user, channel, data): | |
1224 """A master index of what CTCP tags this client knows. | |
1225 | |
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. | |
1229 """ | |
1230 | |
1231 nick = string.split(user,"!")[0] | |
1232 if not data: | |
1233 # XXX: prefixedMethodNames gets methods from my *class*, | |
1234 # but it's entirely possible that this *instance* has more | |
1235 # methods. | |
1236 names = reflect.prefixedMethodNames(self.__class__, | |
1237 'ctcpQuery_') | |
1238 | |
1239 self.ctcpMakeReply(nick, [('CLIENTINFO', | |
1240 string.join(names, ' '))]) | |
1241 else: | |
1242 args = string.split(data) | |
1243 method = getattr(self, 'ctcpQuery_%s' % (args[0],), None) | |
1244 if not method: | |
1245 self.ctcpMakeReply(nick, [('ERRMSG', | |
1246 "CLIENTINFO %s :" | |
1247 "Unknown query '%s'" | |
1248 % (data, args[0]))]) | |
1249 return | |
1250 doc = getattr(method, '__doc__', '') | |
1251 self.ctcpMakeReply(nick, [('CLIENTINFO', doc)]) | |
1252 | |
1253 | |
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)]) | |
1260 | |
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?" | |
1264 % (user, data)) | |
1265 nick = string.split(user,"!")[0] | |
1266 self.ctcpMakeReply(nick, | |
1267 [('TIME', ':%s' % | |
1268 time.asctime(time.localtime(time.time())))]) | |
1269 | |
1270 def ctcpQuery_DCC(self, user, channel, data): | |
1271 """Initiate a Direct Client Connection | |
1272 """ | |
1273 | |
1274 if not data: return | |
1275 dcctype = data.split(None, 1)[0].upper() | |
1276 handler = getattr(self, "dcc_" + dcctype, None) | |
1277 if handler: | |
1278 if self.dcc_sessions is None: | |
1279 self.dcc_sessions = [] | |
1280 data = data[len(dcctype)+1:] | |
1281 handler(user, channel, data) | |
1282 else: | |
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" | |
1288 % (user, dcctype)) | |
1289 | |
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) | |
1293 if len(data) < 3: | |
1294 raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,) | |
1295 | |
1296 (filename, address, port) = data[:3] | |
1297 | |
1298 address = dccParseAddress(address) | |
1299 try: | |
1300 port = int(port) | |
1301 except ValueError: | |
1302 raise IRCBadMessage, "Indecipherable port %r" % (port,) | |
1303 | |
1304 size = -1 | |
1305 if len(data) >= 4: | |
1306 try: | |
1307 size = int(data[3]) | |
1308 except ValueError: | |
1309 pass | |
1310 | |
1311 # XXX Should we bother passing this data? | |
1312 self.dccDoSend(user, address, port, filename, size, data) | |
1313 | |
1314 def dcc_ACCEPT(self, user, channel, data): | |
1315 data = text.splitQuoted(data) | |
1316 if len(data) < 3: | |
1317 raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data
,) | |
1318 (filename, port, resumePos) = data[:3] | |
1319 try: | |
1320 port = int(port) | |
1321 resumePos = int(resumePos) | |
1322 except ValueError: | |
1323 return | |
1324 | |
1325 self.dccDoAcceptResume(user, filename, port, resumePos) | |
1326 | |
1327 def dcc_RESUME(self, user, channel, data): | |
1328 data = text.splitQuoted(data) | |
1329 if len(data) < 3: | |
1330 raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data
,) | |
1331 (filename, port, resumePos) = data[:3] | |
1332 try: | |
1333 port = int(port) | |
1334 resumePos = int(resumePos) | |
1335 except ValueError: | |
1336 return | |
1337 self.dccDoResume(user, filename, port, resumePos) | |
1338 | |
1339 def dcc_CHAT(self, user, channel, data): | |
1340 data = text.splitQuoted(data) | |
1341 if len(data) < 3: | |
1342 raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,) | |
1343 | |
1344 (filename, address, port) = data[:3] | |
1345 | |
1346 address = dccParseAddress(address) | |
1347 try: | |
1348 port = int(port) | |
1349 except ValueError: | |
1350 raise IRCBadMessage, "Indecipherable port %r" % (port,) | |
1351 | |
1352 self.dccDoChat(user, channel, address, port, data) | |
1353 | |
1354 ### The dccDo methods are the slightly higher-level siblings of | |
1355 ### common dcc_ methods; the arguments have been parsed for them. | |
1356 | |
1357 def dccDoSend(self, user, address, port, fileName, size, data): | |
1358 """Called when I receive a DCC SEND offer from a client. | |
1359 | |
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) | |
1366 pass | |
1367 | |
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).""" | |
1372 pass | |
1373 | |
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.""" | |
1377 pass | |
1378 | |
1379 def dccDoChat(self, user, channel, address, port, data): | |
1380 pass | |
1381 #factory = DccChatFactory(self, queryData=(user, channel, data)) | |
1382 #reactor.connectTCP(address, port, factory) | |
1383 #self.dcc_sessions.append(factory) | |
1384 | |
1385 #def ctcpQuery_SED(self, user, data): | |
1386 # """Simple Encryption Doodoo | |
1387 # | |
1388 # Feel free to implement this, but no specification is available. | |
1389 # """ | |
1390 # raise NotImplementedError | |
1391 | |
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))]) | |
1397 | |
1398 log.msg("Unknown CTCP query from %s: %s %s\n" | |
1399 % (user, tag, data)) | |
1400 | |
1401 def ctcpMakeReply(self, user, messages): | |
1402 """Send one or more X{extended messages} as a CTCP reply. | |
1403 | |
1404 @type messages: a list of extended messages. An extended | |
1405 message is a (tag, data) tuple, where 'data' may be C{None}. | |
1406 """ | |
1407 self.notice(user, ctcpStringify(messages)) | |
1408 | |
1409 ### client CTCP query commands | |
1410 | |
1411 def ctcpMakeQuery(self, user, messages): | |
1412 """Send one or more X{extended messages} as a CTCP query. | |
1413 | |
1414 @type messages: a list of extended messages. An extended | |
1415 message is a (tag, data) tuple, where 'data' may be C{None}. | |
1416 """ | |
1417 self.msg(user, ctcpStringify(messages)) | |
1418 | |
1419 ### Receiving a response to a CTCP query (presumably to one we made) | |
1420 ### You may want to add methods here, or override UnknownReply. | |
1421 | |
1422 def ctcpReply(self, user, channel, messages): | |
1423 """Dispatch method for any CTCP replies received. | |
1424 """ | |
1425 for m in messages: | |
1426 method = getattr(self, "ctcpReply_%s" % m[0], None) | |
1427 if method: | |
1428 method(user, channel, m[1]) | |
1429 else: | |
1430 self.ctcpUnknownReply(user, channel, m[0], m[1]) | |
1431 | |
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) | |
1437 | |
1438 t0 = self._pings[(nick, data)] | |
1439 self.pong(user, time.time() - t0) | |
1440 | |
1441 def ctcpUnknownReply(self, user, channel, tag, data): | |
1442 """Called when a fitting ctcpReply_ method is not found. | |
1443 | |
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. | |
1447 """ | |
1448 log.msg("Unknown CTCP reply from %s: %s %s\n" | |
1449 % (user, tag, data)) | |
1450 | |
1451 ### Error handlers | |
1452 ### You may override these with something more appropriate to your UI. | |
1453 | |
1454 def badMessage(self, line, excType, excValue, tb): | |
1455 """When I get a message that's so broken I can't use it. | |
1456 """ | |
1457 log.msg(line) | |
1458 log.msg(string.join(traceback.format_exception(excType, | |
1459 excValue, | |
1460 tb),'')) | |
1461 | |
1462 def quirkyMessage(self, s): | |
1463 """This is called when I receive a message which is peculiar, | |
1464 but not wholly indecipherable. | |
1465 """ | |
1466 log.msg(s + '\n') | |
1467 | |
1468 ### Protocool methods | |
1469 | |
1470 def connectionMade(self): | |
1471 self._queue = [] | |
1472 if self.performLogin: | |
1473 self.register(self.nickname) | |
1474 | |
1475 def dataReceived(self, data): | |
1476 basic.LineReceiver.dataReceived(self, data.replace('\r', '')) | |
1477 | |
1478 def lineReceived(self, line): | |
1479 line = lowDequote(line) | |
1480 try: | |
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()) | |
1487 | |
1488 | |
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. | |
1492 """ | |
1493 method = getattr(self, "irc_%s" % command, None) | |
1494 try: | |
1495 if method is not None: | |
1496 method(prefix, params) | |
1497 else: | |
1498 self.irc_unknown(prefix, command, params) | |
1499 except: | |
1500 log.deferr() | |
1501 | |
1502 | |
1503 def __getstate__(self): | |
1504 dct = self.__dict__.copy() | |
1505 dct['dcc_sessions'] = None | |
1506 dct['_pings'] = None | |
1507 return dct | |
1508 | |
1509 | |
1510 def dccParseAddress(address): | |
1511 if '.' in address: | |
1512 pass | |
1513 else: | |
1514 try: | |
1515 address = long(address) | |
1516 except ValueError: | |
1517 raise IRCBadMessage,\ | |
1518 "Indecipherable address %r" % (address,) | |
1519 else: | |
1520 address = ( | |
1521 (address >> 24) & 0xFF, | |
1522 (address >> 16) & 0xFF, | |
1523 (address >> 8) & 0xFF, | |
1524 address & 0xFF, | |
1525 ) | |
1526 address = '.'.join(map(str,address)) | |
1527 return address | |
1528 | |
1529 | |
1530 class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral): | |
1531 """Bare protocol to receive a Direct Client Connection SEND stream. | |
1532 | |
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. | |
1535 """ | |
1536 | |
1537 bytesReceived = 0 | |
1538 | |
1539 def __init__(self, resumeOffset=0): | |
1540 self.bytesReceived = resumeOffset | |
1541 self.resume = (resumeOffset != 0) | |
1542 | |
1543 def dataReceived(self, data): | |
1544 """Called when data is received. | |
1545 | |
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. | |
1549 """ | |
1550 self.bytesReceived = self.bytesReceived + len(data) | |
1551 self.transport.write(struct.pack('!i', self.bytesReceived)) | |
1552 | |
1553 | |
1554 class DccSendProtocol(protocol.Protocol, styles.Ephemeral): | |
1555 """Protocol for an outgoing Direct Client Connection SEND. | |
1556 """ | |
1557 | |
1558 blocksize = 1024 | |
1559 file = None | |
1560 bytesSent = 0 | |
1561 completed = 0 | |
1562 connected = 0 | |
1563 | |
1564 def __init__(self, file): | |
1565 if type(file) is types.StringType: | |
1566 self.file = open(file, 'r') | |
1567 | |
1568 def connectionMade(self): | |
1569 self.connected = 1 | |
1570 self.sendBlock() | |
1571 | |
1572 def dataReceived(self, data): | |
1573 # XXX: Do we need to check to see if len(data) != fmtsize? | |
1574 | |
1575 bytesShesGot = struct.unpack("!I", data) | |
1576 if bytesShesGot < self.bytesSent: | |
1577 # Wait for her. | |
1578 # XXX? Add some checks to see if we've stalled out? | |
1579 return | |
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." | |
1584 # % (self.file, | |
1585 # bytesShesGot, self.bytesSent)) | |
1586 self.transport.loseConnection() | |
1587 return | |
1588 | |
1589 self.sendBlock() | |
1590 | |
1591 def sendBlock(self): | |
1592 block = self.file.read(self.blocksize) | |
1593 if block: | |
1594 self.transport.write(block) | |
1595 self.bytesSent = self.bytesSent + len(block) | |
1596 else: | |
1597 # Nothing more to send, transfer complete. | |
1598 self.transport.loseConnection() | |
1599 self.completed = 1 | |
1600 | |
1601 def connectionLost(self, reason): | |
1602 self.connected = 0 | |
1603 if hasattr(self.file, "close"): | |
1604 self.file.close() | |
1605 | |
1606 | |
1607 class DccSendFactory(protocol.Factory): | |
1608 protocol = DccSendProtocol | |
1609 def __init__(self, file): | |
1610 self.file = file | |
1611 | |
1612 def buildProtocol(self, connection): | |
1613 p = self.protocol(self.file) | |
1614 p.factory = self | |
1615 return p | |
1616 | |
1617 | |
1618 def fileSize(file): | |
1619 """I'll try my damndest to determine the size of this file object. | |
1620 """ | |
1621 size = None | |
1622 if hasattr(file, "fileno"): | |
1623 fileno = file.fileno() | |
1624 try: | |
1625 stat_ = os.fstat(fileno) | |
1626 size = stat_[stat.ST_SIZE] | |
1627 except: | |
1628 pass | |
1629 else: | |
1630 return size | |
1631 | |
1632 if hasattr(file, "name") and path.exists(file.name): | |
1633 try: | |
1634 size = path.getsize(file.name) | |
1635 except: | |
1636 pass | |
1637 else: | |
1638 return size | |
1639 | |
1640 if hasattr(file, "seek") and hasattr(file, "tell"): | |
1641 try: | |
1642 try: | |
1643 file.seek(0, 2) | |
1644 size = file.tell() | |
1645 finally: | |
1646 file.seek(0, 0) | |
1647 except: | |
1648 pass | |
1649 else: | |
1650 return size | |
1651 | |
1652 return size | |
1653 | |
1654 class DccChat(basic.LineReceiver, styles.Ephemeral): | |
1655 """Direct Client Connection protocol type CHAT. | |
1656 | |
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. | |
1661 | |
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. | |
1666 """ | |
1667 | |
1668 queryData = None | |
1669 delimiter = CR + NL | |
1670 client = None | |
1671 remoteParty = None | |
1672 buffer = "" | |
1673 | |
1674 def __init__(self, client, queryData=None): | |
1675 """Initialize a new DCC CHAT session. | |
1676 | |
1677 queryData is a 3-tuple of | |
1678 (fromUser, targetUserOrChannel, data) | |
1679 as received by the CTCP query. | |
1680 | |
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.) | |
1684 """ | |
1685 self.client = client | |
1686 if queryData: | |
1687 self.queryData = queryData | |
1688 self.remoteParty = self.queryData[0] | |
1689 | |
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 | |
1694 # buffer | |
1695 self.buffer = lines.pop() | |
1696 | |
1697 for line in lines: | |
1698 if line[-1] == CR: | |
1699 line = line[:-1] | |
1700 self.lineReceived(line) | |
1701 | |
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) | |
1706 | |
1707 | |
1708 class DccChatFactory(protocol.ClientFactory): | |
1709 protocol = DccChat | |
1710 noisy = 0 | |
1711 def __init__(self, client, queryData): | |
1712 self.client = client | |
1713 self.queryData = queryData | |
1714 | |
1715 def buildProtocol(self, addr): | |
1716 p = self.protocol(client=self.client, queryData=self.queryData) | |
1717 p.factory = self | |
1718 | |
1719 def clientConnectionFailed(self, unused_connector, unused_reason): | |
1720 self.client.dcc_sessions.remove(self) | |
1721 | |
1722 def clientConnectionLost(self, unused_connector, unused_reason): | |
1723 self.client.dcc_sessions.remove(self) | |
1724 | |
1725 | |
1726 def dccDescribe(data): | |
1727 """Given the data chunk from a DCC query, return a descriptive string. | |
1728 """ | |
1729 | |
1730 orig_data = data | |
1731 data = string.split(data) | |
1732 if len(data) < 4: | |
1733 return orig_data | |
1734 | |
1735 (dcctype, arg, address, port) = data[:4] | |
1736 | |
1737 if '.' in address: | |
1738 pass | |
1739 else: | |
1740 try: | |
1741 address = long(address) | |
1742 except ValueError: | |
1743 pass | |
1744 else: | |
1745 address = ( | |
1746 (address >> 24) & 0xFF, | |
1747 (address >> 16) & 0xFF, | |
1748 (address >> 8) & 0xFF, | |
1749 address & 0xFF, | |
1750 ) | |
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)), ".") | |
1754 | |
1755 if dcctype == 'SEND': | |
1756 filename = arg | |
1757 | |
1758 size_txt = '' | |
1759 if len(data) >= 5: | |
1760 try: | |
1761 size = int(data[4]) | |
1762 size_txt = ' of size %d bytes' % (size,) | |
1763 except ValueError: | |
1764 pass | |
1765 | |
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" | |
1770 % (address, port)) | |
1771 else: | |
1772 dcc_text = orig_data | |
1773 | |
1774 return dcc_text | |
1775 | |
1776 | |
1777 class DccFileReceive(DccFileReceiveBasic): | |
1778 """Higher-level coverage for getting a file from DCC SEND. | |
1779 | |
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. | |
1784 | |
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 writte
n. | |
1789 """ | |
1790 | |
1791 filename = 'dcc' | |
1792 fileSize = -1 | |
1793 destDir = '.' | |
1794 overwrite = 0 | |
1795 fromUser = None | |
1796 queryData = None | |
1797 | |
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 | |
1804 | |
1805 if queryData: | |
1806 self.queryData = queryData | |
1807 self.fromUser = self.queryData[0] | |
1808 | |
1809 def set_directory(self, directory): | |
1810 """Set the directory where the downloaded file will be placed. | |
1811 | |
1812 May raise OSError if the supplied directory path is not suitable. | |
1813 """ | |
1814 if not path.exists(directory): | |
1815 raise OSError(errno.ENOENT, "You see no directory there.", | |
1816 directory) | |
1817 if not path.isdir(directory): | |
1818 raise OSError(errno.ENOTDIR, "You cannot put a file into " | |
1819 "something which is not a directory.", | |
1820 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.", | |
1824 directory) | |
1825 self.destDir = directory | |
1826 | |
1827 def set_filename(self, filename): | |
1828 """Change the name of the file being transferred. | |
1829 | |
1830 This replaces the file name provided by the sender. | |
1831 """ | |
1832 self.filename = filename | |
1833 | |
1834 def set_overwrite(self, boolean): | |
1835 """May I overwrite existing files? | |
1836 """ | |
1837 self.overwrite = boolean | |
1838 | |
1839 | |
1840 # Protocol-level methods. | |
1841 | |
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') | |
1853 else: | |
1854 raise OSError(errno.EEXIST, | |
1855 "There's a file in the way. " | |
1856 "Perhaps that's why you cannot open it.", | |
1857 dst) | |
1858 | |
1859 def dataReceived(self, data): | |
1860 self.file.write(data) | |
1861 DccFileReceiveBasic.dataReceived(self, data) | |
1862 | |
1863 # XXX: update a progress indicator here? | |
1864 | |
1865 def connectionLost(self, reason): | |
1866 """When the connection is lost, I close the file. | |
1867 """ | |
1868 self.connected = 0 | |
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: | |
1874 pass # Hooray! | |
1875 elif self.bytesReceived < self.fileSize: | |
1876 logmsg = ("%s (Warning: %d bytes short)" | |
1877 % (logmsg, self.fileSize - self.bytesReceived)) | |
1878 else: | |
1879 logmsg = ("%s (file larger than expected)" | |
1880 % (logmsg,)) | |
1881 else: | |
1882 logmsg = ("%s %d bytes received" | |
1883 % (logmsg, self.bytesReceived)) | |
1884 | |
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() | |
1888 | |
1889 # self.transport.log(logmsg) | |
1890 | |
1891 def __str__(self): | |
1892 if not self.connected: | |
1893 return "<Unconnected DccFileReceive object at %x>" % (id(self),) | |
1894 from_ = self.transport.getPeer() | |
1895 if self.fromUser: | |
1896 from_ = "%s (%s)" % (self.fromUser, from_) | |
1897 | |
1898 s = ("DCC transfer of '%s' from %s" % (self.filename, from_)) | |
1899 return s | |
1900 | |
1901 def __repr__(self): | |
1902 s = ("<%s at %x: GET %s>" | |
1903 % (self.__class__, id(self), self.filename)) | |
1904 return s | |
1905 | |
1906 | |
1907 # CTCP constants and helper functions | |
1908 | |
1909 X_DELIM = chr(001) | |
1910 | |
1911 def ctcpExtract(message): | |
1912 """Extract CTCP data from a string. | |
1913 | |
1914 Returns a dictionary with two items: | |
1915 | |
1916 - C{'extended'}: a list of CTCP (tag, data) tuples | |
1917 - C{'normal'}: a list of strings which were not inside a CTCP delimeter | |
1918 """ | |
1919 | |
1920 extended_messages = [] | |
1921 normal_messages = [] | |
1922 retval = {'extended': extended_messages, | |
1923 'normal': normal_messages } | |
1924 | |
1925 messages = string.split(message, X_DELIM) | |
1926 odd = 0 | |
1927 | |
1928 # X1 extended data X2 nomal data X3 extended data X4 normal... | |
1929 while messages: | |
1930 if odd: | |
1931 extended_messages.append(messages.pop(0)) | |
1932 else: | |
1933 normal_messages.append(messages.pop(0)) | |
1934 odd = not odd | |
1935 | |
1936 extended_messages[:] = filter(None, extended_messages) | |
1937 normal_messages[:] = filter(None, normal_messages) | |
1938 | |
1939 extended_messages[:] = map(ctcpDequote, extended_messages) | |
1940 for i in xrange(len(extended_messages)): | |
1941 m = string.split(extended_messages[i], SPC, 1) | |
1942 tag = m[0] | |
1943 if len(m) > 1: | |
1944 data = m[1] | |
1945 else: | |
1946 data = None | |
1947 | |
1948 extended_messages[i] = (tag, data) | |
1949 | |
1950 return retval | |
1951 | |
1952 # CTCP escaping | |
1953 | |
1954 M_QUOTE= chr(020) | |
1955 | |
1956 mQuoteTable = { | |
1957 NUL: M_QUOTE + '0', | |
1958 NL: M_QUOTE + 'n', | |
1959 CR: M_QUOTE + 'r', | |
1960 M_QUOTE: M_QUOTE + M_QUOTE | |
1961 } | |
1962 | |
1963 mDequoteTable = {} | |
1964 for k, v in mQuoteTable.items(): | |
1965 mDequoteTable[v[-1]] = k | |
1966 del k, v | |
1967 | |
1968 mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL) | |
1969 | |
1970 def lowQuote(s): | |
1971 for c in (M_QUOTE, NUL, NL, CR): | |
1972 s = string.replace(s, c, mQuoteTable[c]) | |
1973 return s | |
1974 | |
1975 def lowDequote(s): | |
1976 def sub(matchobj, mDequoteTable=mDequoteTable): | |
1977 s = matchobj.group()[1] | |
1978 try: | |
1979 s = mDequoteTable[s] | |
1980 except KeyError: | |
1981 s = s | |
1982 return s | |
1983 | |
1984 return mEscape_re.sub(sub, s) | |
1985 | |
1986 X_QUOTE = '\\' | |
1987 | |
1988 xQuoteTable = { | |
1989 X_DELIM: X_QUOTE + 'a', | |
1990 X_QUOTE: X_QUOTE + X_QUOTE | |
1991 } | |
1992 | |
1993 xDequoteTable = {} | |
1994 | |
1995 for k, v in xQuoteTable.items(): | |
1996 xDequoteTable[v[-1]] = k | |
1997 | |
1998 xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL) | |
1999 | |
2000 def ctcpQuote(s): | |
2001 for c in (X_QUOTE, X_DELIM): | |
2002 s = string.replace(s, c, xQuoteTable[c]) | |
2003 return s | |
2004 | |
2005 def ctcpDequote(s): | |
2006 def sub(matchobj, xDequoteTable=xDequoteTable): | |
2007 s = matchobj.group()[1] | |
2008 try: | |
2009 s = xDequoteTable[s] | |
2010 except KeyError: | |
2011 s = s | |
2012 return s | |
2013 | |
2014 return xEscape_re.sub(sub, s) | |
2015 | |
2016 def ctcpStringify(messages): | |
2017 """ | |
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. | |
2021 | |
2022 @returns: String | |
2023 """ | |
2024 coded_messages = [] | |
2025 for (tag, data) in messages: | |
2026 if data: | |
2027 if not isinstance(data, types.StringType): | |
2028 try: | |
2029 # data as list-of-strings | |
2030 data = " ".join(map(str, data)) | |
2031 except TypeError: | |
2032 # No? Then use it's %s representation. | |
2033 pass | |
2034 m = "%s %s" % (tag, data) | |
2035 else: | |
2036 m = str(tag) | |
2037 m = ctcpQuote(m) | |
2038 m = "%s%s%s" % (X_DELIM, m, X_DELIM) | |
2039 coded_messages.append(m) | |
2040 | |
2041 line = string.join(coded_messages, '') | |
2042 return line | |
2043 | |
2044 | |
2045 # Constants (from RFC 2812) | |
2046 RPL_WELCOME = '001' | |
2047 RPL_YOURHOST = '002' | |
2048 RPL_CREATED = '003' | |
2049 RPL_MYINFO = '004' | |
2050 RPL_BOUNCE = '005' | |
2051 RPL_USERHOST = '302' | |
2052 RPL_ISON = '303' | |
2053 RPL_AWAY = '301' | |
2054 RPL_UNAWAY = '305' | |
2055 RPL_NOWAWAY = '306' | |
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' | |
2065 RPL_LIST = '322' | |
2066 RPL_LISTEND = '323' | |
2067 RPL_UNIQOPIS = '325' | |
2068 RPL_CHANNELMODEIS = '324' | |
2069 RPL_NOTOPIC = '331' | |
2070 RPL_TOPIC = '332' | |
2071 RPL_INVITING = '341' | |
2072 RPL_SUMMONING = '342' | |
2073 RPL_INVITELIST = '346' | |
2074 RPL_ENDOFINVITELIST = '347' | |
2075 RPL_EXCEPTLIST = '348' | |
2076 RPL_ENDOFEXCEPTLIST = '349' | |
2077 RPL_VERSION = '351' | |
2078 RPL_WHOREPLY = '352' | |
2079 RPL_ENDOFWHO = '315' | |
2080 RPL_NAMREPLY = '353' | |
2081 RPL_ENDOFNAMES = '366' | |
2082 RPL_LINKS = '364' | |
2083 RPL_ENDOFLINKS = '365' | |
2084 RPL_BANLIST = '367' | |
2085 RPL_ENDOFBANLIST = '368' | |
2086 RPL_INFO = '371' | |
2087 RPL_ENDOFINFO = '374' | |
2088 RPL_MOTDSTART = '375' | |
2089 RPL_MOTD = '372' | |
2090 RPL_ENDOFMOTD = '376' | |
2091 RPL_YOUREOPER = '381' | |
2092 RPL_REHASHING = '382' | |
2093 RPL_YOURESERVICE = '383' | |
2094 RPL_TIME = '391' | |
2095 RPL_USERSSTART = '392' | |
2096 RPL_USERS = '393' | |
2097 RPL_ENDOFUSERS = '394' | |
2098 RPL_NOUSERS = '395' | |
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' | |
2117 RPL_UMODEIS = '221' | |
2118 RPL_SERVLIST = '234' | |
2119 RPL_SERVLISTEND = '235' | |
2120 RPL_LUSERCLIENT = '251' | |
2121 RPL_LUSEROP = '252' | |
2122 RPL_LUSERUNKNOWN = '253' | |
2123 RPL_LUSERCHANNELS = '254' | |
2124 RPL_LUSERME = '255' | |
2125 RPL_ADMINME = '256' | |
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' | |
2143 ERR_BADMASK = '415' | |
2144 ERR_UNKNOWNCOMMAND = '421' | |
2145 ERR_NOMOTD = '422' | |
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' | |
2156 ERR_NOLOGIN = '444' | |
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' | |
2166 ERR_KEYSET = '467' | |
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' | |
2184 | |
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', | |
2193 "RPL_ISON": '303', | |
2194 "RPL_AWAY": '301', | |
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', | |
2206 "RPL_LIST": '322', | |
2207 "RPL_LISTEND": '323', | |
2208 "RPL_UNIQOPIS": '325', | |
2209 "RPL_CHANNELMODEIS": '324', | |
2210 "RPL_NOTOPIC": '331', | |
2211 "RPL_TOPIC": '332', | |
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', | |
2223 "RPL_LINKS": '364', | |
2224 "RPL_ENDOFLINKS": '365', | |
2225 "RPL_BANLIST": '367', | |
2226 "RPL_ENDOFBANLIST": '368', | |
2227 "RPL_INFO": '371', | |
2228 "RPL_ENDOFINFO": '374', | |
2229 "RPL_MOTDSTART": '375', | |
2230 "RPL_MOTD": '372', | |
2231 "RPL_ENDOFMOTD": '376', | |
2232 "RPL_YOUREOPER": '381', | |
2233 "RPL_REHASHING": '382', | |
2234 "RPL_YOURESERVICE": '383', | |
2235 "RPL_TIME": '391', | |
2236 "RPL_USERSSTART": '392', | |
2237 "RPL_USERS": '393', | |
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', | |
2325 } | |
2326 | |
2327 numeric_to_symbolic = {} | |
2328 for k, v in symbolic_to_numeric.items(): | |
2329 numeric_to_symbolic[v] = k | |
OLD | NEW |