| 1 | # -*- test-case-name: twisted.words.test.test_irc -*- |
|---|
| 2 | # Copyright (c) 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 | @var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC |
|---|
| 24 | 2812 section 2.3. |
|---|
| 25 | |
|---|
| 26 | @author: Kevin Turner |
|---|
| 27 | |
|---|
| 28 | @see: RFC 1459: Internet Relay Chat Protocol |
|---|
| 29 | @see: RFC 2812: Internet Relay Chat: Client Protocol |
|---|
| 30 | @see: U{The Client-To-Client-Protocol |
|---|
| 31 | <http://www.irchelp.org/irchelp/rfc/ctcpspec.html>} |
|---|
| 32 | """ |
|---|
| 33 | |
|---|
| 34 | import errno, os, random, re, stat, struct, sys, time, types, traceback |
|---|
| 35 | import string, socket |
|---|
| 36 | import warnings |
|---|
| 37 | import textwrap |
|---|
| 38 | from os import path |
|---|
| 39 | |
|---|
| 40 | from twisted.internet import reactor, protocol, task |
|---|
| 41 | from twisted.persisted import styles |
|---|
| 42 | from twisted.protocols import basic |
|---|
| 43 | from twisted.python import log, reflect, text |
|---|
| 44 | from twisted.python.compat import set |
|---|
| 45 | |
|---|
| 46 | NUL = chr(0) |
|---|
| 47 | CR = chr(015) |
|---|
| 48 | NL = chr(012) |
|---|
| 49 | LF = NL |
|---|
| 50 | SPC = chr(040) |
|---|
| 51 | |
|---|
| 52 | # This includes the CRLF terminator characters. |
|---|
| 53 | MAX_COMMAND_LENGTH = 512 |
|---|
| 54 | |
|---|
| 55 | CHANNEL_PREFIXES = '&#!+' |
|---|
| 56 | |
|---|
| 57 | class IRCBadMessage(Exception): |
|---|
| 58 | pass |
|---|
| 59 | |
|---|
| 60 | class IRCPasswordMismatch(Exception): |
|---|
| 61 | pass |
|---|
| 62 | |
|---|
| 63 | |
|---|
| 64 | |
|---|
| 65 | class IRCBadModes(ValueError): |
|---|
| 66 | """ |
|---|
| 67 | A malformed mode was encountered while attempting to parse a mode string. |
|---|
| 68 | """ |
|---|
| 69 | |
|---|
| 70 | |
|---|
| 71 | |
|---|
| 72 | def parsemsg(s): |
|---|
| 73 | """Breaks a message from an IRC server into its prefix, command, and arguments. |
|---|
| 74 | """ |
|---|
| 75 | prefix = '' |
|---|
| 76 | trailing = [] |
|---|
| 77 | if not s: |
|---|
| 78 | raise IRCBadMessage("Empty line.") |
|---|
| 79 | if s[0] == ':': |
|---|
| 80 | prefix, s = s[1:].split(' ', 1) |
|---|
| 81 | if s.find(' :') != -1: |
|---|
| 82 | s, trailing = s.split(' :', 1) |
|---|
| 83 | args = s.split() |
|---|
| 84 | args.append(trailing) |
|---|
| 85 | else: |
|---|
| 86 | args = s.split() |
|---|
| 87 | command = args.pop(0) |
|---|
| 88 | return prefix, command, args |
|---|
| 89 | |
|---|
| 90 | |
|---|
| 91 | |
|---|
| 92 | def split(str, length=80): |
|---|
| 93 | """ |
|---|
| 94 | Split a string into multiple lines. |
|---|
| 95 | |
|---|
| 96 | Whitespace near C{str[length]} will be preferred as a breaking point. |
|---|
| 97 | C{"\\n"} will also be used as a breaking point. |
|---|
| 98 | |
|---|
| 99 | @param str: The string to split. |
|---|
| 100 | @type str: C{str} |
|---|
| 101 | |
|---|
| 102 | @param length: The maximum length which will be allowed for any string in |
|---|
| 103 | the result. |
|---|
| 104 | @type length: C{int} |
|---|
| 105 | |
|---|
| 106 | @return: C{list} of C{str} |
|---|
| 107 | """ |
|---|
| 108 | return [chunk |
|---|
| 109 | for line in str.split('\n') |
|---|
| 110 | for chunk in textwrap.wrap(line, length)] |
|---|
| 111 | |
|---|
| 112 | |
|---|
| 113 | def _intOrDefault(value, default=None): |
|---|
| 114 | """ |
|---|
| 115 | Convert a value to an integer if possible. |
|---|
| 116 | |
|---|
| 117 | @rtype: C{int} or type of L{default} |
|---|
| 118 | @return: An integer when C{value} can be converted to an integer, |
|---|
| 119 | otherwise return C{default} |
|---|
| 120 | """ |
|---|
| 121 | if value: |
|---|
| 122 | try: |
|---|
| 123 | return int(value) |
|---|
| 124 | except (TypeError, ValueError): |
|---|
| 125 | pass |
|---|
| 126 | return default |
|---|
| 127 | |
|---|
| 128 | |
|---|
| 129 | |
|---|
| 130 | class UnhandledCommand(RuntimeError): |
|---|
| 131 | """ |
|---|
| 132 | A command dispatcher could not locate an appropriate command handler. |
|---|
| 133 | """ |
|---|
| 134 | |
|---|
| 135 | |
|---|
| 136 | |
|---|
| 137 | class _CommandDispatcherMixin(object): |
|---|
| 138 | """ |
|---|
| 139 | Dispatch commands to handlers based on their name. |
|---|
| 140 | |
|---|
| 141 | Command handler names should be of the form C{prefix_commandName}, |
|---|
| 142 | where C{prefix} is the value specified by L{prefix}, and must |
|---|
| 143 | accept the parameters as given to L{dispatch}. |
|---|
| 144 | |
|---|
| 145 | Attempting to mix this in more than once for a single class will cause |
|---|
| 146 | strange behaviour, due to L{prefix} being overwritten. |
|---|
| 147 | |
|---|
| 148 | @type prefix: C{str} |
|---|
| 149 | @ivar prefix: Command handler prefix, used to locate handler attributes |
|---|
| 150 | """ |
|---|
| 151 | prefix = None |
|---|
| 152 | |
|---|
| 153 | def dispatch(self, commandName, *args): |
|---|
| 154 | """ |
|---|
| 155 | Perform actual command dispatch. |
|---|
| 156 | """ |
|---|
| 157 | def _getMethodName(command): |
|---|
| 158 | return '%s_%s' % (self.prefix, command) |
|---|
| 159 | |
|---|
| 160 | def _getMethod(name): |
|---|
| 161 | return getattr(self, _getMethodName(name), None) |
|---|
| 162 | |
|---|
| 163 | method = _getMethod(commandName) |
|---|
| 164 | if method is not None: |
|---|
| 165 | return method(*args) |
|---|
| 166 | |
|---|
| 167 | method = _getMethod('unknown') |
|---|
| 168 | if method is None: |
|---|
| 169 | raise UnhandledCommand("No handler for %r could be found" % (_getMethodName(commandName),)) |
|---|
| 170 | return method(commandName, *args) |
|---|
| 171 | |
|---|
| 172 | |
|---|
| 173 | |
|---|
| 174 | |
|---|
| 175 | |
|---|
| 176 | def parseModes(modes, params, paramModes=('', '')): |
|---|
| 177 | """ |
|---|
| 178 | Parse an IRC mode string. |
|---|
| 179 | |
|---|
| 180 | The mode string is parsed into two lists of mode changes (added and |
|---|
| 181 | removed), with each mode change represented as C{(mode, param)} where mode |
|---|
| 182 | is the mode character, and param is the parameter passed for that mode, or |
|---|
| 183 | C{None} if no parameter is required. |
|---|
| 184 | |
|---|
| 185 | @type modes: C{str} |
|---|
| 186 | @param modes: Modes string to parse. |
|---|
| 187 | |
|---|
| 188 | @type params: C{list} |
|---|
| 189 | @param params: Parameters specified along with L{modes}. |
|---|
| 190 | |
|---|
| 191 | @type paramModes: C{(str, str)} |
|---|
| 192 | @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take |
|---|
| 193 | parameters when added or removed. |
|---|
| 194 | |
|---|
| 195 | @returns: Two lists of mode changes, one for modes added and the other for |
|---|
| 196 | modes removed respectively, mode changes in each list are represented as |
|---|
| 197 | C{(mode, param)}. |
|---|
| 198 | """ |
|---|
| 199 | if len(modes) == 0: |
|---|
| 200 | raise IRCBadModes('Empty mode string') |
|---|
| 201 | |
|---|
| 202 | if modes[0] not in '+-': |
|---|
| 203 | raise IRCBadModes('Malformed modes string: %r' % (modes,)) |
|---|
| 204 | |
|---|
| 205 | changes = ([], []) |
|---|
| 206 | |
|---|
| 207 | direction = None |
|---|
| 208 | count = -1 |
|---|
| 209 | for ch in modes: |
|---|
| 210 | if ch in '+-': |
|---|
| 211 | if count == 0: |
|---|
| 212 | raise IRCBadModes('Empty mode sequence: %r' % (modes,)) |
|---|
| 213 | direction = '+-'.index(ch) |
|---|
| 214 | count = 0 |
|---|
| 215 | else: |
|---|
| 216 | param = None |
|---|
| 217 | if ch in paramModes[direction]: |
|---|
| 218 | try: |
|---|
| 219 | param = params.pop(0) |
|---|
| 220 | except IndexError: |
|---|
| 221 | raise IRCBadModes('Not enough parameters: %r' % (ch,)) |
|---|
| 222 | changes[direction].append((ch, param)) |
|---|
| 223 | count += 1 |
|---|
| 224 | |
|---|
| 225 | if len(params) > 0: |
|---|
| 226 | raise IRCBadModes('Too many parameters: %r %r' % (modes, params)) |
|---|
| 227 | |
|---|
| 228 | if count == 0: |
|---|
| 229 | raise IRCBadModes('Empty mode sequence: %r' % (modes,)) |
|---|
| 230 | |
|---|
| 231 | return changes |
|---|
| 232 | |
|---|
| 233 | |
|---|
| 234 | |
|---|
| 235 | class IRC(protocol.Protocol): |
|---|
| 236 | """ |
|---|
| 237 | Internet Relay Chat server protocol. |
|---|
| 238 | """ |
|---|
| 239 | |
|---|
| 240 | buffer = "" |
|---|
| 241 | hostname = None |
|---|
| 242 | |
|---|
| 243 | encoding = None |
|---|
| 244 | |
|---|
| 245 | def connectionMade(self): |
|---|
| 246 | self.channels = [] |
|---|
| 247 | if self.hostname is None: |
|---|
| 248 | self.hostname = socket.getfqdn() |
|---|
| 249 | |
|---|
| 250 | |
|---|
| 251 | def sendLine(self, line): |
|---|
| 252 | if self.encoding is not None: |
|---|
| 253 | if isinstance(line, unicode): |
|---|
| 254 | line = line.encode(self.encoding) |
|---|
| 255 | self.transport.write("%s%s%s" % (line, CR, LF)) |
|---|
| 256 | |
|---|
| 257 | |
|---|
| 258 | def sendMessage(self, command, *parameter_list, **prefix): |
|---|
| 259 | """ |
|---|
| 260 | Send a line formatted as an IRC message. |
|---|
| 261 | |
|---|
| 262 | First argument is the command, all subsequent arguments are parameters |
|---|
| 263 | to that command. If a prefix is desired, it may be specified with the |
|---|
| 264 | keyword argument 'prefix'. |
|---|
| 265 | """ |
|---|
| 266 | |
|---|
| 267 | if not command: |
|---|
| 268 | raise ValueError, "IRC message requires a command." |
|---|
| 269 | |
|---|
| 270 | if ' ' in command or command[0] == ':': |
|---|
| 271 | # Not the ONLY way to screw up, but provides a little |
|---|
| 272 | # sanity checking to catch likely dumb mistakes. |
|---|
| 273 | raise ValueError, "Somebody screwed up, 'cuz this doesn't" \ |
|---|
| 274 | " look like a command to me: %s" % command |
|---|
| 275 | |
|---|
| 276 | line = string.join([command] + list(parameter_list)) |
|---|
| 277 | if prefix.has_key('prefix'): |
|---|
| 278 | line = ":%s %s" % (prefix['prefix'], line) |
|---|
| 279 | self.sendLine(line) |
|---|
| 280 | |
|---|
| 281 | if len(parameter_list) > 15: |
|---|
| 282 | log.msg("Message has %d parameters (RFC allows 15):\n%s" % |
|---|
| 283 | (len(parameter_list), line)) |
|---|
| 284 | |
|---|
| 285 | |
|---|
| 286 | def dataReceived(self, data): |
|---|
| 287 | """ |
|---|
| 288 | This hack is to support mIRC, which sends LF only, even though the RFC |
|---|
| 289 | says CRLF. (Also, the flexibility of LineReceiver to turn "line mode" |
|---|
| 290 | on and off was not required.) |
|---|
| 291 | """ |
|---|
| 292 | lines = (self.buffer + data).split(LF) |
|---|
| 293 | # Put the (possibly empty) element after the last LF back in the |
|---|
| 294 | # buffer |
|---|
| 295 | self.buffer = lines.pop() |
|---|
| 296 | |
|---|
| 297 | for line in lines: |
|---|
| 298 | if len(line) <= 2: |
|---|
| 299 | # This is a blank line, at best. |
|---|
| 300 | continue |
|---|
| 301 | if line[-1] == CR: |
|---|
| 302 | line = line[:-1] |
|---|
| 303 | prefix, command, params = parsemsg(line) |
|---|
| 304 | # mIRC is a big pile of doo-doo |
|---|
| 305 | command = command.upper() |
|---|
| 306 | # DEBUG: log.msg( "%s %s %s" % (prefix, command, params)) |
|---|
| 307 | |
|---|
| 308 | self.handleCommand(command, prefix, params) |
|---|
| 309 | |
|---|
| 310 | |
|---|
| 311 | def handleCommand(self, command, prefix, params): |
|---|
| 312 | """ |
|---|
| 313 | Determine the function to call for the given command and call it with |
|---|
| 314 | the given arguments. |
|---|
| 315 | """ |
|---|
| 316 | method = getattr(self, "irc_%s" % command, None) |
|---|
| 317 | try: |
|---|
| 318 | if method is not None: |
|---|
| 319 | method(prefix, params) |
|---|
| 320 | else: |
|---|
| 321 | self.irc_unknown(prefix, command, params) |
|---|
| 322 | except: |
|---|
| 323 | log.deferr() |
|---|
| 324 | |
|---|
| 325 | |
|---|
| 326 | def irc_unknown(self, prefix, command, params): |
|---|
| 327 | """ |
|---|
| 328 | Called by L{handleCommand} on a command that doesn't have a defined |
|---|
| 329 | handler. Subclasses should override this method. |
|---|
| 330 | """ |
|---|
| 331 | raise NotImplementedError(command, prefix, params) |
|---|
| 332 | |
|---|
| 333 | |
|---|
| 334 | # Helper methods |
|---|
| 335 | def privmsg(self, sender, recip, message): |
|---|
| 336 | """ |
|---|
| 337 | Send a message to a channel or user |
|---|
| 338 | |
|---|
| 339 | @type sender: C{str} or C{unicode} |
|---|
| 340 | @param sender: Who is sending this message. Should be of the form |
|---|
| 341 | username!ident@hostmask (unless you know better!). |
|---|
| 342 | |
|---|
| 343 | @type recip: C{str} or C{unicode} |
|---|
| 344 | @param recip: The recipient of this message. If a channel, it must |
|---|
| 345 | start with a channel prefix. |
|---|
| 346 | |
|---|
| 347 | @type message: C{str} or C{unicode} |
|---|
| 348 | @param message: The message being sent. |
|---|
| 349 | """ |
|---|
| 350 | self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message))) |
|---|
| 351 | |
|---|
| 352 | |
|---|
| 353 | def notice(self, sender, recip, message): |
|---|
| 354 | """ |
|---|
| 355 | Send a "notice" to a channel or user. |
|---|
| 356 | |
|---|
| 357 | Notices differ from privmsgs in that the RFC claims they are different. |
|---|
| 358 | Robots are supposed to send notices and not respond to them. Clients |
|---|
| 359 | typically display notices differently from privmsgs. |
|---|
| 360 | |
|---|
| 361 | @type sender: C{str} or C{unicode} |
|---|
| 362 | @param sender: Who is sending this message. Should be of the form |
|---|
| 363 | username!ident@hostmask (unless you know better!). |
|---|
| 364 | |
|---|
| 365 | @type recip: C{str} or C{unicode} |
|---|
| 366 | @param recip: The recipient of this message. If a channel, it must |
|---|
| 367 | start with a channel prefix. |
|---|
| 368 | |
|---|
| 369 | @type message: C{str} or C{unicode} |
|---|
| 370 | @param message: The message being sent. |
|---|
| 371 | """ |
|---|
| 372 | self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message)) |
|---|
| 373 | |
|---|
| 374 | |
|---|
| 375 | def action(self, sender, recip, message): |
|---|
| 376 | """ |
|---|
| 377 | Send an action to a channel or user. |
|---|
| 378 | |
|---|
| 379 | @type sender: C{str} or C{unicode} |
|---|
| 380 | @param sender: Who is sending this message. Should be of the form |
|---|
| 381 | username!ident@hostmask (unless you know better!). |
|---|
| 382 | |
|---|
| 383 | @type recip: C{str} or C{unicode} |
|---|
| 384 | @param recip: The recipient of this message. If a channel, it must |
|---|
| 385 | start with a channel prefix. |
|---|
| 386 | |
|---|
| 387 | @type message: C{str} or C{unicode} |
|---|
| 388 | @param message: The action being sent. |
|---|
| 389 | """ |
|---|
| 390 | self.sendLine(":%s ACTION %s :%s" % (sender, recip, message)) |
|---|
| 391 | |
|---|
| 392 | |
|---|
| 393 | def topic(self, user, channel, topic, author=None): |
|---|
| 394 | """ |
|---|
| 395 | Send the topic to a user. |
|---|
| 396 | |
|---|
| 397 | @type user: C{str} or C{unicode} |
|---|
| 398 | @param user: The user receiving the topic. Only their nick name, not |
|---|
| 399 | the full hostmask. |
|---|
| 400 | |
|---|
| 401 | @type channel: C{str} or C{unicode} |
|---|
| 402 | @param channel: The channel for which this is the topic. |
|---|
| 403 | |
|---|
| 404 | @type topic: C{str} or C{unicode} or C{None} |
|---|
| 405 | @param topic: The topic string, unquoted, or None if there is no topic. |
|---|
| 406 | |
|---|
| 407 | @type author: C{str} or C{unicode} |
|---|
| 408 | @param author: If the topic is being changed, the full username and |
|---|
| 409 | hostmask of the person changing it. |
|---|
| 410 | """ |
|---|
| 411 | if author is None: |
|---|
| 412 | if topic is None: |
|---|
| 413 | self.sendLine(':%s %s %s %s :%s' % ( |
|---|
| 414 | self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.')) |
|---|
| 415 | else: |
|---|
| 416 | self.sendLine(":%s %s %s %s :%s" % ( |
|---|
| 417 | self.hostname, RPL_TOPIC, user, channel, lowQuote(topic))) |
|---|
| 418 | else: |
|---|
| 419 | self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic))) |
|---|
| 420 | |
|---|
| 421 | |
|---|
| 422 | def topicAuthor(self, user, channel, author, date): |
|---|
| 423 | """ |
|---|
| 424 | Send the author of and time at which a topic was set for the given |
|---|
| 425 | channel. |
|---|
| 426 | |
|---|
| 427 | This sends a 333 reply message, which is not part of the IRC RFC. |
|---|
| 428 | |
|---|
| 429 | @type user: C{str} or C{unicode} |
|---|
| 430 | @param user: The user receiving the topic. Only their nick name, not |
|---|
| 431 | the full hostmask. |
|---|
| 432 | |
|---|
| 433 | @type channel: C{str} or C{unicode} |
|---|
| 434 | @param channel: The channel for which this information is relevant. |
|---|
| 435 | |
|---|
| 436 | @type author: C{str} or C{unicode} |
|---|
| 437 | @param author: The nickname (without hostmask) of the user who last set |
|---|
| 438 | the topic. |
|---|
| 439 | |
|---|
| 440 | @type date: C{int} |
|---|
| 441 | @param date: A POSIX timestamp (number of seconds since the epoch) at |
|---|
| 442 | which the topic was last set. |
|---|
| 443 | """ |
|---|
| 444 | self.sendLine(':%s %d %s %s %s %d' % ( |
|---|
| 445 | self.hostname, 333, user, channel, author, date)) |
|---|
| 446 | |
|---|
| 447 | |
|---|
| 448 | def names(self, user, channel, names): |
|---|
| 449 | """ |
|---|
| 450 | Send the names of a channel's participants to a user. |
|---|
| 451 | |
|---|
| 452 | @type user: C{str} or C{unicode} |
|---|
| 453 | @param user: The user receiving the name list. Only their nick name, |
|---|
| 454 | not the full hostmask. |
|---|
| 455 | |
|---|
| 456 | @type channel: C{str} or C{unicode} |
|---|
| 457 | @param channel: The channel for which this is the namelist. |
|---|
| 458 | |
|---|
| 459 | @type names: C{list} of C{str} or C{unicode} |
|---|
| 460 | @param names: The names to send. |
|---|
| 461 | """ |
|---|
| 462 | # XXX If unicode is given, these limits are not quite correct |
|---|
| 463 | prefixLength = len(channel) + len(user) + 10 |
|---|
| 464 | namesLength = 512 - prefixLength |
|---|
| 465 | |
|---|
| 466 | L = [] |
|---|
| 467 | count = 0 |
|---|
| 468 | for n in names: |
|---|
| 469 | if count + len(n) + 1 > namesLength: |
|---|
| 470 | self.sendLine(":%s %s %s = %s :%s" % ( |
|---|
| 471 | self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L))) |
|---|
| 472 | L = [n] |
|---|
| 473 | count = len(n) |
|---|
| 474 | else: |
|---|
| 475 | L.append(n) |
|---|
| 476 | count += len(n) + 1 |
|---|
| 477 | if L: |
|---|
| 478 | self.sendLine(":%s %s %s = %s :%s" % ( |
|---|
| 479 | self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L))) |
|---|
| 480 | self.sendLine(":%s %s %s %s :End of /NAMES list" % ( |
|---|
| 481 | self.hostname, RPL_ENDOFNAMES, user, channel)) |
|---|
| 482 | |
|---|
| 483 | |
|---|
| 484 | def who(self, user, channel, memberInfo): |
|---|
| 485 | """ |
|---|
| 486 | Send a list of users participating in a channel. |
|---|
| 487 | |
|---|
| 488 | @type user: C{str} or C{unicode} |
|---|
| 489 | @param user: The user receiving this member information. Only their |
|---|
| 490 | nick name, not the full hostmask. |
|---|
| 491 | |
|---|
| 492 | @type channel: C{str} or C{unicode} |
|---|
| 493 | @param channel: The channel for which this is the member information. |
|---|
| 494 | |
|---|
| 495 | @type memberInfo: C{list} of C{tuples} |
|---|
| 496 | @param memberInfo: For each member of the given channel, a 7-tuple |
|---|
| 497 | containing their username, their hostmask, the server to which they |
|---|
| 498 | are connected, their nickname, the letter "H" or "G" (standing for |
|---|
| 499 | "Here" or "Gone"), the hopcount from C{user} to this member, and |
|---|
| 500 | this member's real name. |
|---|
| 501 | """ |
|---|
| 502 | for info in memberInfo: |
|---|
| 503 | (username, hostmask, server, nickname, flag, hops, realName) = info |
|---|
| 504 | assert flag in ("H", "G") |
|---|
| 505 | self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % ( |
|---|
| 506 | self.hostname, RPL_WHOREPLY, user, channel, |
|---|
| 507 | username, hostmask, server, nickname, flag, hops, realName)) |
|---|
| 508 | |
|---|
| 509 | self.sendLine(":%s %s %s %s :End of /WHO list." % ( |
|---|
| 510 | self.hostname, RPL_ENDOFWHO, user, channel)) |
|---|
| 511 | |
|---|
| 512 | |
|---|
| 513 | def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels): |
|---|
| 514 | """ |
|---|
| 515 | Send information about the state of a particular user. |
|---|
| 516 | |
|---|
| 517 | @type user: C{str} or C{unicode} |
|---|
| 518 | @param user: The user receiving this information. Only their nick name, |
|---|
| 519 | not the full hostmask. |
|---|
| 520 | |
|---|
| 521 | @type nick: C{str} or C{unicode} |
|---|
| 522 | @param nick: The nickname of the user this information describes. |
|---|
| 523 | |
|---|
| 524 | @type username: C{str} or C{unicode} |
|---|
| 525 | @param username: The user's username (eg, ident response) |
|---|
| 526 | |
|---|
| 527 | @type hostname: C{str} |
|---|
| 528 | @param hostname: The user's hostmask |
|---|
| 529 | |
|---|
| 530 | @type realName: C{str} or C{unicode} |
|---|
| 531 | @param realName: The user's real name |
|---|
| 532 | |
|---|
| 533 | @type server: C{str} or C{unicode} |
|---|
| 534 | @param server: The name of the server to which the user is connected |
|---|
| 535 | |
|---|
| 536 | @type serverInfo: C{str} or C{unicode} |
|---|
| 537 | @param serverInfo: A descriptive string about that server |
|---|
| 538 | |
|---|
| 539 | @type oper: C{bool} |
|---|
| 540 | @param oper: Indicates whether the user is an IRC operator |
|---|
| 541 | |
|---|
| 542 | @type idle: C{int} |
|---|
| 543 | @param idle: The number of seconds since the user last sent a message |
|---|
| 544 | |
|---|
| 545 | @type signOn: C{int} |
|---|
| 546 | @param signOn: A POSIX timestamp (number of seconds since the epoch) |
|---|
| 547 | indicating the time the user signed on |
|---|
| 548 | |
|---|
| 549 | @type channels: C{list} of C{str} or C{unicode} |
|---|
| 550 | @param channels: A list of the channels which the user is participating in |
|---|
| 551 | """ |
|---|
| 552 | self.sendLine(":%s %s %s %s %s %s * :%s" % ( |
|---|
| 553 | self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName)) |
|---|
| 554 | self.sendLine(":%s %s %s %s %s :%s" % ( |
|---|
| 555 | self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo)) |
|---|
| 556 | if oper: |
|---|
| 557 | self.sendLine(":%s %s %s %s :is an IRC operator" % ( |
|---|
| 558 | self.hostname, RPL_WHOISOPERATOR, user, nick)) |
|---|
| 559 | self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % ( |
|---|
| 560 | self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn)) |
|---|
| 561 | self.sendLine(":%s %s %s %s :%s" % ( |
|---|
| 562 | self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels))) |
|---|
| 563 | self.sendLine(":%s %s %s %s :End of WHOIS list." % ( |
|---|
| 564 | self.hostname, RPL_ENDOFWHOIS, user, nick)) |
|---|
| 565 | |
|---|
| 566 | |
|---|
| 567 | def join(self, who, where): |
|---|
| 568 | """ |
|---|
| 569 | Send a join message. |
|---|
| 570 | |
|---|
| 571 | @type who: C{str} or C{unicode} |
|---|
| 572 | @param who: The name of the user joining. Should be of the form |
|---|
| 573 | username!ident@hostmask (unless you know better!). |
|---|
| 574 | |
|---|
| 575 | @type where: C{str} or C{unicode} |
|---|
| 576 | @param where: The channel the user is joining. |
|---|
| 577 | """ |
|---|
| 578 | self.sendLine(":%s JOIN %s" % (who, where)) |
|---|
| 579 | |
|---|
| 580 | |
|---|
| 581 | def part(self, who, where, reason=None): |
|---|
| 582 | """ |
|---|
| 583 | Send a part message. |
|---|
| 584 | |
|---|
| 585 | @type who: C{str} or C{unicode} |
|---|
| 586 | @param who: The name of the user joining. Should be of the form |
|---|
| 587 | username!ident@hostmask (unless you know better!). |
|---|
| 588 | |
|---|
| 589 | @type where: C{str} or C{unicode} |
|---|
| 590 | @param where: The channel the user is joining. |
|---|
| 591 | |
|---|
| 592 | @type reason: C{str} or C{unicode} |
|---|
| 593 | @param reason: A string describing the misery which caused this poor |
|---|
| 594 | soul to depart. |
|---|
| 595 | """ |
|---|
| 596 | if reason: |
|---|
| 597 | self.sendLine(":%s PART %s :%s" % (who, where, reason)) |
|---|
| 598 | else: |
|---|
| 599 | self.sendLine(":%s PART %s" % (who, where)) |
|---|
| 600 | |
|---|
| 601 | |
|---|
| 602 | def channelMode(self, user, channel, mode, *args): |
|---|
| 603 | """ |
|---|
| 604 | Send information about the mode of a channel. |
|---|
| 605 | |
|---|
| 606 | @type user: C{str} or C{unicode} |
|---|
| 607 | @param user: The user receiving the name list. Only their nick name, |
|---|
| 608 | not the full hostmask. |
|---|
| 609 | |
|---|
| 610 | @type channel: C{str} or C{unicode} |
|---|
| 611 | @param channel: The channel for which this is the namelist. |
|---|
| 612 | |
|---|
| 613 | @type mode: C{str} |
|---|
| 614 | @param mode: A string describing this channel's modes. |
|---|
| 615 | |
|---|
| 616 | @param args: Any additional arguments required by the modes. |
|---|
| 617 | """ |
|---|
| 618 | self.sendLine(":%s %s %s %s %s %s" % ( |
|---|
| 619 | self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args))) |
|---|
| 620 | |
|---|
| 621 | |
|---|
| 622 | |
|---|
| 623 | class ServerSupportedFeatures(_CommandDispatcherMixin): |
|---|
| 624 | """ |
|---|
| 625 | Handle ISUPPORT messages. |
|---|
| 626 | |
|---|
| 627 | Feature names match those in the ISUPPORT RFC draft identically. |
|---|
| 628 | |
|---|
| 629 | Information regarding the specifics of ISUPPORT was gleaned from |
|---|
| 630 | <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>. |
|---|
| 631 | """ |
|---|
| 632 | prefix = 'isupport' |
|---|
| 633 | |
|---|
| 634 | def __init__(self): |
|---|
| 635 | self._features = { |
|---|
| 636 | 'CHANNELLEN': 200, |
|---|
| 637 | 'CHANTYPES': tuple('#&'), |
|---|
| 638 | 'MODES': 3, |
|---|
| 639 | 'NICKLEN': 9, |
|---|
| 640 | 'PREFIX': self._parsePrefixParam('(ovh)@+%'), |
|---|
| 641 | # The ISUPPORT draft explicitly says that there is no default for |
|---|
| 642 | # CHANMODES, but we're defaulting it here to handle the case where |
|---|
| 643 | # the IRC server doesn't send us any ISUPPORT information, since |
|---|
| 644 | # IRCClient.getChannelModeParams relies on this value. |
|---|
| 645 | 'CHANMODES': self._parseChanModesParam(['b', '', 'lk'])} |
|---|
| 646 | |
|---|
| 647 | |
|---|
| 648 | def _splitParamArgs(cls, params, valueProcessor=None): |
|---|
| 649 | """ |
|---|
| 650 | Split ISUPPORT parameter arguments. |
|---|
| 651 | |
|---|
| 652 | Values can optionally be processed by C{valueProcessor}. |
|---|
| 653 | |
|---|
| 654 | For example:: |
|---|
| 655 | |
|---|
| 656 | >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2']) |
|---|
| 657 | (('A', '1'), ('B', '2')) |
|---|
| 658 | |
|---|
| 659 | @type params: C{iterable} of C{str} |
|---|
| 660 | |
|---|
| 661 | @type valueProcessor: C{callable} taking {str} |
|---|
| 662 | @param valueProcessor: Callable to process argument values, or C{None} |
|---|
| 663 | to perform no processing |
|---|
| 664 | |
|---|
| 665 | @rtype: C{list} of C{(str, object)} |
|---|
| 666 | @return: Sequence of C{(name, processedValue)} |
|---|
| 667 | """ |
|---|
| 668 | if valueProcessor is None: |
|---|
| 669 | valueProcessor = lambda x: x |
|---|
| 670 | |
|---|
| 671 | def _parse(): |
|---|
| 672 | for param in params: |
|---|
| 673 | if ':' not in param: |
|---|
| 674 | param += ':' |
|---|
| 675 | a, b = param.split(':', 1) |
|---|
| 676 | yield a, valueProcessor(b) |
|---|
| 677 | return list(_parse()) |
|---|
| 678 | _splitParamArgs = classmethod(_splitParamArgs) |
|---|
| 679 | |
|---|
| 680 | |
|---|
| 681 | def _unescapeParamValue(cls, value): |
|---|
| 682 | """ |
|---|
| 683 | Unescape an ISUPPORT parameter. |
|---|
| 684 | |
|---|
| 685 | The only form of supported escape is C{\\xHH}, where HH must be a valid |
|---|
| 686 | 2-digit hexadecimal number. |
|---|
| 687 | |
|---|
| 688 | @rtype: C{str} |
|---|
| 689 | """ |
|---|
| 690 | def _unescape(): |
|---|
| 691 | parts = value.split('\\x') |
|---|
| 692 | # The first part can never be preceeded by the escape. |
|---|
| 693 | yield parts.pop(0) |
|---|
| 694 | for s in parts: |
|---|
| 695 | octet, rest = s[:2], s[2:] |
|---|
| 696 | try: |
|---|
| 697 | octet = int(octet, 16) |
|---|
| 698 | except ValueError: |
|---|
| 699 | raise ValueError('Invalid hex octet: %r' % (octet,)) |
|---|
| 700 | yield chr(octet) + rest |
|---|
| 701 | |
|---|
| 702 | if '\\x' not in value: |
|---|
| 703 | return value |
|---|
| 704 | return ''.join(_unescape()) |
|---|
| 705 | _unescapeParamValue = classmethod(_unescapeParamValue) |
|---|
| 706 | |
|---|
| 707 | |
|---|
| 708 | def _splitParam(cls, param): |
|---|
| 709 | """ |
|---|
| 710 | Split an ISUPPORT parameter. |
|---|
| 711 | |
|---|
| 712 | @type param: C{str} |
|---|
| 713 | |
|---|
| 714 | @rtype: C{(str, list)} |
|---|
| 715 | @return C{(key, arguments)} |
|---|
| 716 | """ |
|---|
| 717 | if '=' not in param: |
|---|
| 718 | param += '=' |
|---|
| 719 | key, value = param.split('=', 1) |
|---|
| 720 | return key, map(cls._unescapeParamValue, value.split(',')) |
|---|
| 721 | _splitParam = classmethod(_splitParam) |
|---|
| 722 | |
|---|
| 723 | |
|---|
| 724 | def _parsePrefixParam(cls, prefix): |
|---|
| 725 | """ |
|---|
| 726 | Parse the ISUPPORT "PREFIX" parameter. |
|---|
| 727 | |
|---|
| 728 | The order in which the parameter arguments appear is significant, the |
|---|
| 729 | earlier a mode appears the more privileges it gives. |
|---|
| 730 | |
|---|
| 731 | @rtype: C{dict} mapping C{str} to C{(str, int)} |
|---|
| 732 | @return: A dictionary mapping a mode character to a two-tuple of |
|---|
| 733 | C({symbol, priority)}, the lower a priority (the lowest being |
|---|
| 734 | C{0}) the more privileges it gives |
|---|
| 735 | """ |
|---|
| 736 | if not prefix: |
|---|
| 737 | return None |
|---|
| 738 | if prefix[0] != '(' and ')' not in prefix: |
|---|
| 739 | raise ValueError('Malformed PREFIX parameter') |
|---|
| 740 | modes, symbols = prefix.split(')', 1) |
|---|
| 741 | symbols = zip(symbols, xrange(len(symbols))) |
|---|
| 742 | modes = modes[1:] |
|---|
| 743 | return dict(zip(modes, symbols)) |
|---|
| 744 | _parsePrefixParam = classmethod(_parsePrefixParam) |
|---|
| 745 | |
|---|
| 746 | |
|---|
| 747 | def _parseChanModesParam(self, params): |
|---|
| 748 | """ |
|---|
| 749 | Parse the ISUPPORT "CHANMODES" parameter. |
|---|
| 750 | |
|---|
| 751 | See L{isupport_CHANMODES} for a detailed explanation of this parameter. |
|---|
| 752 | """ |
|---|
| 753 | names = ('addressModes', 'param', 'setParam', 'noParam') |
|---|
| 754 | if len(params) > len(names): |
|---|
| 755 | raise ValueError( |
|---|
| 756 | 'Expecting a maximum of %d channel mode parameters, got %d' % ( |
|---|
| 757 | len(names), len(params))) |
|---|
| 758 | items = map(lambda key, value: (key, value or ''), names, params) |
|---|
| 759 | return dict(items) |
|---|
| 760 | _parseChanModesParam = classmethod(_parseChanModesParam) |
|---|
| 761 | |
|---|
| 762 | |
|---|
| 763 | def getFeature(self, feature, default=None): |
|---|
| 764 | """ |
|---|
| 765 | Get a server supported feature's value. |
|---|
| 766 | |
|---|
| 767 | A feature with the value C{None} is equivalent to the feature being |
|---|
| 768 | unsupported. |
|---|
| 769 | |
|---|
| 770 | @type feature: C{str} |
|---|
| 771 | @param feature: Feature name |
|---|
| 772 | |
|---|
| 773 | @type default: C{object} |
|---|
| 774 | @param default: The value to default to, assuming that C{feature} |
|---|
| 775 | is not supported |
|---|
| 776 | |
|---|
| 777 | @return: Feature value |
|---|
| 778 | """ |
|---|
| 779 | return self._features.get(feature, default) |
|---|
| 780 | |
|---|
| 781 | |
|---|
| 782 | def hasFeature(self, feature): |
|---|
| 783 | """ |
|---|
| 784 | Determine whether a feature is supported or not. |
|---|
| 785 | |
|---|
| 786 | @rtype: C{bool} |
|---|
| 787 | """ |
|---|
| 788 | return self.getFeature(feature) is not None |
|---|
| 789 | |
|---|
| 790 | |
|---|
| 791 | def parse(self, params): |
|---|
| 792 | """ |
|---|
| 793 | Parse ISUPPORT parameters. |
|---|
| 794 | |
|---|
| 795 | If an unknown parameter is encountered, it is simply added to the |
|---|
| 796 | dictionary, keyed by its name, as a tuple of the parameters provided. |
|---|
| 797 | |
|---|
| 798 | @type params: C{iterable} of C{str} |
|---|
| 799 | @param params: Iterable of ISUPPORT parameters to parse |
|---|
| 800 | """ |
|---|
| 801 | for param in params: |
|---|
| 802 | key, value = self._splitParam(param) |
|---|
| 803 | if key.startswith('-'): |
|---|
| 804 | self._features.pop(key[1:], None) |
|---|
| 805 | else: |
|---|
| 806 | self._features[key] = self.dispatch(key, value) |
|---|
| 807 | |
|---|
| 808 | |
|---|
| 809 | def isupport_unknown(self, command, params): |
|---|
| 810 | """ |
|---|
| 811 | Unknown ISUPPORT parameter. |
|---|
| 812 | """ |
|---|
| 813 | return tuple(params) |
|---|
| 814 | |
|---|
| 815 | |
|---|
| 816 | def isupport_CHANLIMIT(self, params): |
|---|
| 817 | """ |
|---|
| 818 | The maximum number of each channel type a user may join. |
|---|
| 819 | """ |
|---|
| 820 | return self._splitParamArgs(params, _intOrDefault) |
|---|
| 821 | |
|---|
| 822 | |
|---|
| 823 | def isupport_CHANMODES(self, params): |
|---|
| 824 | """ |
|---|
| 825 | Available channel modes. |
|---|
| 826 | |
|---|
| 827 | There are 4 categories of channel mode:: |
|---|
| 828 | |
|---|
| 829 | addressModes - Modes that add or remove an address to or from a |
|---|
| 830 | list, these modes always take a parameter. |
|---|
| 831 | |
|---|
| 832 | param - Modes that change a setting on a channel, these modes |
|---|
| 833 | always take a parameter. |
|---|
| 834 | |
|---|
| 835 | setParam - Modes that change a setting on a channel, these modes |
|---|
| 836 | only take a parameter when being set. |
|---|
| 837 | |
|---|
| 838 | noParam - Modes that change a setting on a channel, these modes |
|---|
| 839 | never take a parameter. |
|---|
| 840 | """ |
|---|
| 841 | try: |
|---|
| 842 | return self._parseChanModesParam(params) |
|---|
| 843 | except ValueError: |
|---|
| 844 | return self.getFeature('CHANMODES') |
|---|
| 845 | |
|---|
| 846 | |
|---|
| 847 | def isupport_CHANNELLEN(self, params): |
|---|
| 848 | """ |
|---|
| 849 | Maximum length of a channel name a client may create. |
|---|
| 850 | """ |
|---|
| 851 | return _intOrDefault(params[0], self.getFeature('CHANNELLEN')) |
|---|
| 852 | |
|---|
| 853 | |
|---|
| 854 | def isupport_CHANTYPES(self, params): |
|---|
| 855 | """ |
|---|
| 856 | Valid channel prefixes. |
|---|
| 857 | """ |
|---|
| 858 | return tuple(params[0]) |
|---|
| 859 | |
|---|
| 860 | |
|---|
| 861 | def isupport_EXCEPTS(self, params): |
|---|
| 862 | """ |
|---|
| 863 | Mode character for "ban exceptions". |
|---|
| 864 | |
|---|
| 865 | The presence of this parameter indicates that the server supports |
|---|
| 866 | this functionality. |
|---|
| 867 | """ |
|---|
| 868 | return params[0] or 'e' |
|---|
| 869 | |
|---|
| 870 | |
|---|
| 871 | def isupport_IDCHAN(self, params): |
|---|
| 872 | """ |
|---|
| 873 | Safe channel identifiers. |
|---|
| 874 | |
|---|
| 875 | The presence of this parameter indicates that the server supports |
|---|
| 876 | this functionality. |
|---|
| 877 | """ |
|---|
| 878 | return self._splitParamArgs(params) |
|---|
| 879 | |
|---|
| 880 | |
|---|
| 881 | def isupport_INVEX(self, params): |
|---|
| 882 | """ |
|---|
| 883 | Mode character for "invite exceptions". |
|---|
| 884 | |
|---|
| 885 | The presence of this parameter indicates that the server supports |
|---|
| 886 | this functionality. |
|---|
| 887 | """ |
|---|
| 888 | return params[0] or 'I' |
|---|
| 889 | |
|---|
| 890 | |
|---|
| 891 | def isupport_KICKLEN(self, params): |
|---|
| 892 | """ |
|---|
| 893 | Maximum length of a kick message a client may provide. |
|---|
| 894 | """ |
|---|
| 895 | return _intOrDefault(params[0]) |
|---|
| 896 | |
|---|
| 897 | |
|---|
| 898 | def isupport_MAXLIST(self, params): |
|---|
| 899 | """ |
|---|
| 900 | Maximum number of "list modes" a client may set on a channel at once. |
|---|
| 901 | |
|---|
| 902 | List modes are identified by the "addressModes" key in CHANMODES. |
|---|
| 903 | """ |
|---|
| 904 | return self._splitParamArgs(params, _intOrDefault) |
|---|
| 905 | |
|---|
| 906 | |
|---|
| 907 | def isupport_MODES(self, params): |
|---|
| 908 | """ |
|---|
| 909 | Maximum number of modes accepting parameters that may be sent, by a |
|---|
| 910 | client, in a single MODE command. |
|---|
| 911 | """ |
|---|
| 912 | return _intOrDefault(params[0]) |
|---|
| 913 | |
|---|
| 914 | |
|---|
| 915 | def isupport_NETWORK(self, params): |
|---|
| 916 | """ |
|---|
| 917 | IRC network name. |
|---|
| 918 | """ |
|---|
| 919 | return params[0] |
|---|
| 920 | |
|---|
| 921 | |
|---|
| 922 | def isupport_NICKLEN(self, params): |
|---|
| 923 | """ |
|---|
| 924 | Maximum length of a nickname the client may use. |
|---|
| 925 | """ |
|---|
| 926 | return _intOrDefault(params[0], self.getFeature('NICKLEN')) |
|---|
| 927 | |
|---|
| 928 | |
|---|
| 929 | def isupport_PREFIX(self, params): |
|---|
| 930 | """ |
|---|
| 931 | Mapping of channel modes that clients may have to status flags. |
|---|
| 932 | """ |
|---|
| 933 | try: |
|---|
| 934 | return self._parsePrefixParam(params[0]) |
|---|
| 935 | except ValueError: |
|---|
| 936 | return self.getFeature('PREFIX') |
|---|
| 937 | |
|---|
| 938 | |
|---|
| 939 | def isupport_SAFELIST(self, params): |
|---|
| 940 | """ |
|---|
| 941 | Flag indicating that a client may request a LIST without being |
|---|
| 942 | disconnected due to the large amount of data generated. |
|---|
| 943 | """ |
|---|
| 944 | return True |
|---|
| 945 | |
|---|
| 946 | |
|---|
| 947 | def isupport_STATUSMSG(self, params): |
|---|
| 948 | """ |
|---|
| 949 | The server supports sending messages to only to clients on a channel |
|---|
| 950 | with a specific status. |
|---|
| 951 | """ |
|---|
| 952 | return params[0] |
|---|
| 953 | |
|---|
| 954 | |
|---|
| 955 | def isupport_TARGMAX(self, params): |
|---|
| 956 | """ |
|---|
| 957 | Maximum number of targets allowable for commands that accept multiple |
|---|
| 958 | targets. |
|---|
| 959 | """ |
|---|
| 960 | return dict(self._splitParamArgs(params, _intOrDefault)) |
|---|
| 961 | |
|---|
| 962 | |
|---|
| 963 | def isupport_TOPICLEN(self, params): |
|---|
| 964 | """ |
|---|
| 965 | Maximum length of a topic that may be set. |
|---|
| 966 | """ |
|---|
| 967 | return _intOrDefault(params[0]) |
|---|
| 968 | |
|---|
| 969 | |
|---|
| 970 | |
|---|
| 971 | class IRCClient(basic.LineReceiver): |
|---|
| 972 | """ |
|---|
| 973 | Internet Relay Chat client protocol, with sprinkles. |
|---|
| 974 | |
|---|
| 975 | In addition to providing an interface for an IRC client protocol, |
|---|
| 976 | this class also contains reasonable implementations of many common |
|---|
| 977 | CTCP methods. |
|---|
| 978 | |
|---|
| 979 | TODO |
|---|
| 980 | ==== |
|---|
| 981 | - Limit the length of messages sent (because the IRC server probably |
|---|
| 982 | does). |
|---|
| 983 | - Add flood protection/rate limiting for my CTCP replies. |
|---|
| 984 | - NickServ cooperation. (a mix-in?) |
|---|
| 985 | |
|---|
| 986 | @ivar nickname: Nickname the client will use. |
|---|
| 987 | @ivar password: Password used to log on to the server. May be C{None}. |
|---|
| 988 | @ivar realname: Supplied to the server during login as the "Real name" |
|---|
| 989 | or "ircname". May be C{None}. |
|---|
| 990 | @ivar username: Supplied to the server during login as the "User name". |
|---|
| 991 | May be C{None} |
|---|
| 992 | |
|---|
| 993 | @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query. If C{None}, no |
|---|
| 994 | USERINFO reply will be sent. |
|---|
| 995 | "This is used to transmit a string which is settable by |
|---|
| 996 | the user (and never should be set by the client)." |
|---|
| 997 | @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query. If C{None}, no |
|---|
| 998 | FINGER reply will be sent. |
|---|
| 999 | @type fingerReply: Callable or String |
|---|
| 1000 | |
|---|
| 1001 | @ivar versionName: CTCP VERSION reply, client name. If C{None}, no VERSION |
|---|
| 1002 | reply will be sent. |
|---|
| 1003 | @type versionName: C{str}, or None. |
|---|
| 1004 | @ivar versionNum: CTCP VERSION reply, client version. |
|---|
| 1005 | @type versionNum: C{str}, or None. |
|---|
| 1006 | @ivar versionEnv: CTCP VERSION reply, environment the client is running in. |
|---|
| 1007 | @type versionEnv: C{str}, or None. |
|---|
| 1008 | |
|---|
| 1009 | @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this |
|---|
| 1010 | client may be found. If C{None}, no SOURCE reply will be sent. |
|---|
| 1011 | |
|---|
| 1012 | @ivar lineRate: Minimum delay between lines sent to the server. If |
|---|
| 1013 | C{None}, no delay will be imposed. |
|---|
| 1014 | @type lineRate: Number of Seconds. |
|---|
| 1015 | |
|---|
| 1016 | @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and |
|---|
| 1017 | I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content |
|---|
| 1018 | of an I{RPL_MOTD} message. |
|---|
| 1019 | |
|---|
| 1020 | @ivar erroneousNickFallback: Default nickname assigned when an unregistered |
|---|
| 1021 | client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register |
|---|
| 1022 | with an illegal nickname. |
|---|
| 1023 | @type erroneousNickFallback: C{str} |
|---|
| 1024 | |
|---|
| 1025 | @ivar _registered: Whether or not the user is registered. It becomes True |
|---|
| 1026 | once a welcome has been received from the server. |
|---|
| 1027 | @type _registered: C{bool} |
|---|
| 1028 | |
|---|
| 1029 | @ivar _attemptedNick: The nickname that will try to get registered. It may |
|---|
| 1030 | change if it is illegal or already taken. L{nickname} becomes the |
|---|
| 1031 | L{_attemptedNick} that is successfully registered. |
|---|
| 1032 | @type _attemptedNick: C{str} |
|---|
| 1033 | |
|---|
| 1034 | @type supported: L{ServerSupportedFeatures} |
|---|
| 1035 | @ivar supported: Available ISUPPORT features on the server |
|---|
| 1036 | |
|---|
| 1037 | @type hostname: C{str} |
|---|
| 1038 | @ivar hostname: Host name of the IRC server the client is connected to. |
|---|
| 1039 | Initially the host name is C{None} and later is set to the host name |
|---|
| 1040 | from which the I{RPL_WELCOME} message is received. |
|---|
| 1041 | |
|---|
| 1042 | @type _heartbeat: L{task.LoopingCall} |
|---|
| 1043 | @ivar _heartbeat: Looping call to perform the keepalive by calling |
|---|
| 1044 | L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or |
|---|
| 1045 | C{None} if there is no heartbeat. |
|---|
| 1046 | |
|---|
| 1047 | @type heartbeatInterval: C{float} |
|---|
| 1048 | @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to |
|---|
| 1049 | the server as a form of keepalive, defaults to 120 seconds. Use C{None} |
|---|
| 1050 | to disable the heartbeat. |
|---|
| 1051 | """ |
|---|
| 1052 | hostname = None |
|---|
| 1053 | motd = None |
|---|
| 1054 | nickname = 'irc' |
|---|
| 1055 | password = None |
|---|
| 1056 | realname = None |
|---|
| 1057 | username = None |
|---|
| 1058 | ### Responses to various CTCP queries. |
|---|
| 1059 | |
|---|
| 1060 | userinfo = None |
|---|
| 1061 | # fingerReply is a callable returning a string, or a str()able object. |
|---|
| 1062 | fingerReply = None |
|---|
| 1063 | versionName = None |
|---|
| 1064 | versionNum = None |
|---|
| 1065 | versionEnv = None |
|---|
| 1066 | |
|---|
| 1067 | sourceURL = "http://twistedmatrix.com/downloads/" |
|---|
| 1068 | |
|---|
| 1069 | dcc_destdir = '.' |
|---|
| 1070 | dcc_sessions = None |
|---|
| 1071 | |
|---|
| 1072 | # If this is false, no attempt will be made to identify |
|---|
| 1073 | # ourself to the server. |
|---|
| 1074 | performLogin = 1 |
|---|
| 1075 | |
|---|
| 1076 | lineRate = None |
|---|
| 1077 | _queue = None |
|---|
| 1078 | _queueEmptying = None |
|---|
| 1079 | |
|---|
| 1080 | delimiter = '\n' # '\r\n' will also work (see dataReceived) |
|---|
| 1081 | |
|---|
| 1082 | __pychecker__ = 'unusednames=params,prefix,channel' |
|---|
| 1083 | |
|---|
| 1084 | _registered = False |
|---|
| 1085 | _attemptedNick = '' |
|---|
| 1086 | erroneousNickFallback = 'defaultnick' |
|---|
| 1087 | |
|---|
| 1088 | _heartbeat = None |
|---|
| 1089 | heartbeatInterval = 120 |
|---|
| 1090 | |
|---|
| 1091 | |
|---|
| 1092 | def _reallySendLine(self, line): |
|---|
| 1093 | return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r') |
|---|
| 1094 | |
|---|
| 1095 | def sendLine(self, line): |
|---|
| 1096 | if self.lineRate is None: |
|---|
| 1097 | self._reallySendLine(line) |
|---|
| 1098 | else: |
|---|
| 1099 | self._queue.append(line) |
|---|
| 1100 | if not self._queueEmptying: |
|---|
| 1101 | self._sendLine() |
|---|
| 1102 | |
|---|
| 1103 | def _sendLine(self): |
|---|
| 1104 | if self._queue: |
|---|
| 1105 | self._reallySendLine(self._queue.pop(0)) |
|---|
| 1106 | self._queueEmptying = reactor.callLater(self.lineRate, |
|---|
| 1107 | self._sendLine) |
|---|
| 1108 | else: |
|---|
| 1109 | self._queueEmptying = None |
|---|
| 1110 | |
|---|
| 1111 | |
|---|
| 1112 | def connectionLost(self, reason): |
|---|
| 1113 | basic.LineReceiver.connectionLost(self, reason) |
|---|
| 1114 | self.stopHeartbeat() |
|---|
| 1115 | |
|---|
| 1116 | |
|---|
| 1117 | def _createHeartbeat(self): |
|---|
| 1118 | """ |
|---|
| 1119 | Create the heartbeat L{LoopingCall}. |
|---|
| 1120 | """ |
|---|
| 1121 | return task.LoopingCall(self._sendHeartbeat) |
|---|
| 1122 | |
|---|
| 1123 | |
|---|
| 1124 | def _sendHeartbeat(self): |
|---|
| 1125 | """ |
|---|
| 1126 | Send a I{PING} message to the IRC server as a form of keepalive. |
|---|
| 1127 | """ |
|---|
| 1128 | self.sendLine('PING ' + self.hostname) |
|---|
| 1129 | |
|---|
| 1130 | |
|---|
| 1131 | def stopHeartbeat(self): |
|---|
| 1132 | """ |
|---|
| 1133 | Stop sending I{PING} messages to keep the connection to the server |
|---|
| 1134 | alive. |
|---|
| 1135 | |
|---|
| 1136 | @since: 11.1 |
|---|
| 1137 | """ |
|---|
| 1138 | if self._heartbeat is not None: |
|---|
| 1139 | self._heartbeat.stop() |
|---|
| 1140 | self._heartbeat = None |
|---|
| 1141 | |
|---|
| 1142 | |
|---|
| 1143 | def startHeartbeat(self): |
|---|
| 1144 | """ |
|---|
| 1145 | Start sending I{PING} messages every L{IRCClient.heartbeatInterval} |
|---|
| 1146 | seconds to keep the connection to the server alive during periods of no |
|---|
| 1147 | activity. |
|---|
| 1148 | |
|---|
| 1149 | @since: 11.1 |
|---|
| 1150 | """ |
|---|
| 1151 | self.stopHeartbeat() |
|---|
| 1152 | if self.heartbeatInterval is None: |
|---|
| 1153 | return |
|---|
| 1154 | self._heartbeat = self._createHeartbeat() |
|---|
| 1155 | self._heartbeat.start(self.heartbeatInterval, now=False) |
|---|
| 1156 | |
|---|
| 1157 | |
|---|
| 1158 | ### Interface level client->user output methods |
|---|
| 1159 | ### |
|---|
| 1160 | ### You'll want to override these. |
|---|
| 1161 | |
|---|
| 1162 | ### Methods relating to the server itself |
|---|
| 1163 | |
|---|
| 1164 | def created(self, when): |
|---|
| 1165 | """ |
|---|
| 1166 | Called with creation date information about the server, usually at logon. |
|---|
| 1167 | |
|---|
| 1168 | @type when: C{str} |
|---|
| 1169 | @param when: A string describing when the server was created, probably. |
|---|
| 1170 | """ |
|---|
| 1171 | |
|---|
| 1172 | def yourHost(self, info): |
|---|
| 1173 | """ |
|---|
| 1174 | Called with daemon information about the server, usually at logon. |
|---|
| 1175 | |
|---|
| 1176 | @type info: C{str} |
|---|
| 1177 | @param when: A string describing what software the server is running, probably. |
|---|
| 1178 | """ |
|---|
| 1179 | |
|---|
| 1180 | def myInfo(self, servername, version, umodes, cmodes): |
|---|
| 1181 | """ |
|---|
| 1182 | Called with information about the server, usually at logon. |
|---|
| 1183 | |
|---|
| 1184 | @type servername: C{str} |
|---|
| 1185 | @param servername: The hostname of this server. |
|---|
| 1186 | |
|---|
| 1187 | @type version: C{str} |
|---|
| 1188 | @param version: A description of what software this server runs. |
|---|
| 1189 | |
|---|
| 1190 | @type umodes: C{str} |
|---|
| 1191 | @param umodes: All the available user modes. |
|---|
| 1192 | |
|---|
| 1193 | @type cmodes: C{str} |
|---|
| 1194 | @param cmodes: All the available channel modes. |
|---|
| 1195 | """ |
|---|
| 1196 | |
|---|
| 1197 | def luserClient(self, info): |
|---|
| 1198 | """ |
|---|
| 1199 | Called with information about the number of connections, usually at logon. |
|---|
| 1200 | |
|---|
| 1201 | @type info: C{str} |
|---|
| 1202 | @param info: A description of the number of clients and servers |
|---|
| 1203 | connected to the network, probably. |
|---|
| 1204 | """ |
|---|
| 1205 | |
|---|
| 1206 | def bounce(self, info): |
|---|
| 1207 | """ |
|---|
| 1208 | Called with information about where the client should reconnect. |
|---|
| 1209 | |
|---|
| 1210 | @type info: C{str} |
|---|
| 1211 | @param info: A plaintext description of the address that should be |
|---|
| 1212 | connected to. |
|---|
| 1213 | """ |
|---|
| 1214 | |
|---|
| 1215 | def isupport(self, options): |
|---|
| 1216 | """ |
|---|
| 1217 | Called with various information about what the server supports. |
|---|
| 1218 | |
|---|
| 1219 | @type options: C{list} of C{str} |
|---|
| 1220 | @param options: Descriptions of features or limits of the server, possibly |
|---|
| 1221 | in the form "NAME=VALUE". |
|---|
| 1222 | """ |
|---|
| 1223 | |
|---|
| 1224 | def luserChannels(self, channels): |
|---|
| 1225 | """ |
|---|
| 1226 | Called with the number of channels existant on the server. |
|---|
| 1227 | |
|---|
| 1228 | @type channels: C{int} |
|---|
| 1229 | """ |
|---|
| 1230 | |
|---|
| 1231 | def luserOp(self, ops): |
|---|
| 1232 | """ |
|---|
| 1233 | Called with the number of ops logged on to the server. |
|---|
| 1234 | |
|---|
| 1235 | @type ops: C{int} |
|---|
| 1236 | """ |
|---|
| 1237 | |
|---|
| 1238 | def luserMe(self, info): |
|---|
| 1239 | """ |
|---|
| 1240 | Called with information about the server connected to. |
|---|
| 1241 | |
|---|
| 1242 | @type info: C{str} |
|---|
| 1243 | @param info: A plaintext string describing the number of users and servers |
|---|
| 1244 | connected to this server. |
|---|
| 1245 | """ |
|---|
| 1246 | |
|---|
| 1247 | ### Methods involving me directly |
|---|
| 1248 | |
|---|
| 1249 | def privmsg(self, user, channel, message): |
|---|
| 1250 | """ |
|---|
| 1251 | Called when I have a message from a user to me or a channel. |
|---|
| 1252 | """ |
|---|
| 1253 | pass |
|---|
| 1254 | |
|---|
| 1255 | def joined(self, channel): |
|---|
| 1256 | """ |
|---|
| 1257 | Called when I finish joining a channel. |
|---|
| 1258 | |
|---|
| 1259 | channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'}) |
|---|
| 1260 | intact. |
|---|
| 1261 | """ |
|---|
| 1262 | |
|---|
| 1263 | def left(self, channel): |
|---|
| 1264 | """ |
|---|
| 1265 | Called when I have left a channel. |
|---|
| 1266 | |
|---|
| 1267 | channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'}) |
|---|
| 1268 | intact. |
|---|
| 1269 | """ |
|---|
| 1270 | |
|---|
| 1271 | |
|---|
| 1272 | def noticed(self, user, channel, message): |
|---|
| 1273 | """ |
|---|
| 1274 | Called when I have a notice from a user to me or a channel. |
|---|
| 1275 | |
|---|
| 1276 | If the client makes any automated replies, it must not do so in |
|---|
| 1277 | response to a NOTICE message, per the RFC:: |
|---|
| 1278 | |
|---|
| 1279 | The difference between NOTICE and PRIVMSG is that |
|---|
| 1280 | automatic replies MUST NEVER be sent in response to a |
|---|
| 1281 | NOTICE message. [...] The object of this rule is to avoid |
|---|
| 1282 | loops between clients automatically sending something in |
|---|
| 1283 | response to something it received. |
|---|
| 1284 | """ |
|---|
| 1285 | |
|---|
| 1286 | |
|---|
| 1287 | def modeChanged(self, user, channel, set, modes, args): |
|---|
| 1288 | """ |
|---|
| 1289 | Called when users or channel's modes are changed. |
|---|
| 1290 | |
|---|
| 1291 | @type user: C{str} |
|---|
| 1292 | @param user: The user and hostmask which instigated this change. |
|---|
| 1293 | |
|---|
| 1294 | @type channel: C{str} |
|---|
| 1295 | @param channel: The channel where the modes are changed. If args is |
|---|
| 1296 | empty the channel for which the modes are changing. If the changes are |
|---|
| 1297 | at server level it could be equal to C{user}. |
|---|
| 1298 | |
|---|
| 1299 | @type set: C{bool} or C{int} |
|---|
| 1300 | @param set: True if the mode(s) is being added, False if it is being |
|---|
| 1301 | removed. If some modes are added and others removed at the same time |
|---|
| 1302 | this function will be called twice, the first time with all the added |
|---|
| 1303 | modes, the second with the removed ones. (To change this behaviour |
|---|
| 1304 | override the irc_MODE method) |
|---|
| 1305 | |
|---|
| 1306 | @type modes: C{str} |
|---|
| 1307 | @param modes: The mode or modes which are being changed. |
|---|
| 1308 | |
|---|
| 1309 | @type args: C{tuple} |
|---|
| 1310 | @param args: Any additional information required for the mode |
|---|
| 1311 | change. |
|---|
| 1312 | """ |
|---|
| 1313 | |
|---|
| 1314 | def pong(self, user, secs): |
|---|
| 1315 | """ |
|---|
| 1316 | Called with the results of a CTCP PING query. |
|---|
| 1317 | """ |
|---|
| 1318 | pass |
|---|
| 1319 | |
|---|
| 1320 | def signedOn(self): |
|---|
| 1321 | """ |
|---|
| 1322 | Called after sucessfully signing on to the server. |
|---|
| 1323 | """ |
|---|
| 1324 | pass |
|---|
| 1325 | |
|---|
| 1326 | def kickedFrom(self, channel, kicker, message): |
|---|
| 1327 | """ |
|---|
| 1328 | Called when I am kicked from a channel. |
|---|
| 1329 | """ |
|---|
| 1330 | pass |
|---|
| 1331 | |
|---|
| 1332 | def nickChanged(self, nick): |
|---|
| 1333 | """ |
|---|
| 1334 | Called when my nick has been changed. |
|---|
| 1335 | """ |
|---|
| 1336 | self.nickname = nick |
|---|
| 1337 | |
|---|
| 1338 | |
|---|
| 1339 | ### Things I observe other people doing in a channel. |
|---|
| 1340 | |
|---|
| 1341 | def userJoined(self, user, channel): |
|---|
| 1342 | """ |
|---|
| 1343 | Called when I see another user joining a channel. |
|---|
| 1344 | """ |
|---|
| 1345 | pass |
|---|
| 1346 | |
|---|
| 1347 | def userLeft(self, user, channel): |
|---|
| 1348 | """ |
|---|
| 1349 | Called when I see another user leaving a channel. |
|---|
| 1350 | """ |
|---|
| 1351 | pass |
|---|
| 1352 | |
|---|
| 1353 | def userQuit(self, user, quitMessage): |
|---|
| 1354 | """ |
|---|
| 1355 | Called when I see another user disconnect from the network. |
|---|
| 1356 | """ |
|---|
| 1357 | pass |
|---|
| 1358 | |
|---|
| 1359 | def userKicked(self, kickee, channel, kicker, message): |
|---|
| 1360 | """ |
|---|
| 1361 | Called when I observe someone else being kicked from a channel. |
|---|
| 1362 | """ |
|---|
| 1363 | pass |
|---|
| 1364 | |
|---|
| 1365 | def action(self, user, channel, data): |
|---|
| 1366 | """ |
|---|
| 1367 | Called when I see a user perform an ACTION on a channel. |
|---|
| 1368 | """ |
|---|
| 1369 | pass |
|---|
| 1370 | |
|---|
| 1371 | def topicUpdated(self, user, channel, newTopic): |
|---|
| 1372 | """ |
|---|
| 1373 | In channel, user changed the topic to newTopic. |
|---|
| 1374 | |
|---|
| 1375 | Also called when first joining a channel. |
|---|
| 1376 | """ |
|---|
| 1377 | pass |
|---|
| 1378 | |
|---|
| 1379 | def userRenamed(self, oldname, newname): |
|---|
| 1380 | """ |
|---|
| 1381 | A user changed their name from oldname to newname. |
|---|
| 1382 | """ |
|---|
| 1383 | pass |
|---|
| 1384 | |
|---|
| 1385 | ### Information from the server. |
|---|
| 1386 | |
|---|
| 1387 | def receivedMOTD(self, motd): |
|---|
| 1388 | """ |
|---|
| 1389 | I received a message-of-the-day banner from the server. |
|---|
| 1390 | |
|---|
| 1391 | motd is a list of strings, where each string was sent as a seperate |
|---|
| 1392 | message from the server. To display, you might want to use:: |
|---|
| 1393 | |
|---|
| 1394 | '\\n'.join(motd) |
|---|
| 1395 | |
|---|
| 1396 | to get a nicely formatted string. |
|---|
| 1397 | """ |
|---|
| 1398 | pass |
|---|
| 1399 | |
|---|
| 1400 | ### user input commands, client->server |
|---|
| 1401 | ### Your client will want to invoke these. |
|---|
| 1402 | |
|---|
| 1403 | def join(self, channel, key=None): |
|---|
| 1404 | """ |
|---|
| 1405 | Join a channel. |
|---|
| 1406 | |
|---|
| 1407 | @type channel: C{str} |
|---|
| 1408 | @param channel: The name of the channel to join. If it has no prefix, |
|---|
| 1409 | C{'#'} will be prepended to it. |
|---|
| 1410 | @type key: C{str} |
|---|
| 1411 | @param key: If specified, the key used to join the channel. |
|---|
| 1412 | """ |
|---|
| 1413 | if channel[0] not in CHANNEL_PREFIXES: |
|---|
| 1414 | channel = '#' + channel |
|---|
| 1415 | if key: |
|---|
| 1416 | self.sendLine("JOIN %s %s" % (channel, key)) |
|---|
| 1417 | else: |
|---|
| 1418 | self.sendLine("JOIN %s" % (channel,)) |
|---|
| 1419 | |
|---|
| 1420 | def leave(self, channel, reason=None): |
|---|
| 1421 | """ |
|---|
| 1422 | Leave a channel. |
|---|
| 1423 | |
|---|
| 1424 | @type channel: C{str} |
|---|
| 1425 | @param channel: The name of the channel to leave. If it has no prefix, |
|---|
| 1426 | C{'#'} will be prepended to it. |
|---|
| 1427 | @type reason: C{str} |
|---|
| 1428 | @param reason: If given, the reason for leaving. |
|---|
| 1429 | """ |
|---|
| 1430 | if channel[0] not in CHANNEL_PREFIXES: |
|---|
| 1431 | channel = '#' + channel |
|---|
| 1432 | if reason: |
|---|
| 1433 | self.sendLine("PART %s :%s" % (channel, reason)) |
|---|
| 1434 | else: |
|---|
| 1435 | self.sendLine("PART %s" % (channel,)) |
|---|
| 1436 | |
|---|
| 1437 | def kick(self, channel, user, reason=None): |
|---|
| 1438 | """ |
|---|
| 1439 | Attempt to kick a user from a channel. |
|---|
| 1440 | |
|---|
| 1441 | @type channel: C{str} |
|---|
| 1442 | @param channel: The name of the channel to kick the user from. If it has |
|---|
| 1443 | no prefix, C{'#'} will be prepended to it. |
|---|
| 1444 | @type user: C{str} |
|---|
| 1445 | @param user: The nick of the user to kick. |
|---|
| 1446 | @type reason: C{str} |
|---|
| 1447 | @param reason: If given, the reason for kicking the user. |
|---|
| 1448 | """ |
|---|
| 1449 | if channel[0] not in CHANNEL_PREFIXES: |
|---|
| 1450 | channel = '#' + channel |
|---|
| 1451 | if reason: |
|---|
| 1452 | self.sendLine("KICK %s %s :%s" % (channel, user, reason)) |
|---|
| 1453 | else: |
|---|
| 1454 | self.sendLine("KICK %s %s" % (channel, user)) |
|---|
| 1455 | |
|---|
| 1456 | part = leave |
|---|
| 1457 | |
|---|
| 1458 | |
|---|
| 1459 | def invite(self, user, channel): |
|---|
| 1460 | """ |
|---|
| 1461 | Attempt to invite user to channel |
|---|
| 1462 | |
|---|
| 1463 | @type user: C{str} |
|---|
| 1464 | @param user: The user to invite |
|---|
| 1465 | @type channel: C{str} |
|---|
| 1466 | @param channel: The channel to invite the user too |
|---|
| 1467 | |
|---|
| 1468 | @since: 11.0 |
|---|
| 1469 | """ |
|---|
| 1470 | if channel[0] not in CHANNEL_PREFIXES: |
|---|
| 1471 | channel = '#' + channel |
|---|
| 1472 | self.sendLine("INVITE %s %s" % (user, channel)) |
|---|
| 1473 | |
|---|
| 1474 | |
|---|
| 1475 | def topic(self, channel, topic=None): |
|---|
| 1476 | """ |
|---|
| 1477 | Attempt to set the topic of the given channel, or ask what it is. |
|---|
| 1478 | |
|---|
| 1479 | If topic is None, then I sent a topic query instead of trying to set the |
|---|
| 1480 | topic. The server should respond with a TOPIC message containing the |
|---|
| 1481 | current topic of the given channel. |
|---|
| 1482 | |
|---|
| 1483 | @type channel: C{str} |
|---|
| 1484 | @param channel: The name of the channel to change the topic on. If it |
|---|
| 1485 | has no prefix, C{'#'} will be prepended to it. |
|---|
| 1486 | @type topic: C{str} |
|---|
| 1487 | @param topic: If specified, what to set the topic to. |
|---|
| 1488 | """ |
|---|
| 1489 | # << TOPIC #xtestx :fff |
|---|
| 1490 | if channel[0] not in CHANNEL_PREFIXES: |
|---|
| 1491 | channel = '#' + channel |
|---|
| 1492 | if topic != None: |
|---|
| 1493 | self.sendLine("TOPIC %s :%s" % (channel, topic)) |
|---|
| 1494 | else: |
|---|
| 1495 | self.sendLine("TOPIC %s" % (channel,)) |
|---|
| 1496 | |
|---|
| 1497 | |
|---|
| 1498 | def mode(self, chan, set, modes, limit = None, user = None, mask = None): |
|---|
| 1499 | """ |
|---|
| 1500 | Change the modes on a user or channel. |
|---|
| 1501 | |
|---|
| 1502 | The C{limit}, C{user}, and C{mask} parameters are mutually exclusive. |
|---|
| 1503 | |
|---|
| 1504 | @type chan: C{str} |
|---|
| 1505 | @param chan: The name of the channel to operate on. |
|---|
| 1506 | @type set: C{bool} |
|---|
| 1507 | @param set: True to give the user or channel permissions and False to |
|---|
| 1508 | remove them. |
|---|
| 1509 | @type modes: C{str} |
|---|
| 1510 | @param modes: The mode flags to set on the user or channel. |
|---|
| 1511 | @type limit: C{int} |
|---|
| 1512 | @param limit: In conjuction with the C{'l'} mode flag, limits the |
|---|
| 1513 | number of users on the channel. |
|---|
| 1514 | @type user: C{str} |
|---|
| 1515 | @param user: The user to change the mode on. |
|---|
| 1516 | @type mask: C{str} |
|---|
| 1517 | @param mask: In conjuction with the C{'b'} mode flag, sets a mask of |
|---|
| 1518 | users to be banned from the channel. |
|---|
| 1519 | """ |
|---|
| 1520 | if set: |
|---|
| 1521 | line = 'MODE %s +%s' % (chan, modes) |
|---|
| 1522 | else: |
|---|
| 1523 | line = 'MODE %s -%s' % (chan, modes) |
|---|
| 1524 | if limit is not None: |
|---|
| 1525 | line = '%s %d' % (line, limit) |
|---|
| 1526 | elif user is not None: |
|---|
| 1527 | line = '%s %s' % (line, user) |
|---|
| 1528 | elif mask is not None: |
|---|
| 1529 | line = '%s %s' % (line, mask) |
|---|
| 1530 | self.sendLine(line) |
|---|
| 1531 | |
|---|
| 1532 | |
|---|
| 1533 | def say(self, channel, message, length=None): |
|---|
| 1534 | """ |
|---|
| 1535 | Send a message to a channel |
|---|
| 1536 | |
|---|
| 1537 | @type channel: C{str} |
|---|
| 1538 | @param channel: The channel to say the message on. If it has no prefix, |
|---|
| 1539 | C{'#'} will be prepended to it. |
|---|
| 1540 | @type message: C{str} |
|---|
| 1541 | @param message: The message to say. |
|---|
| 1542 | @type length: C{int} |
|---|
| 1543 | @param length: The maximum number of octets to send at a time. This has |
|---|
| 1544 | the effect of turning a single call to C{msg()} into multiple |
|---|
| 1545 | commands to the server. This is useful when long messages may be |
|---|
| 1546 | sent that would otherwise cause the server to kick us off or |
|---|
| 1547 | silently truncate the text we are sending. If None is passed, the |
|---|
| 1548 | entire message is always send in one command. |
|---|
| 1549 | """ |
|---|
| 1550 | if channel[0] not in CHANNEL_PREFIXES: |
|---|
| 1551 | channel = '#' + channel |
|---|
| 1552 | self.msg(channel, message, length) |
|---|
| 1553 | |
|---|
| 1554 | |
|---|
| 1555 | def _safeMaximumLineLength(self, command): |
|---|
| 1556 | """ |
|---|
| 1557 | Estimate a safe maximum line length for the given command. |
|---|
| 1558 | |
|---|
| 1559 | This is done by assuming the maximum values for nickname length, |
|---|
| 1560 | realname and hostname combined with the command that needs to be sent |
|---|
| 1561 | and some guessing. A theoretical maximum value is used because it is |
|---|
| 1562 | possible that our nickname, username or hostname changes (on the server |
|---|
| 1563 | side) while the length is still being calculated. |
|---|
| 1564 | """ |
|---|
| 1565 | # :nickname!realname@hostname COMMAND ... |
|---|
| 1566 | theoretical = ':%s!%s@%s %s' % ( |
|---|
| 1567 | 'a' * self.supported.getFeature('NICKLEN'), |
|---|
| 1568 | # This value is based on observation. |
|---|
| 1569 | 'b' * 10, |
|---|
| 1570 | # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>. |
|---|
| 1571 | 'c' * 63, |
|---|
| 1572 | command) |
|---|
| 1573 | # Fingers crossed. |
|---|
| 1574 | fudge = 10 |
|---|
| 1575 | return MAX_COMMAND_LENGTH - len(theoretical) - fudge |
|---|
| 1576 | |
|---|
| 1577 | |
|---|
| 1578 | def msg(self, user, message, length=None): |
|---|
| 1579 | """ |
|---|
| 1580 | Send a message to a user or channel. |
|---|
| 1581 | |
|---|
| 1582 | The message will be split into multiple commands to the server if: |
|---|
| 1583 | - The message contains any newline characters |
|---|
| 1584 | - Any span between newline characters is longer than the given |
|---|
| 1585 | line-length. |
|---|
| 1586 | |
|---|
| 1587 | @param user: Username or channel name to which to direct the |
|---|
| 1588 | message. |
|---|
| 1589 | @type user: C{str} |
|---|
| 1590 | |
|---|
| 1591 | @param message: Text to send. |
|---|
| 1592 | @type message: C{str} |
|---|
| 1593 | |
|---|
| 1594 | @param length: Maximum number of octets to send in a single |
|---|
| 1595 | command, including the IRC protocol framing. If C{None} is given |
|---|
| 1596 | then L{IRCClient._safeMaximumLineLength} is used to determine a |
|---|
| 1597 | value. |
|---|
| 1598 | @type length: C{int} |
|---|
| 1599 | """ |
|---|
| 1600 | fmt = 'PRIVMSG %s :' % (user,) |
|---|
| 1601 | |
|---|
| 1602 | if length is None: |
|---|
| 1603 | length = self._safeMaximumLineLength(fmt) |
|---|
| 1604 | |
|---|
| 1605 | # Account for the line terminator. |
|---|
| 1606 | minimumLength = len(fmt) + 2 |
|---|
| 1607 | if length <= minimumLength: |
|---|
| 1608 | raise ValueError("Maximum length must exceed %d for message " |
|---|
| 1609 | "to %s" % (minimumLength, user)) |
|---|
| 1610 | for line in split(message, length - minimumLength): |
|---|
| 1611 | self.sendLine(fmt + line) |
|---|
| 1612 | |
|---|
| 1613 | |
|---|
| 1614 | def notice(self, user, message): |
|---|
| 1615 | """ |
|---|
| 1616 | Send a notice to a user. |
|---|
| 1617 | |
|---|
| 1618 | Notices are like normal message, but should never get automated |
|---|
| 1619 | replies. |
|---|
| 1620 | |
|---|
| 1621 | @type user: C{str} |
|---|
| 1622 | @param user: The user to send a notice to. |
|---|
| 1623 | @type message: C{str} |
|---|
| 1624 | @param message: The contents of the notice to send. |
|---|
| 1625 | """ |
|---|
| 1626 | self.sendLine("NOTICE %s :%s" % (user, message)) |
|---|
| 1627 | |
|---|
| 1628 | |
|---|
| 1629 | def away(self, message=''): |
|---|
| 1630 | """ |
|---|
| 1631 | Mark this client as away. |
|---|
| 1632 | |
|---|
| 1633 | @type message: C{str} |
|---|
| 1634 | @param message: If specified, the away message. |
|---|
| 1635 | """ |
|---|
| 1636 | self.sendLine("AWAY :%s" % message) |
|---|
| 1637 | |
|---|
| 1638 | |
|---|
| 1639 | def back(self): |
|---|
| 1640 | """ |
|---|
| 1641 | Clear the away status. |
|---|
| 1642 | """ |
|---|
| 1643 | # An empty away marks us as back |
|---|
| 1644 | self.away() |
|---|
| 1645 | |
|---|
| 1646 | |
|---|
| 1647 | def whois(self, nickname, server=None): |
|---|
| 1648 | """ |
|---|
| 1649 | Retrieve user information about the given nick name. |
|---|
| 1650 | |
|---|
| 1651 | @type nickname: C{str} |
|---|
| 1652 | @param nickname: The nick name about which to retrieve information. |
|---|
| 1653 | |
|---|
| 1654 | @since: 8.2 |
|---|
| 1655 | """ |
|---|
| 1656 | if server is None: |
|---|
| 1657 | self.sendLine('WHOIS ' + nickname) |
|---|
| 1658 | else: |
|---|
| 1659 | self.sendLine('WHOIS %s %s' % (server, nickname)) |
|---|
| 1660 | |
|---|
| 1661 | |
|---|
| 1662 | def register(self, nickname, hostname='foo', servername='bar'): |
|---|
| 1663 | """ |
|---|
| 1664 | Login to the server. |
|---|
| 1665 | |
|---|
| 1666 | @type nickname: C{str} |
|---|
| 1667 | @param nickname: The nickname to register. |
|---|
| 1668 | @type hostname: C{str} |
|---|
| 1669 | @param hostname: If specified, the hostname to logon as. |
|---|
| 1670 | @type servername: C{str} |
|---|
| 1671 | @param servername: If specified, the servername to logon as. |
|---|
| 1672 | """ |
|---|
| 1673 | if self.password is not None: |
|---|
| 1674 | self.sendLine("PASS %s" % self.password) |
|---|
| 1675 | self.setNick(nickname) |
|---|
| 1676 | if self.username is None: |
|---|
| 1677 | self.username = nickname |
|---|
| 1678 | self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname)) |
|---|
| 1679 | |
|---|
| 1680 | |
|---|
| 1681 | def setNick(self, nickname): |
|---|
| 1682 | """ |
|---|
| 1683 | Set this client's nickname. |
|---|
| 1684 | |
|---|
| 1685 | @type nickname: C{str} |
|---|
| 1686 | @param nickname: The nickname to change to. |
|---|
| 1687 | """ |
|---|
| 1688 | self._attemptedNick = nickname |
|---|
| 1689 | self.sendLine("NICK %s" % nickname) |
|---|
| 1690 | |
|---|
| 1691 | |
|---|
| 1692 | def quit(self, message = ''): |
|---|
| 1693 | """ |
|---|
| 1694 | Disconnect from the server |
|---|
| 1695 | |
|---|
| 1696 | @type message: C{str} |
|---|
| 1697 | |
|---|
| 1698 | @param message: If specified, the message to give when quitting the |
|---|
| 1699 | server. |
|---|
| 1700 | """ |
|---|
| 1701 | self.sendLine("QUIT :%s" % message) |
|---|
| 1702 | |
|---|
| 1703 | ### user input commands, client->client |
|---|
| 1704 | |
|---|
| 1705 | def describe(self, channel, action): |
|---|
| 1706 | """ |
|---|
| 1707 | Strike a pose. |
|---|
| 1708 | |
|---|
| 1709 | @type channel: C{str} |
|---|
| 1710 | @param channel: The name of the channel to have an action on. If it |
|---|
| 1711 | has no prefix, it is sent to the user of that name. |
|---|
| 1712 | @type action: C{str} |
|---|
| 1713 | @param action: The action to preform. |
|---|
| 1714 | @since: 9.0 |
|---|
| 1715 | """ |
|---|
| 1716 | self.ctcpMakeQuery(channel, [('ACTION', action)]) |
|---|
| 1717 | |
|---|
| 1718 | |
|---|
| 1719 | _pings = None |
|---|
| 1720 | _MAX_PINGRING = 12 |
|---|
| 1721 | |
|---|
| 1722 | def ping(self, user, text = None): |
|---|
| 1723 | """ |
|---|
| 1724 | Measure round-trip delay to another IRC client. |
|---|
| 1725 | """ |
|---|
| 1726 | if self._pings is None: |
|---|
| 1727 | self._pings = {} |
|---|
| 1728 | |
|---|
| 1729 | if text is None: |
|---|
| 1730 | chars = string.letters + string.digits + string.punctuation |
|---|
| 1731 | key = ''.join([random.choice(chars) for i in range(12)]) |
|---|
| 1732 | else: |
|---|
| 1733 | key = str(text) |
|---|
| 1734 | self._pings[(user, key)] = time.time() |
|---|
| 1735 | self.ctcpMakeQuery(user, [('PING', key)]) |
|---|
| 1736 | |
|---|
| 1737 | if len(self._pings) > self._MAX_PINGRING: |
|---|
| 1738 | # Remove some of the oldest entries. |
|---|
| 1739 | byValue = [(v, k) for (k, v) in self._pings.items()] |
|---|
| 1740 | byValue.sort() |
|---|
| 1741 | excess = self._MAX_PINGRING - len(self._pings) |
|---|
| 1742 | for i in xrange(excess): |
|---|
| 1743 | del self._pings[byValue[i][1]] |
|---|
| 1744 | |
|---|
| 1745 | |
|---|
| 1746 | def dccSend(self, user, file): |
|---|
| 1747 | if type(file) == types.StringType: |
|---|
| 1748 | file = open(file, 'r') |
|---|
| 1749 | |
|---|
| 1750 | size = fileSize(file) |
|---|
| 1751 | |
|---|
| 1752 | name = getattr(file, "name", "file@%s" % (id(file),)) |
|---|
| 1753 | |
|---|
| 1754 | factory = DccSendFactory(file) |
|---|
| 1755 | port = reactor.listenTCP(0, factory, 1) |
|---|
| 1756 | |
|---|
| 1757 | raise NotImplementedError,( |
|---|
| 1758 | "XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. " |
|---|
| 1759 | "(and stop accepting once we've made a single connection.)") |
|---|
| 1760 | |
|---|
| 1761 | my_address = struct.pack("!I", my_address) |
|---|
| 1762 | |
|---|
| 1763 | args = ['SEND', name, my_address, str(port)] |
|---|
| 1764 | |
|---|
| 1765 | if not (size is None): |
|---|
| 1766 | args.append(size) |
|---|
| 1767 | |
|---|
| 1768 | args = string.join(args, ' ') |
|---|
| 1769 | |
|---|
| 1770 | self.ctcpMakeQuery(user, [('DCC', args)]) |
|---|
| 1771 | |
|---|
| 1772 | |
|---|
| 1773 | def dccResume(self, user, fileName, port, resumePos): |
|---|
| 1774 | """ |
|---|
| 1775 | Send a DCC RESUME request to another user. |
|---|
| 1776 | """ |
|---|
| 1777 | self.ctcpMakeQuery(user, [ |
|---|
| 1778 | ('DCC', ['RESUME', fileName, port, resumePos])]) |
|---|
| 1779 | |
|---|
| 1780 | |
|---|
| 1781 | def dccAcceptResume(self, user, fileName, port, resumePos): |
|---|
| 1782 | """ |
|---|
| 1783 | Send a DCC ACCEPT response to clients who have requested a resume. |
|---|
| 1784 | """ |
|---|
| 1785 | self.ctcpMakeQuery(user, [ |
|---|
| 1786 | ('DCC', ['ACCEPT', fileName, port, resumePos])]) |
|---|
| 1787 | |
|---|
| 1788 | ### server->client messages |
|---|
| 1789 | ### You might want to fiddle with these, |
|---|
| 1790 | ### but it is safe to leave them alone. |
|---|
| 1791 | |
|---|
| 1792 | def irc_ERR_NICKNAMEINUSE(self, prefix, params): |
|---|
| 1793 | """ |
|---|
| 1794 | Called when we try to register or change to a nickname that is already |
|---|
| 1795 | taken. |
|---|
| 1796 | """ |
|---|
| 1797 | self._attemptedNick = self.alterCollidedNick(self._attemptedNick) |
|---|
| 1798 | self.setNick(self._attemptedNick) |
|---|
| 1799 | |
|---|
| 1800 | |
|---|
| 1801 | def alterCollidedNick(self, nickname): |
|---|
| 1802 | """ |
|---|
| 1803 | Generate an altered version of a nickname that caused a collision in an |
|---|
| 1804 | effort to create an unused related name for subsequent registration. |
|---|
| 1805 | |
|---|
| 1806 | @param nickname: The nickname a user is attempting to register. |
|---|
| 1807 | @type nickname: C{str} |
|---|
| 1808 | |
|---|
| 1809 | @returns: A string that is in some way different from the nickname. |
|---|
| 1810 | @rtype: C{str} |
|---|
| 1811 | """ |
|---|
| 1812 | return nickname + '_' |
|---|
| 1813 | |
|---|
| 1814 | |
|---|
| 1815 | def irc_ERR_ERRONEUSNICKNAME(self, prefix, params): |
|---|
| 1816 | """ |
|---|
| 1817 | Called when we try to register or change to an illegal nickname. |
|---|
| 1818 | |
|---|
| 1819 | The server should send this reply when the nickname contains any |
|---|
| 1820 | disallowed characters. The bot will stall, waiting for RPL_WELCOME, if |
|---|
| 1821 | we don't handle this during sign-on. |
|---|
| 1822 | |
|---|
| 1823 | @note: The method uses the spelling I{erroneus}, as it appears in |
|---|
| 1824 | the RFC, section 6.1. |
|---|
| 1825 | """ |
|---|
| 1826 | if not self._registered: |
|---|
| 1827 | self.setNick(self.erroneousNickFallback) |
|---|
| 1828 | |
|---|
| 1829 | |
|---|
| 1830 | def irc_ERR_PASSWDMISMATCH(self, prefix, params): |
|---|
| 1831 | """ |
|---|
| 1832 | Called when the login was incorrect. |
|---|
| 1833 | """ |
|---|
| 1834 | raise IRCPasswordMismatch("Password Incorrect.") |
|---|
| 1835 | |
|---|
| 1836 | |
|---|
| 1837 | def irc_RPL_WELCOME(self, prefix, params): |
|---|
| 1838 | """ |
|---|
| 1839 | Called when we have received the welcome from the server. |
|---|
| 1840 | """ |
|---|
| 1841 | self.hostname = prefix |
|---|
| 1842 | self._registered = True |
|---|
| 1843 | self.nickname = self._attemptedNick |
|---|
| 1844 | self.signedOn() |
|---|
| 1845 | self.startHeartbeat() |
|---|
| 1846 | |
|---|
| 1847 | |
|---|
| 1848 | def irc_JOIN(self, prefix, params): |
|---|
| 1849 | """ |
|---|
| 1850 | Called when a user joins a channel. |
|---|
| 1851 | """ |
|---|
| 1852 | nick = string.split(prefix,'!')[0] |
|---|
| 1853 | channel = params[-1] |
|---|
| 1854 | if nick == self.nickname: |
|---|
| 1855 | self.joined(channel) |
|---|
| 1856 | else: |
|---|
| 1857 | self.userJoined(nick, channel) |
|---|
| 1858 | |
|---|
| 1859 | def irc_PART(self, prefix, params): |
|---|
| 1860 | """ |
|---|
| 1861 | Called when a user leaves a channel. |
|---|
| 1862 | """ |
|---|
| 1863 | nick = string.split(prefix,'!')[0] |
|---|
| 1864 | channel = params[0] |
|---|
| 1865 | if nick == self.nickname: |
|---|
| 1866 | self.left(channel) |
|---|
| 1867 | else: |
|---|
| 1868 | self.userLeft(nick, channel) |
|---|
| 1869 | |
|---|
| 1870 | def irc_QUIT(self, prefix, params): |
|---|
| 1871 | """ |
|---|
| 1872 | Called when a user has quit. |
|---|
| 1873 | """ |
|---|
| 1874 | nick = string.split(prefix,'!')[0] |
|---|
| 1875 | self.userQuit(nick, params[0]) |
|---|
| 1876 | |
|---|
| 1877 | |
|---|
| 1878 | def irc_MODE(self, user, params): |
|---|
| 1879 | """ |
|---|
| 1880 | Parse a server mode change message. |
|---|
| 1881 | """ |
|---|
| 1882 | channel, modes, args = params[0], params[1], params[2:] |
|---|
| 1883 | |
|---|
| 1884 | if modes[0] not in '-+': |
|---|
| 1885 | modes = '+' + modes |
|---|
| 1886 | |
|---|
| 1887 | if channel == self.nickname: |
|---|
| 1888 | # This is a mode change to our individual user, not a channel mode |
|---|
| 1889 | # that involves us. |
|---|
| 1890 | paramModes = self.getUserModeParams() |
|---|
| 1891 | else: |
|---|
| 1892 | paramModes = self.getChannelModeParams() |
|---|
| 1893 | |
|---|
| 1894 | try: |
|---|
| 1895 | added, removed = parseModes(modes, args, paramModes) |
|---|
| 1896 | except IRCBadModes: |
|---|
| 1897 | log.err(None, 'An error occured while parsing the following ' |
|---|
| 1898 | 'MODE message: MODE %s' % (' '.join(params),)) |
|---|
| 1899 | else: |
|---|
| 1900 | if added: |
|---|
| 1901 | modes, params = zip(*added) |
|---|
| 1902 | self.modeChanged(user, channel, True, ''.join(modes), params) |
|---|
| 1903 | |
|---|
| 1904 | if removed: |
|---|
| 1905 | modes, params = zip(*removed) |
|---|
| 1906 | self.modeChanged(user, channel, False, ''.join(modes), params) |
|---|
| 1907 | |
|---|
| 1908 | |
|---|
| 1909 | def irc_PING(self, prefix, params): |
|---|
| 1910 | """ |
|---|
| 1911 | Called when some has pinged us. |
|---|
| 1912 | """ |
|---|
| 1913 | self.sendLine("PONG %s" % params[-1]) |
|---|
| 1914 | |
|---|
| 1915 | def irc_PRIVMSG(self, prefix, params): |
|---|
| 1916 | """ |
|---|
| 1917 | Called when we get a message. |
|---|
| 1918 | """ |
|---|
| 1919 | user = prefix |
|---|
| 1920 | channel = params[0] |
|---|
| 1921 | message = params[-1] |
|---|
| 1922 | |
|---|
| 1923 | if not message: |
|---|
| 1924 | # Don't raise an exception if we get blank message. |
|---|
| 1925 | return |
|---|
| 1926 | |
|---|
| 1927 | if message[0] == X_DELIM: |
|---|
| 1928 | m = ctcpExtract(message) |
|---|
| 1929 | if m['extended']: |
|---|
| 1930 | self.ctcpQuery(user, channel, m['extended']) |
|---|
| 1931 | |
|---|
| 1932 | if not m['normal']: |
|---|
| 1933 | return |
|---|
| 1934 | |
|---|
| 1935 | message = string.join(m['normal'], ' ') |
|---|
| 1936 | |
|---|
| 1937 | self.privmsg(user, channel, message) |
|---|
| 1938 | |
|---|
| 1939 | def irc_NOTICE(self, prefix, params): |
|---|
| 1940 | """ |
|---|
| 1941 | Called when a user gets a notice. |
|---|
| 1942 | """ |
|---|
| 1943 | user = prefix |
|---|
| 1944 | channel = params[0] |
|---|
| 1945 | message = params[-1] |
|---|
| 1946 | |
|---|
| 1947 | if message[0]==X_DELIM: |
|---|
| 1948 | m = ctcpExtract(message) |
|---|
| 1949 | if m['extended']: |
|---|
| 1950 | self.ctcpReply(user, channel, m['extended']) |
|---|
| 1951 | |
|---|
| 1952 | if not m['normal']: |
|---|
| 1953 | return |
|---|
| 1954 | |
|---|
| 1955 | message = string.join(m['normal'], ' ') |
|---|
| 1956 | |
|---|
| 1957 | self.noticed(user, channel, message) |
|---|
| 1958 | |
|---|
| 1959 | def irc_NICK(self, prefix, params): |
|---|
| 1960 | """ |
|---|
| 1961 | Called when a user changes their nickname. |
|---|
| 1962 | """ |
|---|
| 1963 | nick = string.split(prefix,'!', 1)[0] |
|---|
| 1964 | if nick == self.nickname: |
|---|
| 1965 | self.nickChanged(params[0]) |
|---|
| 1966 | else: |
|---|
| 1967 | self.userRenamed(nick, params[0]) |
|---|
| 1968 | |
|---|
| 1969 | def irc_KICK(self, prefix, params): |
|---|
| 1970 | """ |
|---|
| 1971 | Called when a user is kicked from a channel. |
|---|
| 1972 | """ |
|---|
| 1973 | kicker = string.split(prefix,'!')[0] |
|---|
| 1974 | channel = params[0] |
|---|
| 1975 | kicked = params[1] |
|---|
| 1976 | message = params[-1] |
|---|
| 1977 | if string.lower(kicked) == string.lower(self.nickname): |
|---|
| 1978 | # Yikes! |
|---|
| 1979 | self.kickedFrom(channel, kicker, message) |
|---|
| 1980 | else: |
|---|
| 1981 | self.userKicked(kicked, channel, kicker, message) |
|---|
| 1982 | |
|---|
| 1983 | def irc_TOPIC(self, prefix, params): |
|---|
| 1984 | """ |
|---|
| 1985 | Someone in the channel set the topic. |
|---|
| 1986 | """ |
|---|
| 1987 | user = string.split(prefix, '!')[0] |
|---|
| 1988 | channel = params[0] |
|---|
| 1989 | newtopic = params[1] |
|---|
| 1990 | self.topicUpdated(user, channel, newtopic) |
|---|
| 1991 | |
|---|
| 1992 | def irc_RPL_TOPIC(self, prefix, params): |
|---|
| 1993 | """ |
|---|
| 1994 | Called when the topic for a channel is initially reported or when it |
|---|
| 1995 | subsequently changes. |
|---|
| 1996 | """ |
|---|
| 1997 | user = string.split(prefix, '!')[0] |
|---|
| 1998 | channel = params[1] |
|---|
| 1999 | newtopic = params[2] |
|---|
| 2000 | self.topicUpdated(user, channel, newtopic) |
|---|
| 2001 | |
|---|
| 2002 | def irc_RPL_NOTOPIC(self, prefix, params): |
|---|
| 2003 | user = string.split(prefix, '!')[0] |
|---|
| 2004 | channel = params[1] |
|---|
| 2005 | newtopic = "" |
|---|
| 2006 | self.topicUpdated(user, channel, newtopic) |
|---|
| 2007 | |
|---|
| 2008 | def irc_RPL_MOTDSTART(self, prefix, params): |
|---|
| 2009 | if params[-1].startswith("- "): |
|---|
| 2010 | params[-1] = params[-1][2:] |
|---|
| 2011 | self.motd = [params[-1]] |
|---|
| 2012 | |
|---|
| 2013 | def irc_RPL_MOTD(self, prefix, params): |
|---|
| 2014 | if params[-1].startswith("- "): |
|---|
| 2015 | params[-1] = params[-1][2:] |
|---|
| 2016 | if self.motd is None: |
|---|
| 2017 | self.motd = [] |
|---|
| 2018 | self.motd.append(params[-1]) |
|---|
| 2019 | |
|---|
| 2020 | |
|---|
| 2021 | def irc_RPL_ENDOFMOTD(self, prefix, params): |
|---|
| 2022 | """ |
|---|
| 2023 | I{RPL_ENDOFMOTD} indicates the end of the message of the day |
|---|
| 2024 | messages. Deliver the accumulated lines to C{receivedMOTD}. |
|---|
| 2025 | """ |
|---|
| 2026 | motd = self.motd |
|---|
| 2027 | self.motd = None |
|---|
| 2028 | self.receivedMOTD(motd) |
|---|
| 2029 | |
|---|
| 2030 | |
|---|
| 2031 | def irc_RPL_CREATED(self, prefix, params): |
|---|
| 2032 | self.created(params[1]) |
|---|
| 2033 | |
|---|
| 2034 | def irc_RPL_YOURHOST(self, prefix, params): |
|---|
| 2035 | self.yourHost(params[1]) |
|---|
| 2036 | |
|---|
| 2037 | def irc_RPL_MYINFO(self, prefix, params): |
|---|
| 2038 | info = params[1].split(None, 3) |
|---|
| 2039 | while len(info) < 4: |
|---|
| 2040 | info.append(None) |
|---|
| 2041 | self.myInfo(*info) |
|---|
| 2042 | |
|---|
| 2043 | def irc_RPL_BOUNCE(self, prefix, params): |
|---|
| 2044 | self.bounce(params[1]) |
|---|
| 2045 | |
|---|
| 2046 | def irc_RPL_ISUPPORT(self, prefix, params): |
|---|
| 2047 | args = params[1:-1] |
|---|
| 2048 | # Several ISUPPORT messages, in no particular order, may be sent |
|---|
| 2049 | # to the client at any given point in time (usually only on connect, |
|---|
| 2050 | # though.) For this reason, ServerSupportedFeatures.parse is intended |
|---|
| 2051 | # to mutate the supported feature list. |
|---|
| 2052 | self.supported.parse(args) |
|---|
| 2053 | self.isupport(args) |
|---|
| 2054 | |
|---|
| 2055 | def irc_RPL_LUSERCLIENT(self, prefix, params): |
|---|
| 2056 | self.luserClient(params[1]) |
|---|
| 2057 | |
|---|
| 2058 | def irc_RPL_LUSEROP(self, prefix, params): |
|---|
| 2059 | try: |
|---|
| 2060 | self.luserOp(int(params[1])) |
|---|
| 2061 | except ValueError: |
|---|
| 2062 | pass |
|---|
| 2063 | |
|---|
| 2064 | def irc_RPL_LUSERCHANNELS(self, prefix, params): |
|---|
| 2065 | try: |
|---|
| 2066 | self.luserChannels(int(params[1])) |
|---|
| 2067 | except ValueError: |
|---|
| 2068 | pass |
|---|
| 2069 | |
|---|
| 2070 | def irc_RPL_LUSERME(self, prefix, params): |
|---|
| 2071 | self.luserMe(params[1]) |
|---|
| 2072 | |
|---|
| 2073 | def irc_unknown(self, prefix, command, params): |
|---|
| 2074 | pass |
|---|
| 2075 | |
|---|
| 2076 | ### Receiving a CTCP query from another party |
|---|
| 2077 | ### It is safe to leave these alone. |
|---|
| 2078 | |
|---|
| 2079 | |
|---|
| 2080 | def ctcpQuery(self, user, channel, messages): |
|---|
| 2081 | """ |
|---|
| 2082 | Dispatch method for any CTCP queries received. |
|---|
| 2083 | |
|---|
| 2084 | Duplicated CTCP queries are ignored and no dispatch is |
|---|
| 2085 | made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}. |
|---|
| 2086 | """ |
|---|
| 2087 | seen = set() |
|---|
| 2088 | for tag, data in messages: |
|---|
| 2089 | method = getattr(self, 'ctcpQuery_%s' % tag, None) |
|---|
| 2090 | if tag not in seen: |
|---|
| 2091 | if method is not None: |
|---|
| 2092 | method(user, channel, data) |
|---|
| 2093 | else: |
|---|
| 2094 | self.ctcpUnknownQuery(user, channel, tag, data) |
|---|
| 2095 | seen.add(tag) |
|---|
| 2096 | |
|---|
| 2097 | |
|---|
| 2098 | def ctcpUnknownQuery(self, user, channel, tag, data): |
|---|
| 2099 | """ |
|---|
| 2100 | Fallback handler for unrecognized CTCP queries. |
|---|
| 2101 | |
|---|
| 2102 | No CTCP I{ERRMSG} reply is made to remove a potential denial of service |
|---|
| 2103 | avenue. |
|---|
| 2104 | """ |
|---|
| 2105 | log.msg('Unknown CTCP query from %r: %r %r' % (user, tag, data)) |
|---|
| 2106 | |
|---|
| 2107 | |
|---|
| 2108 | def ctcpQuery_ACTION(self, user, channel, data): |
|---|
| 2109 | self.action(user, channel, data) |
|---|
| 2110 | |
|---|
| 2111 | def ctcpQuery_PING(self, user, channel, data): |
|---|
| 2112 | nick = string.split(user,"!")[0] |
|---|
| 2113 | self.ctcpMakeReply(nick, [("PING", data)]) |
|---|
| 2114 | |
|---|
| 2115 | def ctcpQuery_FINGER(self, user, channel, data): |
|---|
| 2116 | if data is not None: |
|---|
| 2117 | self.quirkyMessage("Why did %s send '%s' with a FINGER query?" |
|---|
| 2118 | % (user, data)) |
|---|
| 2119 | if not self.fingerReply: |
|---|
| 2120 | return |
|---|
| 2121 | |
|---|
| 2122 | if callable(self.fingerReply): |
|---|
| 2123 | reply = self.fingerReply() |
|---|
| 2124 | else: |
|---|
| 2125 | reply = str(self.fingerReply) |
|---|
| 2126 | |
|---|
| 2127 | nick = string.split(user,"!")[0] |
|---|
| 2128 | self.ctcpMakeReply(nick, [('FINGER', reply)]) |
|---|
| 2129 | |
|---|
| 2130 | def ctcpQuery_VERSION(self, user, channel, data): |
|---|
| 2131 | if data is not None: |
|---|
| 2132 | self.quirkyMessage("Why did %s send '%s' with a VERSION query?" |
|---|
| 2133 | % (user, data)) |
|---|
| 2134 | |
|---|
| 2135 | if self.versionName: |
|---|
| 2136 | nick = string.split(user,"!")[0] |
|---|
| 2137 | self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' % |
|---|
| 2138 | (self.versionName, |
|---|
| 2139 | self.versionNum or '', |
|---|
| 2140 | self.versionEnv or ''))]) |
|---|
| 2141 | |
|---|
| 2142 | def ctcpQuery_SOURCE(self, user, channel, data): |
|---|
| 2143 | if data is not None: |
|---|
| 2144 | self.quirkyMessage("Why did %s send '%s' with a SOURCE query?" |
|---|
| 2145 | % (user, data)) |
|---|
| 2146 | if self.sourceURL: |
|---|
| 2147 | nick = string.split(user,"!")[0] |
|---|
| 2148 | # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE |
|---|
| 2149 | # replies should be responded to with the location of an anonymous |
|---|
| 2150 | # FTP server in host:directory:file format. I'm taking the liberty |
|---|
| 2151 | # of bringing it into the 21st century by sending a URL instead. |
|---|
| 2152 | self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL), |
|---|
| 2153 | ('SOURCE', None)]) |
|---|
| 2154 | |
|---|
| 2155 | def ctcpQuery_USERINFO(self, user, channel, data): |
|---|
| 2156 | if data is not None: |
|---|
| 2157 | self.quirkyMessage("Why did %s send '%s' with a USERINFO query?" |
|---|
| 2158 | % (user, data)) |
|---|
| 2159 | if self.userinfo: |
|---|
| 2160 | nick = string.split(user,"!")[0] |
|---|
| 2161 | self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)]) |
|---|
| 2162 | |
|---|
| 2163 | def ctcpQuery_CLIENTINFO(self, user, channel, data): |
|---|
| 2164 | """ |
|---|
| 2165 | A master index of what CTCP tags this client knows. |
|---|
| 2166 | |
|---|
| 2167 | If no arguments are provided, respond with a list of known tags. |
|---|
| 2168 | If an argument is provided, provide human-readable help on |
|---|
| 2169 | the usage of that tag. |
|---|
| 2170 | """ |
|---|
| 2171 | |
|---|
| 2172 | nick = string.split(user,"!")[0] |
|---|
| 2173 | if not data: |
|---|
| 2174 | # XXX: prefixedMethodNames gets methods from my *class*, |
|---|
| 2175 | # but it's entirely possible that this *instance* has more |
|---|
| 2176 | # methods. |
|---|
| 2177 | names = reflect.prefixedMethodNames(self.__class__, |
|---|
| 2178 | 'ctcpQuery_') |
|---|
| 2179 | |
|---|
| 2180 | self.ctcpMakeReply(nick, [('CLIENTINFO', |
|---|
| 2181 | string.join(names, ' '))]) |
|---|
| 2182 | else: |
|---|
| 2183 | args = string.split(data) |
|---|
| 2184 | method = getattr(self, 'ctcpQuery_%s' % (args[0],), None) |
|---|
| 2185 | if not method: |
|---|
| 2186 | self.ctcpMakeReply(nick, [('ERRMSG', |
|---|
| 2187 | "CLIENTINFO %s :" |
|---|
| 2188 | "Unknown query '%s'" |
|---|
| 2189 | % (data, args[0]))]) |
|---|
| 2190 | return |
|---|
| 2191 | doc = getattr(method, '__doc__', '') |
|---|
| 2192 | self.ctcpMakeReply(nick, [('CLIENTINFO', doc)]) |
|---|
| 2193 | |
|---|
| 2194 | |
|---|
| 2195 | def ctcpQuery_ERRMSG(self, user, channel, data): |
|---|
| 2196 | # Yeah, this seems strange, but that's what the spec says to do |
|---|
| 2197 | # when faced with an ERRMSG query (not a reply). |
|---|
| 2198 | nick = string.split(user,"!")[0] |
|---|
| 2199 | self.ctcpMakeReply(nick, [('ERRMSG', |
|---|
| 2200 | "%s :No error has occoured." % data)]) |
|---|
| 2201 | |
|---|
| 2202 | def ctcpQuery_TIME(self, user, channel, data): |
|---|
| 2203 | if data is not None: |
|---|
| 2204 | self.quirkyMessage("Why did %s send '%s' with a TIME query?" |
|---|
| 2205 | % (user, data)) |
|---|
| 2206 | nick = string.split(user,"!")[0] |
|---|
| 2207 | self.ctcpMakeReply(nick, |
|---|
| 2208 | [('TIME', ':%s' % |
|---|
| 2209 | time.asctime(time.localtime(time.time())))]) |
|---|
| 2210 | |
|---|
| 2211 | def ctcpQuery_DCC(self, user, channel, data): |
|---|
| 2212 | """Initiate a Direct Client Connection |
|---|
| 2213 | """ |
|---|
| 2214 | |
|---|
| 2215 | if not data: return |
|---|
| 2216 | dcctype = data.split(None, 1)[0].upper() |
|---|
| 2217 | handler = getattr(self, "dcc_" + dcctype, None) |
|---|
| 2218 | if handler: |
|---|
| 2219 | if self.dcc_sessions is None: |
|---|
| 2220 | self.dcc_sessions = [] |
|---|
| 2221 | data = data[len(dcctype)+1:] |
|---|
| 2222 | handler(user, channel, data) |
|---|
| 2223 | else: |
|---|
| 2224 | nick = string.split(user,"!")[0] |
|---|
| 2225 | self.ctcpMakeReply(nick, [('ERRMSG', |
|---|
| 2226 | "DCC %s :Unknown DCC type '%s'" |
|---|
| 2227 | % (data, dcctype))]) |
|---|
| 2228 | self.quirkyMessage("%s offered unknown DCC type %s" |
|---|
| 2229 | % (user, dcctype)) |
|---|
| 2230 | |
|---|
| 2231 | def dcc_SEND(self, user, channel, data): |
|---|
| 2232 | # Use splitQuoted for those who send files with spaces in the names. |
|---|
| 2233 | data = text.splitQuoted(data) |
|---|
| 2234 | if len(data) < 3: |
|---|
| 2235 | raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,) |
|---|
| 2236 | |
|---|
| 2237 | (filename, address, port) = data[:3] |
|---|
| 2238 | |
|---|
| 2239 | address = dccParseAddress(address) |
|---|
| 2240 | try: |
|---|
| 2241 | port = int(port) |
|---|
| 2242 | except ValueError: |
|---|
| 2243 | raise IRCBadMessage, "Indecipherable port %r" % (port,) |
|---|
| 2244 | |
|---|
| 2245 | size = -1 |
|---|
| 2246 | if len(data) >= 4: |
|---|
| 2247 | try: |
|---|
| 2248 | size = int(data[3]) |
|---|
| 2249 | except ValueError: |
|---|
| 2250 | pass |
|---|
| 2251 | |
|---|
| 2252 | # XXX Should we bother passing this data? |
|---|
| 2253 | self.dccDoSend(user, address, port, filename, size, data) |
|---|
| 2254 | |
|---|
| 2255 | def dcc_ACCEPT(self, user, channel, data): |
|---|
| 2256 | data = text.splitQuoted(data) |
|---|
| 2257 | if len(data) < 3: |
|---|
| 2258 | raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,) |
|---|
| 2259 | (filename, port, resumePos) = data[:3] |
|---|
| 2260 | try: |
|---|
| 2261 | port = int(port) |
|---|
| 2262 | resumePos = int(resumePos) |
|---|
| 2263 | except ValueError: |
|---|
| 2264 | return |
|---|
| 2265 | |
|---|
| 2266 | self.dccDoAcceptResume(user, filename, port, resumePos) |
|---|
| 2267 | |
|---|
| 2268 | def dcc_RESUME(self, user, channel, data): |
|---|
| 2269 | data = text.splitQuoted(data) |
|---|
| 2270 | if len(data) < 3: |
|---|
| 2271 | raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,) |
|---|
| 2272 | (filename, port, resumePos) = data[:3] |
|---|
| 2273 | try: |
|---|
| 2274 | port = int(port) |
|---|
| 2275 | resumePos = int(resumePos) |
|---|
| 2276 | except ValueError: |
|---|
| 2277 | return |
|---|
| 2278 | self.dccDoResume(user, filename, port, resumePos) |
|---|
| 2279 | |
|---|
| 2280 | def dcc_CHAT(self, user, channel, data): |
|---|
| 2281 | data = text.splitQuoted(data) |
|---|
| 2282 | if len(data) < 3: |
|---|
| 2283 | raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,) |
|---|
| 2284 | |
|---|
| 2285 | (filename, address, port) = data[:3] |
|---|
| 2286 | |
|---|
| 2287 | address = dccParseAddress(address) |
|---|
| 2288 | try: |
|---|
| 2289 | port = int(port) |
|---|
| 2290 | except ValueError: |
|---|
| 2291 | raise IRCBadMessage, "Indecipherable port %r" % (port,) |
|---|
| 2292 | |
|---|
| 2293 | self.dccDoChat(user, channel, address, port, data) |
|---|
| 2294 | |
|---|
| 2295 | ### The dccDo methods are the slightly higher-level siblings of |
|---|
| 2296 | ### common dcc_ methods; the arguments have been parsed for them. |
|---|
| 2297 | |
|---|
| 2298 | def dccDoSend(self, user, address, port, fileName, size, data): |
|---|
| 2299 | """Called when I receive a DCC SEND offer from a client. |
|---|
| 2300 | |
|---|
| 2301 | By default, I do nothing here.""" |
|---|
| 2302 | ## filename = path.basename(arg) |
|---|
| 2303 | ## protocol = DccFileReceive(filename, size, |
|---|
| 2304 | ## (user,channel,data),self.dcc_destdir) |
|---|
| 2305 | ## reactor.clientTCP(address, port, protocol) |
|---|
| 2306 | ## self.dcc_sessions.append(protocol) |
|---|
| 2307 | pass |
|---|
| 2308 | |
|---|
| 2309 | def dccDoResume(self, user, file, port, resumePos): |
|---|
| 2310 | """Called when a client is trying to resume an offered file |
|---|
| 2311 | via DCC send. It should be either replied to with a DCC |
|---|
| 2312 | ACCEPT or ignored (default).""" |
|---|
| 2313 | pass |
|---|
| 2314 | |
|---|
| 2315 | def dccDoAcceptResume(self, user, file, port, resumePos): |
|---|
| 2316 | """Called when a client has verified and accepted a DCC resume |
|---|
| 2317 | request made by us. By default it will do nothing.""" |
|---|
| 2318 | pass |
|---|
| 2319 | |
|---|
| 2320 | def dccDoChat(self, user, channel, address, port, data): |
|---|
| 2321 | pass |
|---|
| 2322 | #factory = DccChatFactory(self, queryData=(user, channel, data)) |
|---|
| 2323 | #reactor.connectTCP(address, port, factory) |
|---|
| 2324 | #self.dcc_sessions.append(factory) |
|---|
| 2325 | |
|---|
| 2326 | #def ctcpQuery_SED(self, user, data): |
|---|
| 2327 | # """Simple Encryption Doodoo |
|---|
| 2328 | # |
|---|
| 2329 | # Feel free to implement this, but no specification is available. |
|---|
| 2330 | # """ |
|---|
| 2331 | # raise NotImplementedError |
|---|
| 2332 | |
|---|
| 2333 | |
|---|
| 2334 | def ctcpMakeReply(self, user, messages): |
|---|
| 2335 | """ |
|---|
| 2336 | Send one or more C{extended messages} as a CTCP reply. |
|---|
| 2337 | |
|---|
| 2338 | @type messages: a list of extended messages. An extended |
|---|
| 2339 | message is a (tag, data) tuple, where 'data' may be C{None}. |
|---|
| 2340 | """ |
|---|
| 2341 | self.notice(user, ctcpStringify(messages)) |
|---|
| 2342 | |
|---|
| 2343 | ### client CTCP query commands |
|---|
| 2344 | |
|---|
| 2345 | def ctcpMakeQuery(self, user, messages): |
|---|
| 2346 | """ |
|---|
| 2347 | Send one or more C{extended messages} as a CTCP query. |
|---|
| 2348 | |
|---|
| 2349 | @type messages: a list of extended messages. An extended |
|---|
| 2350 | message is a (tag, data) tuple, where 'data' may be C{None}. |
|---|
| 2351 | """ |
|---|
| 2352 | self.msg(user, ctcpStringify(messages)) |
|---|
| 2353 | |
|---|
| 2354 | ### Receiving a response to a CTCP query (presumably to one we made) |
|---|
| 2355 | ### You may want to add methods here, or override UnknownReply. |
|---|
| 2356 | |
|---|
| 2357 | def ctcpReply(self, user, channel, messages): |
|---|
| 2358 | """ |
|---|
| 2359 | Dispatch method for any CTCP replies received. |
|---|
| 2360 | """ |
|---|
| 2361 | for m in messages: |
|---|
| 2362 | method = getattr(self, "ctcpReply_%s" % m[0], None) |
|---|
| 2363 | if method: |
|---|
| 2364 | method(user, channel, m[1]) |
|---|
| 2365 | else: |
|---|
| 2366 | self.ctcpUnknownReply(user, channel, m[0], m[1]) |
|---|
| 2367 | |
|---|
| 2368 | def ctcpReply_PING(self, user, channel, data): |
|---|
| 2369 | nick = user.split('!', 1)[0] |
|---|
| 2370 | if (not self._pings) or (not self._pings.has_key((nick, data))): |
|---|
| 2371 | raise IRCBadMessage,\ |
|---|
| 2372 | "Bogus PING response from %s: %s" % (user, data) |
|---|
| 2373 | |
|---|
| 2374 | t0 = self._pings[(nick, data)] |
|---|
| 2375 | self.pong(user, time.time() - t0) |
|---|
| 2376 | |
|---|
| 2377 | def ctcpUnknownReply(self, user, channel, tag, data): |
|---|
| 2378 | """Called when a fitting ctcpReply_ method is not found. |
|---|
| 2379 | |
|---|
| 2380 | XXX: If the client makes arbitrary CTCP queries, |
|---|
| 2381 | this method should probably show the responses to |
|---|
| 2382 | them instead of treating them as anomolies. |
|---|
| 2383 | """ |
|---|
| 2384 | log.msg("Unknown CTCP reply from %s: %s %s\n" |
|---|
| 2385 | % (user, tag, data)) |
|---|
| 2386 | |
|---|
| 2387 | ### Error handlers |
|---|
| 2388 | ### You may override these with something more appropriate to your UI. |
|---|
| 2389 | |
|---|
| 2390 | def badMessage(self, line, excType, excValue, tb): |
|---|
| 2391 | """When I get a message that's so broken I can't use it. |
|---|
| 2392 | """ |
|---|
| 2393 | log.msg(line) |
|---|
| 2394 | log.msg(string.join(traceback.format_exception(excType, |
|---|
| 2395 | excValue, |
|---|
| 2396 | tb),'')) |
|---|
| 2397 | |
|---|
| 2398 | def quirkyMessage(self, s): |
|---|
| 2399 | """This is called when I receive a message which is peculiar, |
|---|
| 2400 | but not wholly indecipherable. |
|---|
| 2401 | """ |
|---|
| 2402 | log.msg(s + '\n') |
|---|
| 2403 | |
|---|
| 2404 | ### Protocool methods |
|---|
| 2405 | |
|---|
| 2406 | def connectionMade(self): |
|---|
| 2407 | self.supported = ServerSupportedFeatures() |
|---|
| 2408 | self._queue = [] |
|---|
| 2409 | if self.performLogin: |
|---|
| 2410 | self.register(self.nickname) |
|---|
| 2411 | |
|---|
| 2412 | def dataReceived(self, data): |
|---|
| 2413 | basic.LineReceiver.dataReceived(self, data.replace('\r', '')) |
|---|
| 2414 | |
|---|
| 2415 | def lineReceived(self, line): |
|---|
| 2416 | line = lowDequote(line) |
|---|
| 2417 | try: |
|---|
| 2418 | prefix, command, params = parsemsg(line) |
|---|
| 2419 | if numeric_to_symbolic.has_key(command): |
|---|
| 2420 | command = numeric_to_symbolic[command] |
|---|
| 2421 | self.handleCommand(command, prefix, params) |
|---|
| 2422 | except IRCBadMessage: |
|---|
| 2423 | self.badMessage(line, *sys.exc_info()) |
|---|
| 2424 | |
|---|
| 2425 | |
|---|
| 2426 | def getUserModeParams(self): |
|---|
| 2427 | """ |
|---|
| 2428 | Get user modes that require parameters for correct parsing. |
|---|
| 2429 | |
|---|
| 2430 | @rtype: C{[str, str]} |
|---|
| 2431 | @return C{[add, remove]} |
|---|
| 2432 | """ |
|---|
| 2433 | return ['', ''] |
|---|
| 2434 | |
|---|
| 2435 | |
|---|
| 2436 | def getChannelModeParams(self): |
|---|
| 2437 | """ |
|---|
| 2438 | Get channel modes that require parameters for correct parsing. |
|---|
| 2439 | |
|---|
| 2440 | @rtype: C{[str, str]} |
|---|
| 2441 | @return C{[add, remove]} |
|---|
| 2442 | """ |
|---|
| 2443 | # PREFIX modes are treated as "type B" CHANMODES, they always take |
|---|
| 2444 | # parameter. |
|---|
| 2445 | params = ['', ''] |
|---|
| 2446 | prefixes = self.supported.getFeature('PREFIX', {}) |
|---|
| 2447 | params[0] = params[1] = ''.join(prefixes.iterkeys()) |
|---|
| 2448 | |
|---|
| 2449 | chanmodes = self.supported.getFeature('CHANMODES') |
|---|
| 2450 | if chanmodes is not None: |
|---|
| 2451 | params[0] += chanmodes.get('addressModes', '') |
|---|
| 2452 | params[0] += chanmodes.get('param', '') |
|---|
| 2453 | params[1] = params[0] |
|---|
| 2454 | params[0] += chanmodes.get('setParam', '') |
|---|
| 2455 | return params |
|---|
| 2456 | |
|---|
| 2457 | |
|---|
| 2458 | def handleCommand(self, command, prefix, params): |
|---|
| 2459 | """Determine the function to call for the given command and call |
|---|
| 2460 | it with the given arguments. |
|---|
| 2461 | """ |
|---|
| 2462 | method = getattr(self, "irc_%s" % command, None) |
|---|
| 2463 | try: |
|---|
| 2464 | if method is not None: |
|---|
| 2465 | method(prefix, params) |
|---|
| 2466 | else: |
|---|
| 2467 | self.irc_unknown(prefix, command, params) |
|---|
| 2468 | except: |
|---|
| 2469 | log.deferr() |
|---|
| 2470 | |
|---|
| 2471 | |
|---|
| 2472 | def __getstate__(self): |
|---|
| 2473 | dct = self.__dict__.copy() |
|---|
| 2474 | dct['dcc_sessions'] = None |
|---|
| 2475 | dct['_pings'] = None |
|---|
| 2476 | return dct |
|---|
| 2477 | |
|---|
| 2478 | |
|---|
| 2479 | def dccParseAddress(address): |
|---|
| 2480 | if '.' in address: |
|---|
| 2481 | pass |
|---|
| 2482 | else: |
|---|
| 2483 | try: |
|---|
| 2484 | address = long(address) |
|---|
| 2485 | except ValueError: |
|---|
| 2486 | raise IRCBadMessage,\ |
|---|
| 2487 | "Indecipherable address %r" % (address,) |
|---|
| 2488 | else: |
|---|
| 2489 | address = ( |
|---|
| 2490 | (address >> 24) & 0xFF, |
|---|
| 2491 | (address >> 16) & 0xFF, |
|---|
| 2492 | (address >> 8) & 0xFF, |
|---|
| 2493 | address & 0xFF, |
|---|
| 2494 | ) |
|---|
| 2495 | address = '.'.join(map(str,address)) |
|---|
| 2496 | return address |
|---|
| 2497 | |
|---|
| 2498 | |
|---|
| 2499 | class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral): |
|---|
| 2500 | """Bare protocol to receive a Direct Client Connection SEND stream. |
|---|
| 2501 | |
|---|
| 2502 | This does enough to keep the other guy talking, but you'll want to |
|---|
| 2503 | extend my dataReceived method to *do* something with the data I get. |
|---|
| 2504 | """ |
|---|
| 2505 | |
|---|
| 2506 | bytesReceived = 0 |
|---|
| 2507 | |
|---|
| 2508 | def __init__(self, resumeOffset=0): |
|---|
| 2509 | self.bytesReceived = resumeOffset |
|---|
| 2510 | self.resume = (resumeOffset != 0) |
|---|
| 2511 | |
|---|
| 2512 | def dataReceived(self, data): |
|---|
| 2513 | """Called when data is received. |
|---|
| 2514 | |
|---|
| 2515 | Warning: This just acknowledges to the remote host that the |
|---|
| 2516 | data has been received; it doesn't *do* anything with the |
|---|
| 2517 | data, so you'll want to override this. |
|---|
| 2518 | """ |
|---|
| 2519 | self.bytesReceived = self.bytesReceived + len(data) |
|---|
| 2520 | self.transport.write(struct.pack('!i', self.bytesReceived)) |
|---|
| 2521 | |
|---|
| 2522 | |
|---|
| 2523 | class DccSendProtocol(protocol.Protocol, styles.Ephemeral): |
|---|
| 2524 | """Protocol for an outgoing Direct Client Connection SEND. |
|---|
| 2525 | """ |
|---|
| 2526 | |
|---|
| 2527 | blocksize = 1024 |
|---|
| 2528 | file = None |
|---|
| 2529 | bytesSent = 0 |
|---|
| 2530 | completed = 0 |
|---|
| 2531 | connected = 0 |
|---|
| 2532 | |
|---|
| 2533 | def __init__(self, file): |
|---|
| 2534 | if type(file) is types.StringType: |
|---|
| 2535 | self.file = open(file, 'r') |
|---|
| 2536 | |
|---|
| 2537 | def connectionMade(self): |
|---|
| 2538 | self.connected = 1 |
|---|
| 2539 | self.sendBlock() |
|---|
| 2540 | |
|---|
| 2541 | def dataReceived(self, data): |
|---|
| 2542 | # XXX: Do we need to check to see if len(data) != fmtsize? |
|---|
| 2543 | |
|---|
| 2544 | bytesShesGot = struct.unpack("!I", data) |
|---|
| 2545 | if bytesShesGot < self.bytesSent: |
|---|
| 2546 | # Wait for her. |
|---|
| 2547 | # XXX? Add some checks to see if we've stalled out? |
|---|
| 2548 | return |
|---|
| 2549 | elif bytesShesGot > self.bytesSent: |
|---|
| 2550 | # self.transport.log("DCC SEND %s: She says she has %d bytes " |
|---|
| 2551 | # "but I've only sent %d. I'm stopping " |
|---|
| 2552 | # "this screwy transfer." |
|---|
| 2553 | # % (self.file, |
|---|
| 2554 | # bytesShesGot, self.bytesSent)) |
|---|
| 2555 | self.transport.loseConnection() |
|---|
| 2556 | return |
|---|
| 2557 | |
|---|
| 2558 | self.sendBlock() |
|---|
| 2559 | |
|---|
| 2560 | def sendBlock(self): |
|---|
| 2561 | block = self.file.read(self.blocksize) |
|---|
| 2562 | if block: |
|---|
| 2563 | self.transport.write(block) |
|---|
| 2564 | self.bytesSent = self.bytesSent + len(block) |
|---|
| 2565 | else: |
|---|
| 2566 | # Nothing more to send, transfer complete. |
|---|
| 2567 | self.transport.loseConnection() |
|---|
| 2568 | self.completed = 1 |
|---|
| 2569 | |
|---|
| 2570 | def connectionLost(self, reason): |
|---|
| 2571 | self.connected = 0 |
|---|
| 2572 | if hasattr(self.file, "close"): |
|---|
| 2573 | self.file.close() |
|---|
| 2574 | |
|---|
| 2575 | |
|---|
| 2576 | class DccSendFactory(protocol.Factory): |
|---|
| 2577 | protocol = DccSendProtocol |
|---|
| 2578 | def __init__(self, file): |
|---|
| 2579 | self.file = file |
|---|
| 2580 | |
|---|
| 2581 | def buildProtocol(self, connection): |
|---|
| 2582 | p = self.protocol(self.file) |
|---|
| 2583 | p.factory = self |
|---|
| 2584 | return p |
|---|
| 2585 | |
|---|
| 2586 | |
|---|
| 2587 | def fileSize(file): |
|---|
| 2588 | """I'll try my damndest to determine the size of this file object. |
|---|
| 2589 | """ |
|---|
| 2590 | size = None |
|---|
| 2591 | if hasattr(file, "fileno"): |
|---|
| 2592 | fileno = file.fileno() |
|---|
| 2593 | try: |
|---|
| 2594 | stat_ = os.fstat(fileno) |
|---|
| 2595 | size = stat_[stat.ST_SIZE] |
|---|
| 2596 | except: |
|---|
| 2597 | pass |
|---|
| 2598 | else: |
|---|
| 2599 | return size |
|---|
| 2600 | |
|---|
| 2601 | if hasattr(file, "name") and path.exists(file.name): |
|---|
| 2602 | try: |
|---|
| 2603 | size = path.getsize(file.name) |
|---|
| 2604 | except: |
|---|
| 2605 | pass |
|---|
| 2606 | else: |
|---|
| 2607 | return size |
|---|
| 2608 | |
|---|
| 2609 | if hasattr(file, "seek") and hasattr(file, "tell"): |
|---|
| 2610 | try: |
|---|
| 2611 | try: |
|---|
| 2612 | file.seek(0, 2) |
|---|
| 2613 | size = file.tell() |
|---|
| 2614 | finally: |
|---|
| 2615 | file.seek(0, 0) |
|---|
| 2616 | except: |
|---|
| 2617 | pass |
|---|
| 2618 | else: |
|---|
| 2619 | return size |
|---|
| 2620 | |
|---|
| 2621 | return size |
|---|
| 2622 | |
|---|
| 2623 | class DccChat(basic.LineReceiver, styles.Ephemeral): |
|---|
| 2624 | """Direct Client Connection protocol type CHAT. |
|---|
| 2625 | |
|---|
| 2626 | DCC CHAT is really just your run o' the mill basic.LineReceiver |
|---|
| 2627 | protocol. This class only varies from that slightly, accepting |
|---|
| 2628 | either LF or CR LF for a line delimeter for incoming messages |
|---|
| 2629 | while always using CR LF for outgoing. |
|---|
| 2630 | |
|---|
| 2631 | The lineReceived method implemented here uses the DCC connection's |
|---|
| 2632 | 'client' attribute (provided upon construction) to deliver incoming |
|---|
| 2633 | lines from the DCC chat via IRCClient's normal privmsg interface. |
|---|
| 2634 | That's something of a spoof, which you may well want to override. |
|---|
| 2635 | """ |
|---|
| 2636 | |
|---|
| 2637 | queryData = None |
|---|
| 2638 | delimiter = CR + NL |
|---|
| 2639 | client = None |
|---|
| 2640 | remoteParty = None |
|---|
| 2641 | buffer = "" |
|---|
| 2642 | |
|---|
| 2643 | def __init__(self, client, queryData=None): |
|---|
| 2644 | """Initialize a new DCC CHAT session. |
|---|
| 2645 | |
|---|
| 2646 | queryData is a 3-tuple of |
|---|
| 2647 | (fromUser, targetUserOrChannel, data) |
|---|
| 2648 | as received by the CTCP query. |
|---|
| 2649 | |
|---|
| 2650 | (To be honest, fromUser is the only thing that's currently |
|---|
| 2651 | used here. targetUserOrChannel is potentially useful, while |
|---|
| 2652 | the 'data' argument is soley for informational purposes.) |
|---|
| 2653 | """ |
|---|
| 2654 | self.client = client |
|---|
| 2655 | if queryData: |
|---|
| 2656 | self.queryData = queryData |
|---|
| 2657 | self.remoteParty = self.queryData[0] |
|---|
| 2658 | |
|---|
| 2659 | def dataReceived(self, data): |
|---|
| 2660 | self.buffer = self.buffer + data |
|---|
| 2661 | lines = string.split(self.buffer, LF) |
|---|
| 2662 | # Put the (possibly empty) element after the last LF back in the |
|---|
| 2663 | # buffer |
|---|
| 2664 | self.buffer = lines.pop() |
|---|
| 2665 | |
|---|
| 2666 | for line in lines: |
|---|
| 2667 | if line[-1] == CR: |
|---|
| 2668 | line = line[:-1] |
|---|
| 2669 | self.lineReceived(line) |
|---|
| 2670 | |
|---|
| 2671 | def lineReceived(self, line): |
|---|
| 2672 | log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line)) |
|---|
| 2673 | self.client.privmsg(self.remoteParty, |
|---|
| 2674 | self.client.nickname, line) |
|---|
| 2675 | |
|---|
| 2676 | |
|---|
| 2677 | class DccChatFactory(protocol.ClientFactory): |
|---|
| 2678 | protocol = DccChat |
|---|
| 2679 | noisy = 0 |
|---|
| 2680 | def __init__(self, client, queryData): |
|---|
| 2681 | self.client = client |
|---|
| 2682 | self.queryData = queryData |
|---|
| 2683 | |
|---|
| 2684 | |
|---|
| 2685 | def buildProtocol(self, addr): |
|---|
| 2686 | p = self.protocol(client=self.client, queryData=self.queryData) |
|---|
| 2687 | p.factory = self |
|---|
| 2688 | return p |
|---|
| 2689 | |
|---|
| 2690 | |
|---|
| 2691 | def clientConnectionFailed(self, unused_connector, unused_reason): |
|---|
| 2692 | self.client.dcc_sessions.remove(self) |
|---|
| 2693 | |
|---|
| 2694 | def clientConnectionLost(self, unused_connector, unused_reason): |
|---|
| 2695 | self.client.dcc_sessions.remove(self) |
|---|
| 2696 | |
|---|
| 2697 | |
|---|
| 2698 | def dccDescribe(data): |
|---|
| 2699 | """Given the data chunk from a DCC query, return a descriptive string. |
|---|
| 2700 | """ |
|---|
| 2701 | |
|---|
| 2702 | orig_data = data |
|---|
| 2703 | data = string.split(data) |
|---|
| 2704 | if len(data) < 4: |
|---|
| 2705 | return orig_data |
|---|
| 2706 | |
|---|
| 2707 | (dcctype, arg, address, port) = data[:4] |
|---|
| 2708 | |
|---|
| 2709 | if '.' in address: |
|---|
| 2710 | pass |
|---|
| 2711 | else: |
|---|
| 2712 | try: |
|---|
| 2713 | address = long(address) |
|---|
| 2714 | except ValueError: |
|---|
| 2715 | pass |
|---|
| 2716 | else: |
|---|
| 2717 | address = ( |
|---|
| 2718 | (address >> 24) & 0xFF, |
|---|
| 2719 | (address >> 16) & 0xFF, |
|---|
| 2720 | (address >> 8) & 0xFF, |
|---|
| 2721 | address & 0xFF, |
|---|
| 2722 | ) |
|---|
| 2723 | # The mapping to 'int' is to get rid of those accursed |
|---|
| 2724 | # "L"s which python 1.5.2 puts on the end of longs. |
|---|
| 2725 | address = string.join(map(str,map(int,address)), ".") |
|---|
| 2726 | |
|---|
| 2727 | if dcctype == 'SEND': |
|---|
| 2728 | filename = arg |
|---|
| 2729 | |
|---|
| 2730 | size_txt = '' |
|---|
| 2731 | if len(data) >= 5: |
|---|
| 2732 | try: |
|---|
| 2733 | size = int(data[4]) |
|---|
| 2734 | size_txt = ' of size %d bytes' % (size,) |
|---|
| 2735 | except ValueError: |
|---|
| 2736 | pass |
|---|
| 2737 | |
|---|
| 2738 | dcc_text = ("SEND for file '%s'%s at host %s, port %s" |
|---|
| 2739 | % (filename, size_txt, address, port)) |
|---|
| 2740 | elif dcctype == 'CHAT': |
|---|
| 2741 | dcc_text = ("CHAT for host %s, port %s" |
|---|
| 2742 | % (address, port)) |
|---|
| 2743 | else: |
|---|
| 2744 | dcc_text = orig_data |
|---|
| 2745 | |
|---|
| 2746 | return dcc_text |
|---|
| 2747 | |
|---|
| 2748 | |
|---|
| 2749 | class DccFileReceive(DccFileReceiveBasic): |
|---|
| 2750 | """Higher-level coverage for getting a file from DCC SEND. |
|---|
| 2751 | |
|---|
| 2752 | I allow you to change the file's name and destination directory. |
|---|
| 2753 | I won't overwrite an existing file unless I've been told it's okay |
|---|
| 2754 | to do so. If passed the resumeOffset keyword argument I will attempt to |
|---|
| 2755 | resume the file from that amount of bytes. |
|---|
| 2756 | |
|---|
| 2757 | XXX: I need to let the client know when I am finished. |
|---|
| 2758 | XXX: I need to decide how to keep a progress indicator updated. |
|---|
| 2759 | XXX: Client needs a way to tell me "Do not finish until I say so." |
|---|
| 2760 | XXX: I need to make sure the client understands if the file cannot be written. |
|---|
| 2761 | """ |
|---|
| 2762 | |
|---|
| 2763 | filename = 'dcc' |
|---|
| 2764 | fileSize = -1 |
|---|
| 2765 | destDir = '.' |
|---|
| 2766 | overwrite = 0 |
|---|
| 2767 | fromUser = None |
|---|
| 2768 | queryData = None |
|---|
| 2769 | |
|---|
| 2770 | def __init__(self, filename, fileSize=-1, queryData=None, |
|---|
| 2771 | destDir='.', resumeOffset=0): |
|---|
| 2772 | DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset) |
|---|
| 2773 | self.filename = filename |
|---|
| 2774 | self.destDir = destDir |
|---|
| 2775 | self.fileSize = fileSize |
|---|
| 2776 | |
|---|
| 2777 | if queryData: |
|---|
| 2778 | self.queryData = queryData |
|---|
| 2779 | self.fromUser = self.queryData[0] |
|---|
| 2780 | |
|---|
| 2781 | def set_directory(self, directory): |
|---|
| 2782 | """Set the directory where the downloaded file will be placed. |
|---|
| 2783 | |
|---|
| 2784 | May raise OSError if the supplied directory path is not suitable. |
|---|
| 2785 | """ |
|---|
| 2786 | if not path.exists(directory): |
|---|
| 2787 | raise OSError(errno.ENOENT, "You see no directory there.", |
|---|
| 2788 | directory) |
|---|
| 2789 | if not path.isdir(directory): |
|---|
| 2790 | raise OSError(errno.ENOTDIR, "You cannot put a file into " |
|---|
| 2791 | "something which is not a directory.", |
|---|
| 2792 | directory) |
|---|
| 2793 | if not os.access(directory, os.X_OK | os.W_OK): |
|---|
| 2794 | raise OSError(errno.EACCES, |
|---|
| 2795 | "This directory is too hard to write in to.", |
|---|
| 2796 | directory) |
|---|
| 2797 | self.destDir = directory |
|---|
| 2798 | |
|---|
| 2799 | def set_filename(self, filename): |
|---|
| 2800 | """Change the name of the file being transferred. |
|---|
| 2801 | |
|---|
| 2802 | This replaces the file name provided by the sender. |
|---|
| 2803 | """ |
|---|
| 2804 | self.filename = filename |
|---|
| 2805 | |
|---|
| 2806 | def set_overwrite(self, boolean): |
|---|
| 2807 | """May I overwrite existing files? |
|---|
| 2808 | """ |
|---|
| 2809 | self.overwrite = boolean |
|---|
| 2810 | |
|---|
| 2811 | |
|---|
| 2812 | # Protocol-level methods. |
|---|
| 2813 | |
|---|
| 2814 | def connectionMade(self): |
|---|
| 2815 | dst = path.abspath(path.join(self.destDir,self.filename)) |
|---|
| 2816 | exists = path.exists(dst) |
|---|
| 2817 | if self.resume and exists: |
|---|
| 2818 | # I have been told I want to resume, and a file already |
|---|
| 2819 | # exists - Here we go |
|---|
| 2820 | self.file = open(dst, 'ab') |
|---|
| 2821 | log.msg("Attempting to resume %s - starting from %d bytes" % |
|---|
| 2822 | (self.file, self.file.tell())) |
|---|
| 2823 | elif self.overwrite or not exists: |
|---|
| 2824 | self.file = open(dst, 'wb') |
|---|
| 2825 | else: |
|---|
| 2826 | raise OSError(errno.EEXIST, |
|---|
| 2827 | "There's a file in the way. " |
|---|
| 2828 | "Perhaps that's why you cannot open it.", |
|---|
| 2829 | dst) |
|---|
| 2830 | |
|---|
| 2831 | def dataReceived(self, data): |
|---|
| 2832 | self.file.write(data) |
|---|
| 2833 | DccFileReceiveBasic.dataReceived(self, data) |
|---|
| 2834 | |
|---|
| 2835 | # XXX: update a progress indicator here? |
|---|
| 2836 | |
|---|
| 2837 | def connectionLost(self, reason): |
|---|
| 2838 | """When the connection is lost, I close the file. |
|---|
| 2839 | """ |
|---|
| 2840 | self.connected = 0 |
|---|
| 2841 | logmsg = ("%s closed." % (self,)) |
|---|
| 2842 | if self.fileSize > 0: |
|---|
| 2843 | logmsg = ("%s %d/%d bytes received" |
|---|
| 2844 | % (logmsg, self.bytesReceived, self.fileSize)) |
|---|
| 2845 | if self.bytesReceived == self.fileSize: |
|---|
| 2846 | pass # Hooray! |
|---|
| 2847 | elif self.bytesReceived < self.fileSize: |
|---|
| 2848 | logmsg = ("%s (Warning: %d bytes short)" |
|---|
| 2849 | % (logmsg, self.fileSize - self.bytesReceived)) |
|---|
| 2850 | else: |
|---|
| 2851 | logmsg = ("%s (file larger than expected)" |
|---|
| 2852 | % (logmsg,)) |
|---|
| 2853 | else: |
|---|
| 2854 | logmsg = ("%s %d bytes received" |
|---|
| 2855 | % (logmsg, self.bytesReceived)) |
|---|
| 2856 | |
|---|
| 2857 | if hasattr(self, 'file'): |
|---|
| 2858 | logmsg = "%s and written to %s.\n" % (logmsg, self.file.name) |
|---|
| 2859 | if hasattr(self.file, 'close'): self.file.close() |
|---|
| 2860 | |
|---|
| 2861 | # self.transport.log(logmsg) |
|---|
| 2862 | |
|---|
| 2863 | def __str__(self): |
|---|
| 2864 | if not self.connected: |
|---|
| 2865 | return "<Unconnected DccFileReceive object at %x>" % (id(self),) |
|---|
| 2866 | from_ = self.transport.getPeer() |
|---|
| 2867 | if self.fromUser: |
|---|
| 2868 | from_ = "%s (%s)" % (self.fromUser, from_) |
|---|
| 2869 | |
|---|
| 2870 | s = ("DCC transfer of '%s' from %s" % (self.filename, from_)) |
|---|
| 2871 | return s |
|---|
| 2872 | |
|---|
| 2873 | def __repr__(self): |
|---|
| 2874 | s = ("<%s at %x: GET %s>" |
|---|
| 2875 | % (self.__class__, id(self), self.filename)) |
|---|
| 2876 | return s |
|---|
| 2877 | |
|---|
| 2878 | |
|---|
| 2879 | # CTCP constants and helper functions |
|---|
| 2880 | |
|---|
| 2881 | X_DELIM = chr(001) |
|---|
| 2882 | |
|---|
| 2883 | def ctcpExtract(message): |
|---|
| 2884 | """ |
|---|
| 2885 | Extract CTCP data from a string. |
|---|
| 2886 | |
|---|
| 2887 | @return: A C{dict} containing two keys: |
|---|
| 2888 | - C{'extended'}: A list of CTCP (tag, data) tuples. |
|---|
| 2889 | - C{'normal'}: A list of strings which were not inside a CTCP delimiter. |
|---|
| 2890 | """ |
|---|
| 2891 | extended_messages = [] |
|---|
| 2892 | normal_messages = [] |
|---|
| 2893 | retval = {'extended': extended_messages, |
|---|
| 2894 | 'normal': normal_messages } |
|---|
| 2895 | |
|---|
| 2896 | messages = string.split(message, X_DELIM) |
|---|
| 2897 | odd = 0 |
|---|
| 2898 | |
|---|
| 2899 | # X1 extended data X2 nomal data X3 extended data X4 normal... |
|---|
| 2900 | while messages: |
|---|
| 2901 | if odd: |
|---|
| 2902 | extended_messages.append(messages.pop(0)) |
|---|
| 2903 | else: |
|---|
| 2904 | normal_messages.append(messages.pop(0)) |
|---|
| 2905 | odd = not odd |
|---|
| 2906 | |
|---|
| 2907 | extended_messages[:] = filter(None, extended_messages) |
|---|
| 2908 | normal_messages[:] = filter(None, normal_messages) |
|---|
| 2909 | |
|---|
| 2910 | extended_messages[:] = map(ctcpDequote, extended_messages) |
|---|
| 2911 | for i in xrange(len(extended_messages)): |
|---|
| 2912 | m = string.split(extended_messages[i], SPC, 1) |
|---|
| 2913 | tag = m[0] |
|---|
| 2914 | if len(m) > 1: |
|---|
| 2915 | data = m[1] |
|---|
| 2916 | else: |
|---|
| 2917 | data = None |
|---|
| 2918 | |
|---|
| 2919 | extended_messages[i] = (tag, data) |
|---|
| 2920 | |
|---|
| 2921 | return retval |
|---|
| 2922 | |
|---|
| 2923 | # CTCP escaping |
|---|
| 2924 | |
|---|
| 2925 | M_QUOTE= chr(020) |
|---|
| 2926 | |
|---|
| 2927 | mQuoteTable = { |
|---|
| 2928 | NUL: M_QUOTE + '0', |
|---|
| 2929 | NL: M_QUOTE + 'n', |
|---|
| 2930 | CR: M_QUOTE + 'r', |
|---|
| 2931 | M_QUOTE: M_QUOTE + M_QUOTE |
|---|
| 2932 | } |
|---|
| 2933 | |
|---|
| 2934 | mDequoteTable = {} |
|---|
| 2935 | for k, v in mQuoteTable.items(): |
|---|
| 2936 | mDequoteTable[v[-1]] = k |
|---|
| 2937 | del k, v |
|---|
| 2938 | |
|---|
| 2939 | mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL) |
|---|
| 2940 | |
|---|
| 2941 | def lowQuote(s): |
|---|
| 2942 | for c in (M_QUOTE, NUL, NL, CR): |
|---|
| 2943 | s = string.replace(s, c, mQuoteTable[c]) |
|---|
| 2944 | return s |
|---|
| 2945 | |
|---|
| 2946 | def lowDequote(s): |
|---|
| 2947 | def sub(matchobj, mDequoteTable=mDequoteTable): |
|---|
| 2948 | s = matchobj.group()[1] |
|---|
| 2949 | try: |
|---|
| 2950 | s = mDequoteTable[s] |
|---|
| 2951 | except KeyError: |
|---|
| 2952 | s = s |
|---|
| 2953 | return s |
|---|
| 2954 | |
|---|
| 2955 | return mEscape_re.sub(sub, s) |
|---|
| 2956 | |
|---|
| 2957 | X_QUOTE = '\\' |
|---|
| 2958 | |
|---|
| 2959 | xQuoteTable = { |
|---|
| 2960 | X_DELIM: X_QUOTE + 'a', |
|---|
| 2961 | X_QUOTE: X_QUOTE + X_QUOTE |
|---|
| 2962 | } |
|---|
| 2963 | |
|---|
| 2964 | xDequoteTable = {} |
|---|
| 2965 | |
|---|
| 2966 | for k, v in xQuoteTable.items(): |
|---|
| 2967 | xDequoteTable[v[-1]] = k |
|---|
| 2968 | |
|---|
| 2969 | xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL) |
|---|
| 2970 | |
|---|
| 2971 | def ctcpQuote(s): |
|---|
| 2972 | for c in (X_QUOTE, X_DELIM): |
|---|
| 2973 | s = string.replace(s, c, xQuoteTable[c]) |
|---|
| 2974 | return s |
|---|
| 2975 | |
|---|
| 2976 | def ctcpDequote(s): |
|---|
| 2977 | def sub(matchobj, xDequoteTable=xDequoteTable): |
|---|
| 2978 | s = matchobj.group()[1] |
|---|
| 2979 | try: |
|---|
| 2980 | s = xDequoteTable[s] |
|---|
| 2981 | except KeyError: |
|---|
| 2982 | s = s |
|---|
| 2983 | return s |
|---|
| 2984 | |
|---|
| 2985 | return xEscape_re.sub(sub, s) |
|---|
| 2986 | |
|---|
| 2987 | def ctcpStringify(messages): |
|---|
| 2988 | """ |
|---|
| 2989 | @type messages: a list of extended messages. An extended |
|---|
| 2990 | message is a (tag, data) tuple, where 'data' may be C{None}, a |
|---|
| 2991 | string, or a list of strings to be joined with whitespace. |
|---|
| 2992 | |
|---|
| 2993 | @returns: String |
|---|
| 2994 | """ |
|---|
| 2995 | coded_messages = [] |
|---|
| 2996 | for (tag, data) in messages: |
|---|
| 2997 | if data: |
|---|
| 2998 | if not isinstance(data, types.StringType): |
|---|
| 2999 | try: |
|---|
| 3000 | # data as list-of-strings |
|---|
| 3001 | data = " ".join(map(str, data)) |
|---|
| 3002 | except TypeError: |
|---|
| 3003 | # No? Then use it's %s representation. |
|---|
| 3004 | pass |
|---|
| 3005 | m = "%s %s" % (tag, data) |
|---|
| 3006 | else: |
|---|
| 3007 | m = str(tag) |
|---|
| 3008 | m = ctcpQuote(m) |
|---|
| 3009 | m = "%s%s%s" % (X_DELIM, m, X_DELIM) |
|---|
| 3010 | coded_messages.append(m) |
|---|
| 3011 | |
|---|
| 3012 | line = string.join(coded_messages, '') |
|---|
| 3013 | return line |
|---|
| 3014 | |
|---|
| 3015 | |
|---|
| 3016 | # Constants (from RFC 2812) |
|---|
| 3017 | RPL_WELCOME = '001' |
|---|
| 3018 | RPL_YOURHOST = '002' |
|---|
| 3019 | RPL_CREATED = '003' |
|---|
| 3020 | RPL_MYINFO = '004' |
|---|
| 3021 | RPL_ISUPPORT = '005' |
|---|
| 3022 | RPL_BOUNCE = '010' |
|---|
| 3023 | RPL_USERHOST = '302' |
|---|
| 3024 | RPL_ISON = '303' |
|---|
| 3025 | RPL_AWAY = '301' |
|---|
| 3026 | RPL_UNAWAY = '305' |
|---|
| 3027 | RPL_NOWAWAY = '306' |
|---|
| 3028 | RPL_WHOISUSER = '311' |
|---|
| 3029 | RPL_WHOISSERVER = '312' |
|---|
| 3030 | RPL_WHOISOPERATOR = '313' |
|---|
| 3031 | RPL_WHOISIDLE = '317' |
|---|
| 3032 | RPL_ENDOFWHOIS = '318' |
|---|
| 3033 | RPL_WHOISCHANNELS = '319' |
|---|
| 3034 | RPL_WHOWASUSER = '314' |
|---|
| 3035 | RPL_ENDOFWHOWAS = '369' |
|---|
| 3036 | RPL_LISTSTART = '321' |
|---|
| 3037 | RPL_LIST = '322' |
|---|
| 3038 | RPL_LISTEND = '323' |
|---|
| 3039 | RPL_UNIQOPIS = '325' |
|---|
| 3040 | RPL_CHANNELMODEIS = '324' |
|---|
| 3041 | RPL_NOTOPIC = '331' |
|---|
| 3042 | RPL_TOPIC = '332' |
|---|
| 3043 | RPL_INVITING = '341' |
|---|
| 3044 | RPL_SUMMONING = '342' |
|---|
| 3045 | RPL_INVITELIST = '346' |
|---|
| 3046 | RPL_ENDOFINVITELIST = '347' |
|---|
| 3047 | RPL_EXCEPTLIST = '348' |
|---|
| 3048 | RPL_ENDOFEXCEPTLIST = '349' |
|---|
| 3049 | RPL_VERSION = '351' |
|---|
| 3050 | RPL_WHOREPLY = '352' |
|---|
| 3051 | RPL_ENDOFWHO = '315' |
|---|
| 3052 | RPL_NAMREPLY = '353' |
|---|
| 3053 | RPL_ENDOFNAMES = '366' |
|---|
| 3054 | RPL_LINKS = '364' |
|---|
| 3055 | RPL_ENDOFLINKS = '365' |
|---|
| 3056 | RPL_BANLIST = '367' |
|---|
| 3057 | RPL_ENDOFBANLIST = '368' |
|---|
| 3058 | RPL_INFO = '371' |
|---|
| 3059 | RPL_ENDOFINFO = '374' |
|---|
| 3060 | RPL_MOTDSTART = '375' |
|---|
| 3061 | RPL_MOTD = '372' |
|---|
| 3062 | RPL_ENDOFMOTD = '376' |
|---|
| 3063 | RPL_YOUREOPER = '381' |
|---|
| 3064 | RPL_REHASHING = '382' |
|---|
| 3065 | RPL_YOURESERVICE = '383' |
|---|
| 3066 | RPL_TIME = '391' |
|---|
| 3067 | RPL_USERSSTART = '392' |
|---|
| 3068 | RPL_USERS = '393' |
|---|
| 3069 | RPL_ENDOFUSERS = '394' |
|---|
| 3070 | RPL_NOUSERS = '395' |
|---|
| 3071 | RPL_TRACELINK = '200' |
|---|
| 3072 | RPL_TRACECONNECTING = '201' |
|---|
| 3073 | RPL_TRACEHANDSHAKE = '202' |
|---|
| 3074 | RPL_TRACEUNKNOWN = '203' |
|---|
| 3075 | RPL_TRACEOPERATOR = '204' |
|---|
| 3076 | RPL_TRACEUSER = '205' |
|---|
| 3077 | RPL_TRACESERVER = '206' |
|---|
| 3078 | RPL_TRACESERVICE = '207' |
|---|
| 3079 | RPL_TRACENEWTYPE = '208' |
|---|
| 3080 | RPL_TRACECLASS = '209' |
|---|
| 3081 | RPL_TRACERECONNECT = '210' |
|---|
| 3082 | RPL_TRACELOG = '261' |
|---|
| 3083 | RPL_TRACEEND = '262' |
|---|
| 3084 | RPL_STATSLINKINFO = '211' |
|---|
| 3085 | RPL_STATSCOMMANDS = '212' |
|---|
| 3086 | RPL_ENDOFSTATS = '219' |
|---|
| 3087 | RPL_STATSUPTIME = '242' |
|---|
| 3088 | RPL_STATSOLINE = '243' |
|---|
| 3089 | RPL_UMODEIS = '221' |
|---|
| 3090 | RPL_SERVLIST = '234' |
|---|
| 3091 | RPL_SERVLISTEND = '235' |
|---|
| 3092 | RPL_LUSERCLIENT = '251' |
|---|
| 3093 | RPL_LUSEROP = '252' |
|---|
| 3094 | RPL_LUSERUNKNOWN = '253' |
|---|
| 3095 | RPL_LUSERCHANNELS = '254' |
|---|
| 3096 | RPL_LUSERME = '255' |
|---|
| 3097 | RPL_ADMINME = '256' |
|---|
| 3098 | RPL_ADMINLOC = '257' |
|---|
| 3099 | RPL_ADMINLOC = '258' |
|---|
| 3100 | RPL_ADMINEMAIL = '259' |
|---|
| 3101 | RPL_TRYAGAIN = '263' |
|---|
| 3102 | ERR_NOSUCHNICK = '401' |
|---|
| 3103 | ERR_NOSUCHSERVER = '402' |
|---|
| 3104 | ERR_NOSUCHCHANNEL = '403' |
|---|
| 3105 | ERR_CANNOTSENDTOCHAN = '404' |
|---|
| 3106 | ERR_TOOMANYCHANNELS = '405' |
|---|
| 3107 | ERR_WASNOSUCHNICK = '406' |
|---|
| 3108 | ERR_TOOMANYTARGETS = '407' |
|---|
| 3109 | ERR_NOSUCHSERVICE = '408' |
|---|
| 3110 | ERR_NOORIGIN = '409' |
|---|
| 3111 | ERR_NORECIPIENT = '411' |
|---|
| 3112 | ERR_NOTEXTTOSEND = '412' |
|---|
| 3113 | ERR_NOTOPLEVEL = '413' |
|---|
| 3114 | ERR_WILDTOPLEVEL = '414' |
|---|
| 3115 | ERR_BADMASK = '415' |
|---|
| 3116 | ERR_UNKNOWNCOMMAND = '421' |
|---|
| 3117 | ERR_NOMOTD = '422' |
|---|
| 3118 | ERR_NOADMININFO = '423' |
|---|
| 3119 | ERR_FILEERROR = '424' |
|---|
| 3120 | ERR_NONICKNAMEGIVEN = '431' |
|---|
| 3121 | ERR_ERRONEUSNICKNAME = '432' |
|---|
| 3122 | ERR_NICKNAMEINUSE = '433' |
|---|
| 3123 | ERR_NICKCOLLISION = '436' |
|---|
| 3124 | ERR_UNAVAILRESOURCE = '437' |
|---|
| 3125 | ERR_USERNOTINCHANNEL = '441' |
|---|
| 3126 | ERR_NOTONCHANNEL = '442' |
|---|
| 3127 | ERR_USERONCHANNEL = '443' |
|---|
| 3128 | ERR_NOLOGIN = '444' |
|---|
| 3129 | ERR_SUMMONDISABLED = '445' |
|---|
| 3130 | ERR_USERSDISABLED = '446' |
|---|
| 3131 | ERR_NOTREGISTERED = '451' |
|---|
| 3132 | ERR_NEEDMOREPARAMS = '461' |
|---|
| 3133 | ERR_ALREADYREGISTRED = '462' |
|---|
| 3134 | ERR_NOPERMFORHOST = '463' |
|---|
| 3135 | ERR_PASSWDMISMATCH = '464' |
|---|
| 3136 | ERR_YOUREBANNEDCREEP = '465' |
|---|
| 3137 | ERR_YOUWILLBEBANNED = '466' |
|---|
| 3138 | ERR_KEYSET = '467' |
|---|
| 3139 | ERR_CHANNELISFULL = '471' |
|---|
| 3140 | ERR_UNKNOWNMODE = '472' |
|---|
| 3141 | ERR_INVITEONLYCHAN = '473' |
|---|
| 3142 | ERR_BANNEDFROMCHAN = '474' |
|---|
| 3143 | ERR_BADCHANNELKEY = '475' |
|---|
| 3144 | ERR_BADCHANMASK = '476' |
|---|
| 3145 | ERR_NOCHANMODES = '477' |
|---|
| 3146 | ERR_BANLISTFULL = '478' |
|---|
| 3147 | ERR_NOPRIVILEGES = '481' |
|---|
| 3148 | ERR_CHANOPRIVSNEEDED = '482' |
|---|
| 3149 | ERR_CANTKILLSERVER = '483' |
|---|
| 3150 | ERR_RESTRICTED = '484' |
|---|
| 3151 | ERR_UNIQOPPRIVSNEEDED = '485' |
|---|
| 3152 | ERR_NOOPERHOST = '491' |
|---|
| 3153 | ERR_NOSERVICEHOST = '492' |
|---|
| 3154 | ERR_UMODEUNKNOWNFLAG = '501' |
|---|
| 3155 | ERR_USERSDONTMATCH = '502' |
|---|
| 3156 | |
|---|
| 3157 | # And hey, as long as the strings are already intern'd... |
|---|
| 3158 | symbolic_to_numeric = { |
|---|
| 3159 | "RPL_WELCOME": '001', |
|---|
| 3160 | "RPL_YOURHOST": '002', |
|---|
| 3161 | "RPL_CREATED": '003', |
|---|
| 3162 | "RPL_MYINFO": '004', |
|---|
| 3163 | "RPL_ISUPPORT": '005', |
|---|
| 3164 | "RPL_BOUNCE": '010', |
|---|
| 3165 | "RPL_USERHOST": '302', |
|---|
| 3166 | "RPL_ISON": '303', |
|---|
| 3167 | "RPL_AWAY": '301', |
|---|
| 3168 | "RPL_UNAWAY": '305', |
|---|
| 3169 | "RPL_NOWAWAY": '306', |
|---|
| 3170 | "RPL_WHOISUSER": '311', |
|---|
| 3171 | "RPL_WHOISSERVER": '312', |
|---|
| 3172 | "RPL_WHOISOPERATOR": '313', |
|---|
| 3173 | "RPL_WHOISIDLE": '317', |
|---|
| 3174 | "RPL_ENDOFWHOIS": '318', |
|---|
| 3175 | "RPL_WHOISCHANNELS": '319', |
|---|
| 3176 | "RPL_WHOWASUSER": '314', |
|---|
| 3177 | "RPL_ENDOFWHOWAS": '369', |
|---|
| 3178 | "RPL_LISTSTART": '321', |
|---|
| 3179 | "RPL_LIST": '322', |
|---|
| 3180 | "RPL_LISTEND": '323', |
|---|
| 3181 | "RPL_UNIQOPIS": '325', |
|---|
| 3182 | "RPL_CHANNELMODEIS": '324', |
|---|
| 3183 | "RPL_NOTOPIC": '331', |
|---|
| 3184 | "RPL_TOPIC": '332', |
|---|
| 3185 | "RPL_INVITING": '341', |
|---|
| 3186 | "RPL_SUMMONING": '342', |
|---|
| 3187 | "RPL_INVITELIST": '346', |
|---|
| 3188 | "RPL_ENDOFINVITELIST": '347', |
|---|
| 3189 | "RPL_EXCEPTLIST": '348', |
|---|
| 3190 | "RPL_ENDOFEXCEPTLIST": '349', |
|---|
| 3191 | "RPL_VERSION": '351', |
|---|
| 3192 | "RPL_WHOREPLY": '352', |
|---|
| 3193 | "RPL_ENDOFWHO": '315', |
|---|
| 3194 | "RPL_NAMREPLY": '353', |
|---|
| 3195 | "RPL_ENDOFNAMES": '366', |
|---|
| 3196 | "RPL_LINKS": '364', |
|---|
| 3197 | "RPL_ENDOFLINKS": '365', |
|---|
| 3198 | "RPL_BANLIST": '367', |
|---|
| 3199 | "RPL_ENDOFBANLIST": '368', |
|---|
| 3200 | "RPL_INFO": '371', |
|---|
| 3201 | "RPL_ENDOFINFO": '374', |
|---|
| 3202 | "RPL_MOTDSTART": '375', |
|---|
| 3203 | "RPL_MOTD": '372', |
|---|
| 3204 | "RPL_ENDOFMOTD": '376', |
|---|
| 3205 | "RPL_YOUREOPER": '381', |
|---|
| 3206 | "RPL_REHASHING": '382', |
|---|
| 3207 | "RPL_YOURESERVICE": '383', |
|---|
| 3208 | "RPL_TIME": '391', |
|---|
| 3209 | "RPL_USERSSTART": '392', |
|---|
| 3210 | "RPL_USERS": '393', |
|---|
| 3211 | "RPL_ENDOFUSERS": '394', |
|---|
| 3212 | "RPL_NOUSERS": '395', |
|---|
| 3213 | "RPL_TRACELINK": '200', |
|---|
| 3214 | "RPL_TRACECONNECTING": '201', |
|---|
| 3215 | "RPL_TRACEHANDSHAKE": '202', |
|---|
| 3216 | "RPL_TRACEUNKNOWN": '203', |
|---|
| 3217 | "RPL_TRACEOPERATOR": '204', |
|---|
| 3218 | "RPL_TRACEUSER": '205', |
|---|
| 3219 | "RPL_TRACESERVER": '206', |
|---|
| 3220 | "RPL_TRACESERVICE": '207', |
|---|
| 3221 | "RPL_TRACENEWTYPE": '208', |
|---|
| 3222 | "RPL_TRACECLASS": '209', |
|---|
| 3223 | "RPL_TRACERECONNECT": '210', |
|---|
| 3224 | "RPL_TRACELOG": '261', |
|---|
| 3225 | "RPL_TRACEEND": '262', |
|---|
| 3226 | "RPL_STATSLINKINFO": '211', |
|---|
| 3227 | "RPL_STATSCOMMANDS": '212', |
|---|
| 3228 | "RPL_ENDOFSTATS": '219', |
|---|
| 3229 | "RPL_STATSUPTIME": '242', |
|---|
| 3230 | "RPL_STATSOLINE": '243', |
|---|
| 3231 | "RPL_UMODEIS": '221', |
|---|
| 3232 | "RPL_SERVLIST": '234', |
|---|
| 3233 | "RPL_SERVLISTEND": '235', |
|---|
| 3234 | "RPL_LUSERCLIENT": '251', |
|---|
| 3235 | "RPL_LUSEROP": '252', |
|---|
| 3236 | "RPL_LUSERUNKNOWN": '253', |
|---|
| 3237 | "RPL_LUSERCHANNELS": '254', |
|---|
| 3238 | "RPL_LUSERME": '255', |
|---|
| 3239 | "RPL_ADMINME": '256', |
|---|
| 3240 | "RPL_ADMINLOC": '257', |
|---|
| 3241 | "RPL_ADMINLOC": '258', |
|---|
| 3242 | "RPL_ADMINEMAIL": '259', |
|---|
| 3243 | "RPL_TRYAGAIN": '263', |
|---|
| 3244 | "ERR_NOSUCHNICK": '401', |
|---|
| 3245 | "ERR_NOSUCHSERVER": '402', |
|---|
| 3246 | "ERR_NOSUCHCHANNEL": '403', |
|---|
| 3247 | "ERR_CANNOTSENDTOCHAN": '404', |
|---|
| 3248 | "ERR_TOOMANYCHANNELS": '405', |
|---|
| 3249 | "ERR_WASNOSUCHNICK": '406', |
|---|
| 3250 | "ERR_TOOMANYTARGETS": '407', |
|---|
| 3251 | "ERR_NOSUCHSERVICE": '408', |
|---|
| 3252 | "ERR_NOORIGIN": '409', |
|---|
| 3253 | "ERR_NORECIPIENT": '411', |
|---|
| 3254 | "ERR_NOTEXTTOSEND": '412', |
|---|
| 3255 | "ERR_NOTOPLEVEL": '413', |
|---|
| 3256 | "ERR_WILDTOPLEVEL": '414', |
|---|
| 3257 | "ERR_BADMASK": '415', |
|---|
| 3258 | "ERR_UNKNOWNCOMMAND": '421', |
|---|
| 3259 | "ERR_NOMOTD": '422', |
|---|
| 3260 | "ERR_NOADMININFO": '423', |
|---|
| 3261 | "ERR_FILEERROR": '424', |
|---|
| 3262 | "ERR_NONICKNAMEGIVEN": '431', |
|---|
| 3263 | "ERR_ERRONEUSNICKNAME": '432', |
|---|
| 3264 | "ERR_NICKNAMEINUSE": '433', |
|---|
| 3265 | "ERR_NICKCOLLISION": '436', |
|---|
| 3266 | "ERR_UNAVAILRESOURCE": '437', |
|---|
| 3267 | "ERR_USERNOTINCHANNEL": '441', |
|---|
| 3268 | "ERR_NOTONCHANNEL": '442', |
|---|
| 3269 | "ERR_USERONCHANNEL": '443', |
|---|
| 3270 | "ERR_NOLOGIN": '444', |
|---|
| 3271 | "ERR_SUMMONDISABLED": '445', |
|---|
| 3272 | "ERR_USERSDISABLED": '446', |
|---|
| 3273 | "ERR_NOTREGISTERED": '451', |
|---|
| 3274 | "ERR_NEEDMOREPARAMS": '461', |
|---|
| 3275 | "ERR_ALREADYREGISTRED": '462', |
|---|
| 3276 | "ERR_NOPERMFORHOST": '463', |
|---|
| 3277 | "ERR_PASSWDMISMATCH": '464', |
|---|
| 3278 | "ERR_YOUREBANNEDCREEP": '465', |
|---|
| 3279 | "ERR_YOUWILLBEBANNED": '466', |
|---|
| 3280 | "ERR_KEYSET": '467', |
|---|
| 3281 | "ERR_CHANNELISFULL": '471', |
|---|
| 3282 | "ERR_UNKNOWNMODE": '472', |
|---|
| 3283 | "ERR_INVITEONLYCHAN": '473', |
|---|
| 3284 | "ERR_BANNEDFROMCHAN": '474', |
|---|
| 3285 | "ERR_BADCHANNELKEY": '475', |
|---|
| 3286 | "ERR_BADCHANMASK": '476', |
|---|
| 3287 | "ERR_NOCHANMODES": '477', |
|---|
| 3288 | "ERR_BANLISTFULL": '478', |
|---|
| 3289 | "ERR_NOPRIVILEGES": '481', |
|---|
| 3290 | "ERR_CHANOPRIVSNEEDED": '482', |
|---|
| 3291 | "ERR_CANTKILLSERVER": '483', |
|---|
| 3292 | "ERR_RESTRICTED": '484', |
|---|
| 3293 | "ERR_UNIQOPPRIVSNEEDED": '485', |
|---|
| 3294 | "ERR_NOOPERHOST": '491', |
|---|
| 3295 | "ERR_NOSERVICEHOST": '492', |
|---|
| 3296 | "ERR_UMODEUNKNOWNFLAG": '501', |
|---|
| 3297 | "ERR_USERSDONTMATCH": '502', |
|---|
| 3298 | } |
|---|
| 3299 | |
|---|
| 3300 | numeric_to_symbolic = {} |
|---|
| 3301 | for k, v in symbolic_to_numeric.items(): |
|---|
| 3302 | numeric_to_symbolic[v] = k |
|---|