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