~justin-fathomdb/nova/justinsb-openstack-api-volumes

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/words/protocols/irc.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.words.test.test_irc -*-
 
2
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
"""
 
6
Internet Relay Chat Protocol for client and server.
 
7
 
 
8
Future Plans
 
9
============
 
10
 
 
11
The way the IRCClient class works here encourages people to implement
 
12
IRC clients by subclassing the ephemeral protocol class, and it tends
 
13
to end up with way more state than it should for an object which will
 
14
be destroyed as soon as the TCP transport drops.  Someone oughta do
 
15
something about that, ya know?
 
16
 
 
17
The DCC support needs to have more hooks for the client for it to be
 
18
able to ask the user things like "Do you want to accept this session?"
 
19
and "Transfer #2 is 67% done." and otherwise manage the DCC sessions.
 
20
 
 
21
Test coverage needs to be better.
 
22
 
 
23
@author: Kevin Turner
 
24
 
 
25
@see: RFC 1459: Internet Relay Chat Protocol
 
26
@see: RFC 2812: Internet Relay Chat: Client Protocol
 
27
@see: U{The Client-To-Client-Protocol
 
28
<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
 
29
"""
 
30
 
 
31
import errno, os, random, re, stat, struct, sys, time, types, traceback
 
32
import string, socket
 
33
import warnings
 
34
from os import path
 
35
 
 
36
from twisted.internet import reactor, protocol
 
37
from twisted.persisted import styles
 
38
from twisted.protocols import basic
 
39
from twisted.python import log, reflect, text
 
40
 
 
41
NUL = chr(0)
 
42
CR = chr(015)
 
43
NL = chr(012)
 
44
LF = NL
 
45
SPC = chr(040)
 
46
 
 
47
CHANNEL_PREFIXES = '&#!+'
 
48
 
 
49
class IRCBadMessage(Exception):
 
50
    pass
 
51
 
 
52
class IRCPasswordMismatch(Exception):
 
53
    pass
 
54
 
 
55
 
 
56
 
 
57
class IRCBadModes(ValueError):
 
58
    """
 
59
    A malformed mode was encountered while attempting to parse a mode string.
 
60
    """
 
61
 
 
62
 
 
63
 
 
64
def parsemsg(s):
 
65
    """Breaks a message from an IRC server into its prefix, command, and arguments.
 
66
    """
 
67
    prefix = ''
 
68
    trailing = []
 
69
    if not s:
 
70
        raise IRCBadMessage("Empty line.")
 
71
    if s[0] == ':':
 
72
        prefix, s = s[1:].split(' ', 1)
 
73
    if s.find(' :') != -1:
 
74
        s, trailing = s.split(' :', 1)
 
75
        args = s.split()
 
76
        args.append(trailing)
 
77
    else:
 
78
        args = s.split()
 
79
    command = args.pop(0)
 
80
    return prefix, command, args
 
81
 
 
82
 
 
83
def split(str, length = 80):
 
84
    """I break a message into multiple lines.
 
85
 
 
86
    I prefer to break at whitespace near str[length].  I also break at \\n.
 
87
 
 
88
    @returns: list of strings
 
89
    """
 
90
    if length <= 0:
 
91
        raise ValueError("Length must be a number greater than zero")
 
92
    r = []
 
93
    while len(str) > length:
 
94
        w, n = str[:length].rfind(' '), str[:length].find('\n')
 
95
        if w == -1 and n == -1:
 
96
            line, str = str[:length], str[length:]
 
97
        else:
 
98
            if n == -1:
 
99
                i = w
 
100
            else:
 
101
                i = n
 
102
            if i == 0: # just skip the space or newline. don't append any output.
 
103
                str = str[1:]
 
104
                continue
 
105
            line, str = str[:i], str[i+1:]
 
106
        r.append(line)
 
107
    if len(str):
 
108
        r.extend(str.split('\n'))
 
109
    return r
 
110
 
 
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
    """Internet Relay Chat client protocol, with sprinkles.
 
973
 
 
974
    In addition to providing an interface for an IRC client protocol,
 
975
    this class also contains reasonable implementations of many common
 
976
    CTCP methods.
 
977
 
 
978
    TODO
 
979
    ====
 
980
     - Limit the length of messages sent (because the IRC server probably
 
981
       does).
 
982
     - Add flood protection/rate limiting for my CTCP replies.
 
983
     - NickServ cooperation.  (a mix-in?)
 
984
     - Heartbeat.  The transport may die in such a way that it does not realize
 
985
       it is dead until it is written to.  Sending something (like "PING
 
986
       this.irc-host.net") during idle peroids would alleviate that.  If
 
987
       you're concerned with the stability of the host as well as that of the
 
988
       transport, you might care to watch for the corresponding PONG.
 
989
 
 
990
    @ivar nickname: Nickname the client will use.
 
991
    @ivar password: Password used to log on to the server.  May be C{None}.
 
992
    @ivar realname: Supplied to the server during login as the "Real name"
 
993
        or "ircname".  May be C{None}.
 
994
    @ivar username: Supplied to the server during login as the "User name".
 
995
        May be C{None}
 
996
 
 
997
    @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query.  If C{None}, no
 
998
        USERINFO reply will be sent.
 
999
        "This is used to transmit a string which is settable by
 
1000
        the user (and never should be set by the client)."
 
1001
    @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query.  If C{None}, no
 
1002
        FINGER reply will be sent.
 
1003
    @type fingerReply: Callable or String
 
1004
 
 
1005
    @ivar versionName: CTCP VERSION reply, client name.  If C{None}, no VERSION
 
1006
        reply will be sent.
 
1007
    @type versionName: C{str}, or None.
 
1008
    @ivar versionNum: CTCP VERSION reply, client version.
 
1009
    @type versionNum: C{str}, or None.
 
1010
    @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
 
1011
    @type versionEnv: C{str}, or None.
 
1012
 
 
1013
    @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
 
1014
        client may be found.  If C{None}, no SOURCE reply will be sent.
 
1015
 
 
1016
    @ivar lineRate: Minimum delay between lines sent to the server.  If
 
1017
        C{None}, no delay will be imposed.
 
1018
    @type lineRate: Number of Seconds.
 
1019
 
 
1020
    @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and
 
1021
        I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content
 
1022
        of an I{RPL_MOTD} message.
 
1023
 
 
1024
    @ivar erroneousNickFallback: Default nickname assigned when an unregistered
 
1025
        client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register
 
1026
        with an illegal nickname.
 
1027
    @type erroneousNickFallback: C{str}
 
1028
 
 
1029
    @ivar _registered: Whether or not the user is registered. It becomes True
 
1030
        once a welcome has been received from the server.
 
1031
    @type _registered: C{bool}
 
1032
 
 
1033
    @ivar _attemptedNick: The nickname that will try to get registered. It may
 
1034
        change if it is illegal or already taken. L{nickname} becomes the
 
1035
        L{_attemptedNick} that is successfully registered.
 
1036
    @type _attemptedNick:  C{str}
 
1037
 
 
1038
    @type supported: L{ServerSupportedFeatures}
 
1039
    @ivar supported: Available ISUPPORT features on the server
 
1040
    """
 
1041
    motd = None
 
1042
    nickname = 'irc'
 
1043
    password = None
 
1044
    realname = None
 
1045
    username = None
 
1046
    ### Responses to various CTCP queries.
 
1047
 
 
1048
    userinfo = None
 
1049
    # fingerReply is a callable returning a string, or a str()able object.
 
1050
    fingerReply = None
 
1051
    versionName = None
 
1052
    versionNum = None
 
1053
    versionEnv = None
 
1054
 
 
1055
    sourceURL = "http://twistedmatrix.com/downloads/"
 
1056
 
 
1057
    dcc_destdir = '.'
 
1058
    dcc_sessions = None
 
1059
 
 
1060
    # If this is false, no attempt will be made to identify
 
1061
    # ourself to the server.
 
1062
    performLogin = 1
 
1063
 
 
1064
    lineRate = None
 
1065
    _queue = None
 
1066
    _queueEmptying = None
 
1067
 
 
1068
    delimiter = '\n' # '\r\n' will also work (see dataReceived)
 
1069
 
 
1070
    __pychecker__ = 'unusednames=params,prefix,channel'
 
1071
 
 
1072
    _registered = False
 
1073
    _attemptedNick = ''
 
1074
    erroneousNickFallback = 'defaultnick'
 
1075
 
 
1076
    def _reallySendLine(self, line):
 
1077
        return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
 
1078
 
 
1079
    def sendLine(self, line):
 
1080
        if self.lineRate is None:
 
1081
            self._reallySendLine(line)
 
1082
        else:
 
1083
            self._queue.append(line)
 
1084
            if not self._queueEmptying:
 
1085
                self._sendLine()
 
1086
 
 
1087
    def _sendLine(self):
 
1088
        if self._queue:
 
1089
            self._reallySendLine(self._queue.pop(0))
 
1090
            self._queueEmptying = reactor.callLater(self.lineRate,
 
1091
                                                    self._sendLine)
 
1092
        else:
 
1093
            self._queueEmptying = None
 
1094
 
 
1095
 
 
1096
    ### Interface level client->user output methods
 
1097
    ###
 
1098
    ### You'll want to override these.
 
1099
 
 
1100
    ### Methods relating to the server itself
 
1101
 
 
1102
    def created(self, when):
 
1103
        """Called with creation date information about the server, usually at logon.
 
1104
 
 
1105
        @type when: C{str}
 
1106
        @param when: A string describing when the server was created, probably.
 
1107
        """
 
1108
 
 
1109
    def yourHost(self, info):
 
1110
        """Called with daemon information about the server, usually at logon.
 
1111
 
 
1112
        @type info: C{str}
 
1113
        @param when: A string describing what software the server is running, probably.
 
1114
        """
 
1115
 
 
1116
    def myInfo(self, servername, version, umodes, cmodes):
 
1117
        """Called with information about the server, usually at logon.
 
1118
 
 
1119
        @type servername: C{str}
 
1120
        @param servername: The hostname of this server.
 
1121
 
 
1122
        @type version: C{str}
 
1123
        @param version: A description of what software this server runs.
 
1124
 
 
1125
        @type umodes: C{str}
 
1126
        @param umodes: All the available user modes.
 
1127
 
 
1128
        @type cmodes: C{str}
 
1129
        @param cmodes: All the available channel modes.
 
1130
        """
 
1131
 
 
1132
    def luserClient(self, info):
 
1133
        """Called with information about the number of connections, usually at logon.
 
1134
 
 
1135
        @type info: C{str}
 
1136
        @param info: A description of the number of clients and servers
 
1137
        connected to the network, probably.
 
1138
        """
 
1139
 
 
1140
    def bounce(self, info):
 
1141
        """Called with information about where the client should reconnect.
 
1142
 
 
1143
        @type info: C{str}
 
1144
        @param info: A plaintext description of the address that should be
 
1145
        connected to.
 
1146
        """
 
1147
 
 
1148
    def isupport(self, options):
 
1149
        """Called with various information about what the server supports.
 
1150
 
 
1151
        @type options: C{list} of C{str}
 
1152
        @param options: Descriptions of features or limits of the server, possibly
 
1153
        in the form "NAME=VALUE".
 
1154
        """
 
1155
 
 
1156
    def luserChannels(self, channels):
 
1157
        """Called with the number of channels existant on the server.
 
1158
 
 
1159
        @type channels: C{int}
 
1160
        """
 
1161
 
 
1162
    def luserOp(self, ops):
 
1163
        """Called with the number of ops logged on to the server.
 
1164
 
 
1165
        @type ops: C{int}
 
1166
        """
 
1167
 
 
1168
    def luserMe(self, info):
 
1169
        """Called with information about the server connected to.
 
1170
 
 
1171
        @type info: C{str}
 
1172
        @param info: A plaintext string describing the number of users and servers
 
1173
        connected to this server.
 
1174
        """
 
1175
 
 
1176
    ### Methods involving me directly
 
1177
 
 
1178
    def privmsg(self, user, channel, message):
 
1179
        """Called when I have a message from a user to me or a channel.
 
1180
        """
 
1181
        pass
 
1182
 
 
1183
    def joined(self, channel):
 
1184
        """
 
1185
        Called when I finish joining a channel.
 
1186
 
 
1187
        channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
 
1188
        intact.
 
1189
        """
 
1190
 
 
1191
    def left(self, channel):
 
1192
        """
 
1193
        Called when I have left a channel.
 
1194
 
 
1195
        channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
 
1196
        intact.
 
1197
        """
 
1198
 
 
1199
    def noticed(self, user, channel, message):
 
1200
        """Called when I have a notice from a user to me or a channel.
 
1201
 
 
1202
        By default, this is equivalent to IRCClient.privmsg, but if your
 
1203
        client makes any automated replies, you must override this!
 
1204
        From the RFC::
 
1205
 
 
1206
            The difference between NOTICE and PRIVMSG is that
 
1207
            automatic replies MUST NEVER be sent in response to a
 
1208
            NOTICE message. [...] The object of this rule is to avoid
 
1209
            loops between clients automatically sending something in
 
1210
            response to something it received.
 
1211
        """
 
1212
        self.privmsg(user, channel, message)
 
1213
 
 
1214
    def modeChanged(self, user, channel, set, modes, args):
 
1215
        """Called when users or channel's modes are changed.
 
1216
 
 
1217
        @type user: C{str}
 
1218
        @param user: The user and hostmask which instigated this change.
 
1219
 
 
1220
        @type channel: C{str}
 
1221
        @param channel: The channel where the modes are changed. If args is
 
1222
        empty the channel for which the modes are changing. If the changes are
 
1223
        at server level it could be equal to C{user}.
 
1224
 
 
1225
        @type set: C{bool} or C{int}
 
1226
        @param set: True if the mode(s) is being added, False if it is being
 
1227
        removed. If some modes are added and others removed at the same time
 
1228
        this function will be called twice, the first time with all the added
 
1229
        modes, the second with the removed ones. (To change this behaviour
 
1230
        override the irc_MODE method)
 
1231
 
 
1232
        @type modes: C{str}
 
1233
        @param modes: The mode or modes which are being changed.
 
1234
 
 
1235
        @type args: C{tuple}
 
1236
        @param args: Any additional information required for the mode
 
1237
        change.
 
1238
        """
 
1239
 
 
1240
    def pong(self, user, secs):
 
1241
        """Called with the results of a CTCP PING query.
 
1242
        """
 
1243
        pass
 
1244
 
 
1245
    def signedOn(self):
 
1246
        """Called after sucessfully signing on to the server.
 
1247
        """
 
1248
        pass
 
1249
 
 
1250
    def kickedFrom(self, channel, kicker, message):
 
1251
        """Called when I am kicked from a channel.
 
1252
        """
 
1253
        pass
 
1254
 
 
1255
    def nickChanged(self, nick):
 
1256
        """Called when my nick has been changed.
 
1257
        """
 
1258
        self.nickname = nick
 
1259
 
 
1260
 
 
1261
    ### Things I observe other people doing in a channel.
 
1262
 
 
1263
    def userJoined(self, user, channel):
 
1264
        """Called when I see another user joining a channel.
 
1265
        """
 
1266
        pass
 
1267
 
 
1268
    def userLeft(self, user, channel):
 
1269
        """Called when I see another user leaving a channel.
 
1270
        """
 
1271
        pass
 
1272
 
 
1273
    def userQuit(self, user, quitMessage):
 
1274
        """Called when I see another user disconnect from the network.
 
1275
        """
 
1276
        pass
 
1277
 
 
1278
    def userKicked(self, kickee, channel, kicker, message):
 
1279
        """Called when I observe someone else being kicked from a channel.
 
1280
        """
 
1281
        pass
 
1282
 
 
1283
    def action(self, user, channel, data):
 
1284
        """Called when I see a user perform an ACTION on a channel.
 
1285
        """
 
1286
        pass
 
1287
 
 
1288
    def topicUpdated(self, user, channel, newTopic):
 
1289
        """In channel, user changed the topic to newTopic.
 
1290
 
 
1291
        Also called when first joining a channel.
 
1292
        """
 
1293
        pass
 
1294
 
 
1295
    def userRenamed(self, oldname, newname):
 
1296
        """A user changed their name from oldname to newname.
 
1297
        """
 
1298
        pass
 
1299
 
 
1300
    ### Information from the server.
 
1301
 
 
1302
    def receivedMOTD(self, motd):
 
1303
        """I received a message-of-the-day banner from the server.
 
1304
 
 
1305
        motd is a list of strings, where each string was sent as a seperate
 
1306
        message from the server. To display, you might want to use::
 
1307
 
 
1308
            '\\n'.join(motd)
 
1309
 
 
1310
        to get a nicely formatted string.
 
1311
        """
 
1312
        pass
 
1313
 
 
1314
    ### user input commands, client->server
 
1315
    ### Your client will want to invoke these.
 
1316
 
 
1317
    def join(self, channel, key=None):
 
1318
        """
 
1319
        Join a channel.
 
1320
 
 
1321
        @type channel: C{str}
 
1322
        @param channel: The name of the channel to join. If it has no prefix,
 
1323
            C{'#'} will be prepended to it.
 
1324
        @type key: C{str}
 
1325
        @param key: If specified, the key used to join the channel.
 
1326
        """
 
1327
        if channel[0] not in CHANNEL_PREFIXES:
 
1328
            channel = '#' + channel
 
1329
        if key:
 
1330
            self.sendLine("JOIN %s %s" % (channel, key))
 
1331
        else:
 
1332
            self.sendLine("JOIN %s" % (channel,))
 
1333
 
 
1334
    def leave(self, channel, reason=None):
 
1335
        """
 
1336
        Leave a channel.
 
1337
 
 
1338
        @type channel: C{str}
 
1339
        @param channel: The name of the channel to leave. If it has no prefix,
 
1340
            C{'#'} will be prepended to it.
 
1341
        @type reason: C{str}
 
1342
        @param reason: If given, the reason for leaving.
 
1343
        """
 
1344
        if channel[0] not in CHANNEL_PREFIXES:
 
1345
            channel = '#' + channel
 
1346
        if reason:
 
1347
            self.sendLine("PART %s :%s" % (channel, reason))
 
1348
        else:
 
1349
            self.sendLine("PART %s" % (channel,))
 
1350
 
 
1351
    def kick(self, channel, user, reason=None):
 
1352
        """
 
1353
        Attempt to kick a user from a channel.
 
1354
 
 
1355
        @type channel: C{str}
 
1356
        @param channel: The name of the channel to kick the user from. If it has
 
1357
            no prefix, C{'#'} will be prepended to it.
 
1358
        @type user: C{str}
 
1359
        @param user: The nick of the user to kick.
 
1360
        @type reason: C{str}
 
1361
        @param reason: If given, the reason for kicking the user.
 
1362
        """
 
1363
        if channel[0] not in CHANNEL_PREFIXES:
 
1364
            channel = '#' + channel
 
1365
        if reason:
 
1366
            self.sendLine("KICK %s %s :%s" % (channel, user, reason))
 
1367
        else:
 
1368
            self.sendLine("KICK %s %s" % (channel, user))
 
1369
 
 
1370
    part = leave
 
1371
 
 
1372
    def topic(self, channel, topic=None):
 
1373
        """
 
1374
        Attempt to set the topic of the given channel, or ask what it is.
 
1375
 
 
1376
        If topic is None, then I sent a topic query instead of trying to set the
 
1377
        topic. The server should respond with a TOPIC message containing the
 
1378
        current topic of the given channel.
 
1379
 
 
1380
        @type channel: C{str}
 
1381
        @param channel: The name of the channel to change the topic on. If it
 
1382
            has no prefix, C{'#'} will be prepended to it.
 
1383
        @type topic: C{str}
 
1384
        @param topic: If specified, what to set the topic to.
 
1385
        """
 
1386
        # << TOPIC #xtestx :fff
 
1387
        if channel[0] not in CHANNEL_PREFIXES:
 
1388
            channel = '#' + channel
 
1389
        if topic != None:
 
1390
            self.sendLine("TOPIC %s :%s" % (channel, topic))
 
1391
        else:
 
1392
            self.sendLine("TOPIC %s" % (channel,))
 
1393
 
 
1394
    def mode(self, chan, set, modes, limit = None, user = None, mask = None):
 
1395
        """
 
1396
        Change the modes on a user or channel.
 
1397
 
 
1398
        The C{limit}, C{user}, and C{mask} parameters are mutually exclusive.
 
1399
 
 
1400
        @type chan: C{str}
 
1401
        @param chan: The name of the channel to operate on.
 
1402
        @type set: C{bool}
 
1403
        @param set: True to give the user or channel permissions and False to
 
1404
            remove them.
 
1405
        @type modes: C{str}
 
1406
        @param modes: The mode flags to set on the user or channel.
 
1407
        @type limit: C{int}
 
1408
        @param limit: In conjuction with the C{'l'} mode flag, limits the
 
1409
             number of users on the channel.
 
1410
        @type user: C{str}
 
1411
        @param user: The user to change the mode on.
 
1412
        @type mask: C{str}
 
1413
        @param mask: In conjuction with the C{'b'} mode flag, sets a mask of
 
1414
            users to be banned from the channel.
 
1415
        """
 
1416
        if set:
 
1417
            line = 'MODE %s +%s' % (chan, modes)
 
1418
        else:
 
1419
            line = 'MODE %s -%s' % (chan, modes)
 
1420
        if limit is not None:
 
1421
            line = '%s %d' % (line, limit)
 
1422
        elif user is not None:
 
1423
            line = '%s %s' % (line, user)
 
1424
        elif mask is not None:
 
1425
            line = '%s %s' % (line, mask)
 
1426
        self.sendLine(line)
 
1427
 
 
1428
 
 
1429
    def say(self, channel, message, length = None):
 
1430
        """
 
1431
        Send a message to a channel
 
1432
 
 
1433
        @type channel: C{str}
 
1434
        @param channel: The channel to say the message on. If it has no prefix,
 
1435
            C{'#'} will be prepended to it.
 
1436
        @type message: C{str}
 
1437
        @param message: The message to say.
 
1438
        @type length: C{int}
 
1439
        @param length: The maximum number of octets to send at a time.  This has
 
1440
            the effect of turning a single call to C{msg()} into multiple
 
1441
            commands to the server.  This is useful when long messages may be
 
1442
            sent that would otherwise cause the server to kick us off or
 
1443
            silently truncate the text we are sending.  If None is passed, the
 
1444
            entire message is always send in one command.
 
1445
        """
 
1446
        if channel[0] not in CHANNEL_PREFIXES:
 
1447
            channel = '#' + channel
 
1448
        self.msg(channel, message, length)
 
1449
 
 
1450
 
 
1451
    def msg(self, user, message, length = None):
 
1452
        """Send a message to a user or channel.
 
1453
 
 
1454
        @type user: C{str}
 
1455
        @param user: The username or channel name to which to direct the
 
1456
        message.
 
1457
 
 
1458
        @type message: C{str}
 
1459
        @param message: The text to send
 
1460
 
 
1461
        @type length: C{int}
 
1462
        @param length: The maximum number of octets to send at a time.  This
 
1463
        has the effect of turning a single call to msg() into multiple
 
1464
        commands to the server.  This is useful when long messages may be
 
1465
        sent that would otherwise cause the server to kick us off or silently
 
1466
        truncate the text we are sending.  If None is passed, the entire
 
1467
        message is always send in one command.
 
1468
        """
 
1469
 
 
1470
        fmt = "PRIVMSG %s :%%s" % (user,)
 
1471
 
 
1472
        if length is None:
 
1473
            self.sendLine(fmt % (message,))
 
1474
        else:
 
1475
            # NOTE: minimumLength really equals len(fmt) - 2 (for '%s') + n
 
1476
            # where n is how many bytes sendLine sends to end the line.
 
1477
            # n was magic numbered to 2, I think incorrectly
 
1478
            minimumLength = len(fmt)
 
1479
            if length <= minimumLength:
 
1480
                raise ValueError("Maximum length must exceed %d for message "
 
1481
                                 "to %s" % (minimumLength, user))
 
1482
            lines = split(message, length - minimumLength)
 
1483
            map(lambda line, self=self, fmt=fmt: self.sendLine(fmt % line),
 
1484
                lines)
 
1485
 
 
1486
    def notice(self, user, message):
 
1487
        """
 
1488
        Send a notice to a user.
 
1489
 
 
1490
        Notices are like normal message, but should never get automated
 
1491
        replies.
 
1492
 
 
1493
        @type user: C{str}
 
1494
        @param user: The user to send a notice to.
 
1495
        @type message: C{str}
 
1496
        @param message: The contents of the notice to send.
 
1497
        """
 
1498
        self.sendLine("NOTICE %s :%s" % (user, message))
 
1499
 
 
1500
    def away(self, message=''):
 
1501
        """
 
1502
        Mark this client as away.
 
1503
 
 
1504
        @type message: C{str}
 
1505
        @param message: If specified, the away message.
 
1506
        """
 
1507
        self.sendLine("AWAY :%s" % message)
 
1508
 
 
1509
 
 
1510
 
 
1511
    def back(self):
 
1512
        """
 
1513
        Clear the away status.
 
1514
        """
 
1515
        # An empty away marks us as back
 
1516
        self.away()
 
1517
 
 
1518
 
 
1519
    def whois(self, nickname, server=None):
 
1520
        """
 
1521
        Retrieve user information about the given nick name.
 
1522
 
 
1523
        @type nickname: C{str}
 
1524
        @param nickname: The nick name about which to retrieve information.
 
1525
 
 
1526
        @since: 8.2
 
1527
        """
 
1528
        if server is None:
 
1529
            self.sendLine('WHOIS ' + nickname)
 
1530
        else:
 
1531
            self.sendLine('WHOIS %s %s' % (server, nickname))
 
1532
 
 
1533
 
 
1534
    def register(self, nickname, hostname='foo', servername='bar'):
 
1535
        """
 
1536
        Login to the server.
 
1537
 
 
1538
        @type nickname: C{str}
 
1539
        @param nickname: The nickname to register.
 
1540
        @type hostname: C{str}
 
1541
        @param hostname: If specified, the hostname to logon as.
 
1542
        @type servername: C{str}
 
1543
        @param servername: If specified, the servername to logon as.
 
1544
        """
 
1545
        if self.password is not None:
 
1546
            self.sendLine("PASS %s" % self.password)
 
1547
        self.setNick(nickname)
 
1548
        if self.username is None:
 
1549
            self.username = nickname
 
1550
        self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname))
 
1551
 
 
1552
    def setNick(self, nickname):
 
1553
        """
 
1554
        Set this client's nickname.
 
1555
 
 
1556
        @type nickname: C{str}
 
1557
        @param nickname: The nickname to change to.
 
1558
        """
 
1559
        self._attemptedNick = nickname
 
1560
        self.sendLine("NICK %s" % nickname)
 
1561
 
 
1562
    def quit(self, message = ''):
 
1563
        """
 
1564
        Disconnect from the server
 
1565
 
 
1566
        @type message: C{str}
 
1567
 
 
1568
        @param message: If specified, the message to give when quitting the
 
1569
            server.
 
1570
        """
 
1571
        self.sendLine("QUIT :%s" % message)
 
1572
 
 
1573
    ### user input commands, client->client
 
1574
 
 
1575
    def describe(self, channel, action):
 
1576
        """
 
1577
        Strike a pose.
 
1578
 
 
1579
        @type channel: C{str}
 
1580
        @param channel: The name of the channel to have an action on. If it
 
1581
            has no prefix, it is sent to the user of that name.
 
1582
        @type action: C{str}
 
1583
        @param action: The action to preform.
 
1584
        @since: 9.0
 
1585
        """
 
1586
        self.ctcpMakeQuery(channel, [('ACTION', action)])
 
1587
 
 
1588
 
 
1589
    def me(self, channel, action):
 
1590
        """
 
1591
        Strike a pose.
 
1592
 
 
1593
        This function is deprecated since Twisted 9.0. Use describe().
 
1594
 
 
1595
        @type channel: C{str}
 
1596
        @param channel: The name of the channel to have an action on. If it
 
1597
            has no prefix, C{'#'} will be prepended to it.
 
1598
        @type action: C{str}
 
1599
        @param action: The action to preform.
 
1600
        """
 
1601
        warnings.warn("me() is deprecated since Twisted 9.0. Use IRCClient.describe().",
 
1602
                DeprecationWarning, stacklevel=2)
 
1603
 
 
1604
        if channel[0] not in CHANNEL_PREFIXES:
 
1605
            channel = '#' + channel
 
1606
        self.describe(channel, action)
 
1607
 
 
1608
 
 
1609
    _pings = None
 
1610
    _MAX_PINGRING = 12
 
1611
 
 
1612
    def ping(self, user, text = None):
 
1613
        """
 
1614
        Measure round-trip delay to another IRC client.
 
1615
        """
 
1616
        if self._pings is None:
 
1617
            self._pings = {}
 
1618
 
 
1619
        if text is None:
 
1620
            chars = string.letters + string.digits + string.punctuation
 
1621
            key = ''.join([random.choice(chars) for i in range(12)])
 
1622
        else:
 
1623
            key = str(text)
 
1624
        self._pings[(user, key)] = time.time()
 
1625
        self.ctcpMakeQuery(user, [('PING', key)])
 
1626
 
 
1627
        if len(self._pings) > self._MAX_PINGRING:
 
1628
            # Remove some of the oldest entries.
 
1629
            byValue = [(v, k) for (k, v) in self._pings.items()]
 
1630
            byValue.sort()
 
1631
            excess = self._MAX_PINGRING - len(self._pings)
 
1632
            for i in xrange(excess):
 
1633
                del self._pings[byValue[i][1]]
 
1634
 
 
1635
    def dccSend(self, user, file):
 
1636
        if type(file) == types.StringType:
 
1637
            file = open(file, 'r')
 
1638
 
 
1639
        size = fileSize(file)
 
1640
 
 
1641
        name = getattr(file, "name", "file@%s" % (id(file),))
 
1642
 
 
1643
        factory = DccSendFactory(file)
 
1644
        port = reactor.listenTCP(0, factory, 1)
 
1645
 
 
1646
        raise NotImplementedError,(
 
1647
            "XXX!!! Help!  I need to bind a socket, have it listen, and tell me its address.  "
 
1648
            "(and stop accepting once we've made a single connection.)")
 
1649
 
 
1650
        my_address = struct.pack("!I", my_address)
 
1651
 
 
1652
        args = ['SEND', name, my_address, str(port)]
 
1653
 
 
1654
        if not (size is None):
 
1655
            args.append(size)
 
1656
 
 
1657
        args = string.join(args, ' ')
 
1658
 
 
1659
        self.ctcpMakeQuery(user, [('DCC', args)])
 
1660
 
 
1661
    def dccResume(self, user, fileName, port, resumePos):
 
1662
        """Send a DCC RESUME request to another user."""
 
1663
        self.ctcpMakeQuery(user, [
 
1664
            ('DCC', ['RESUME', fileName, port, resumePos])])
 
1665
 
 
1666
    def dccAcceptResume(self, user, fileName, port, resumePos):
 
1667
        """Send a DCC ACCEPT response to clients who have requested a resume.
 
1668
        """
 
1669
        self.ctcpMakeQuery(user, [
 
1670
            ('DCC', ['ACCEPT', fileName, port, resumePos])])
 
1671
 
 
1672
    ### server->client messages
 
1673
    ### You might want to fiddle with these,
 
1674
    ### but it is safe to leave them alone.
 
1675
 
 
1676
    def irc_ERR_NICKNAMEINUSE(self, prefix, params):
 
1677
        """
 
1678
        Called when we try to register or change to a nickname that is already
 
1679
        taken.
 
1680
        """
 
1681
        self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
 
1682
        self.setNick(self._attemptedNick)
 
1683
 
 
1684
 
 
1685
    def alterCollidedNick(self, nickname):
 
1686
        """
 
1687
        Generate an altered version of a nickname that caused a collision in an
 
1688
        effort to create an unused related name for subsequent registration.
 
1689
 
 
1690
        @param nickname: The nickname a user is attempting to register.
 
1691
        @type nickname: C{str}
 
1692
 
 
1693
        @returns: A string that is in some way different from the nickname.
 
1694
        @rtype: C{str}
 
1695
        """
 
1696
        return nickname + '_'
 
1697
 
 
1698
 
 
1699
    def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
 
1700
        """
 
1701
        Called when we try to register or change to an illegal nickname.
 
1702
 
 
1703
        The server should send this reply when the nickname contains any
 
1704
        disallowed characters.  The bot will stall, waiting for RPL_WELCOME, if
 
1705
        we don't handle this during sign-on.
 
1706
 
 
1707
        @note: The method uses the spelling I{erroneus}, as it appears in
 
1708
            the RFC, section 6.1.
 
1709
        """
 
1710
        if not self._registered:
 
1711
            self.setNick(self.erroneousNickFallback)
 
1712
 
 
1713
 
 
1714
    def irc_ERR_PASSWDMISMATCH(self, prefix, params):
 
1715
        """
 
1716
        Called when the login was incorrect.
 
1717
        """
 
1718
        raise IRCPasswordMismatch("Password Incorrect.")
 
1719
 
 
1720
    def irc_RPL_WELCOME(self, prefix, params):
 
1721
        """
 
1722
        Called when we have received the welcome from the server.
 
1723
        """
 
1724
        self._registered = True
 
1725
        self.nickname = self._attemptedNick
 
1726
        self.signedOn()
 
1727
 
 
1728
    def irc_JOIN(self, prefix, params):
 
1729
        """
 
1730
        Called when a user joins a channel.
 
1731
        """
 
1732
        nick = string.split(prefix,'!')[0]
 
1733
        channel = params[-1]
 
1734
        if nick == self.nickname:
 
1735
            self.joined(channel)
 
1736
        else:
 
1737
            self.userJoined(nick, channel)
 
1738
 
 
1739
    def irc_PART(self, prefix, params):
 
1740
        """
 
1741
        Called when a user leaves a channel.
 
1742
        """
 
1743
        nick = string.split(prefix,'!')[0]
 
1744
        channel = params[0]
 
1745
        if nick == self.nickname:
 
1746
            self.left(channel)
 
1747
        else:
 
1748
            self.userLeft(nick, channel)
 
1749
 
 
1750
    def irc_QUIT(self, prefix, params):
 
1751
        """
 
1752
        Called when a user has quit.
 
1753
        """
 
1754
        nick = string.split(prefix,'!')[0]
 
1755
        self.userQuit(nick, params[0])
 
1756
 
 
1757
 
 
1758
    def irc_MODE(self, user, params):
 
1759
        """
 
1760
        Parse a server mode change message.
 
1761
        """
 
1762
        channel, modes, args = params[0], params[1], params[2:]
 
1763
 
 
1764
        if modes[0] not in '-+':
 
1765
            modes = '+' + modes
 
1766
 
 
1767
        if channel == self.nickname:
 
1768
            # This is a mode change to our individual user, not a channel mode
 
1769
            # that involves us.
 
1770
            paramModes = self.getUserModeParams()
 
1771
        else:
 
1772
            paramModes = self.getChannelModeParams()
 
1773
 
 
1774
        try:
 
1775
            added, removed = parseModes(modes, args, paramModes)
 
1776
        except IRCBadModes:
 
1777
            log.err(None, 'An error occured while parsing the following '
 
1778
                          'MODE message: MODE %s' % (' '.join(params),))
 
1779
        else:
 
1780
            if added:
 
1781
                modes, params = zip(*added)
 
1782
                self.modeChanged(user, channel, True, ''.join(modes), params)
 
1783
 
 
1784
            if removed:
 
1785
                modes, params = zip(*removed)
 
1786
                self.modeChanged(user, channel, False, ''.join(modes), params)
 
1787
 
 
1788
 
 
1789
    def irc_PING(self, prefix, params):
 
1790
        """
 
1791
        Called when some has pinged us.
 
1792
        """
 
1793
        self.sendLine("PONG %s" % params[-1])
 
1794
 
 
1795
    def irc_PRIVMSG(self, prefix, params):
 
1796
        """
 
1797
        Called when we get a message.
 
1798
        """
 
1799
        user = prefix
 
1800
        channel = params[0]
 
1801
        message = params[-1]
 
1802
 
 
1803
        if not message: return # don't raise an exception if some idiot sends us a blank message
 
1804
 
 
1805
        if message[0]==X_DELIM:
 
1806
            m = ctcpExtract(message)
 
1807
            if m['extended']:
 
1808
                self.ctcpQuery(user, channel, m['extended'])
 
1809
 
 
1810
            if not m['normal']:
 
1811
                return
 
1812
 
 
1813
            message = string.join(m['normal'], ' ')
 
1814
 
 
1815
        self.privmsg(user, channel, message)
 
1816
 
 
1817
    def irc_NOTICE(self, prefix, params):
 
1818
        """
 
1819
        Called when a user gets a notice.
 
1820
        """
 
1821
        user = prefix
 
1822
        channel = params[0]
 
1823
        message = params[-1]
 
1824
 
 
1825
        if message[0]==X_DELIM:
 
1826
            m = ctcpExtract(message)
 
1827
            if m['extended']:
 
1828
                self.ctcpReply(user, channel, m['extended'])
 
1829
 
 
1830
            if not m['normal']:
 
1831
                return
 
1832
 
 
1833
            message = string.join(m['normal'], ' ')
 
1834
 
 
1835
        self.noticed(user, channel, message)
 
1836
 
 
1837
    def irc_NICK(self, prefix, params):
 
1838
        """
 
1839
        Called when a user changes their nickname.
 
1840
        """
 
1841
        nick = string.split(prefix,'!', 1)[0]
 
1842
        if nick == self.nickname:
 
1843
            self.nickChanged(params[0])
 
1844
        else:
 
1845
            self.userRenamed(nick, params[0])
 
1846
 
 
1847
    def irc_KICK(self, prefix, params):
 
1848
        """
 
1849
        Called when a user is kicked from a channel.
 
1850
        """
 
1851
        kicker = string.split(prefix,'!')[0]
 
1852
        channel = params[0]
 
1853
        kicked = params[1]
 
1854
        message = params[-1]
 
1855
        if string.lower(kicked) == string.lower(self.nickname):
 
1856
            # Yikes!
 
1857
            self.kickedFrom(channel, kicker, message)
 
1858
        else:
 
1859
            self.userKicked(kicked, channel, kicker, message)
 
1860
 
 
1861
    def irc_TOPIC(self, prefix, params):
 
1862
        """
 
1863
        Someone in the channel set the topic.
 
1864
        """
 
1865
        user = string.split(prefix, '!')[0]
 
1866
        channel = params[0]
 
1867
        newtopic = params[1]
 
1868
        self.topicUpdated(user, channel, newtopic)
 
1869
 
 
1870
    def irc_RPL_TOPIC(self, prefix, params):
 
1871
        """
 
1872
        Called when the topic for a channel is initially reported or when it
 
1873
        subsequently changes.
 
1874
        """
 
1875
        user = string.split(prefix, '!')[0]
 
1876
        channel = params[1]
 
1877
        newtopic = params[2]
 
1878
        self.topicUpdated(user, channel, newtopic)
 
1879
 
 
1880
    def irc_RPL_NOTOPIC(self, prefix, params):
 
1881
        user = string.split(prefix, '!')[0]
 
1882
        channel = params[1]
 
1883
        newtopic = ""
 
1884
        self.topicUpdated(user, channel, newtopic)
 
1885
 
 
1886
    def irc_RPL_MOTDSTART(self, prefix, params):
 
1887
        if params[-1].startswith("- "):
 
1888
            params[-1] = params[-1][2:]
 
1889
        self.motd = [params[-1]]
 
1890
 
 
1891
    def irc_RPL_MOTD(self, prefix, params):
 
1892
        if params[-1].startswith("- "):
 
1893
            params[-1] = params[-1][2:]
 
1894
        if self.motd is None:
 
1895
            self.motd = []
 
1896
        self.motd.append(params[-1])
 
1897
 
 
1898
 
 
1899
    def irc_RPL_ENDOFMOTD(self, prefix, params):
 
1900
        """
 
1901
        I{RPL_ENDOFMOTD} indicates the end of the message of the day
 
1902
        messages.  Deliver the accumulated lines to C{receivedMOTD}.
 
1903
        """
 
1904
        motd = self.motd
 
1905
        self.motd = None
 
1906
        self.receivedMOTD(motd)
 
1907
 
 
1908
 
 
1909
    def irc_RPL_CREATED(self, prefix, params):
 
1910
        self.created(params[1])
 
1911
 
 
1912
    def irc_RPL_YOURHOST(self, prefix, params):
 
1913
        self.yourHost(params[1])
 
1914
 
 
1915
    def irc_RPL_MYINFO(self, prefix, params):
 
1916
        info = params[1].split(None, 3)
 
1917
        while len(info) < 4:
 
1918
            info.append(None)
 
1919
        self.myInfo(*info)
 
1920
 
 
1921
    def irc_RPL_BOUNCE(self, prefix, params):
 
1922
        self.bounce(params[1])
 
1923
 
 
1924
    def irc_RPL_ISUPPORT(self, prefix, params):
 
1925
        args = params[1:-1]
 
1926
        # Several ISUPPORT messages, in no particular order, may be sent
 
1927
        # to the client at any given point in time (usually only on connect,
 
1928
        # though.) For this reason, ServerSupportedFeatures.parse is intended
 
1929
        # to mutate the supported feature list.
 
1930
        self.supported.parse(args)
 
1931
        self.isupport(args)
 
1932
 
 
1933
    def irc_RPL_LUSERCLIENT(self, prefix, params):
 
1934
        self.luserClient(params[1])
 
1935
 
 
1936
    def irc_RPL_LUSEROP(self, prefix, params):
 
1937
        try:
 
1938
            self.luserOp(int(params[1]))
 
1939
        except ValueError:
 
1940
            pass
 
1941
 
 
1942
    def irc_RPL_LUSERCHANNELS(self, prefix, params):
 
1943
        try:
 
1944
            self.luserChannels(int(params[1]))
 
1945
        except ValueError:
 
1946
            pass
 
1947
 
 
1948
    def irc_RPL_LUSERME(self, prefix, params):
 
1949
        self.luserMe(params[1])
 
1950
 
 
1951
    def irc_unknown(self, prefix, command, params):
 
1952
        pass
 
1953
 
 
1954
    ### Receiving a CTCP query from another party
 
1955
    ### It is safe to leave these alone.
 
1956
 
 
1957
    def ctcpQuery(self, user, channel, messages):
 
1958
        """Dispatch method for any CTCP queries received.
 
1959
        """
 
1960
        for m in messages:
 
1961
            method = getattr(self, "ctcpQuery_%s" % m[0], None)
 
1962
            if method:
 
1963
                method(user, channel, m[1])
 
1964
            else:
 
1965
                self.ctcpUnknownQuery(user, channel, m[0], m[1])
 
1966
 
 
1967
    def ctcpQuery_ACTION(self, user, channel, data):
 
1968
        self.action(user, channel, data)
 
1969
 
 
1970
    def ctcpQuery_PING(self, user, channel, data):
 
1971
        nick = string.split(user,"!")[0]
 
1972
        self.ctcpMakeReply(nick, [("PING", data)])
 
1973
 
 
1974
    def ctcpQuery_FINGER(self, user, channel, data):
 
1975
        if data is not None:
 
1976
            self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
 
1977
                               % (user, data))
 
1978
        if not self.fingerReply:
 
1979
            return
 
1980
 
 
1981
        if callable(self.fingerReply):
 
1982
            reply = self.fingerReply()
 
1983
        else:
 
1984
            reply = str(self.fingerReply)
 
1985
 
 
1986
        nick = string.split(user,"!")[0]
 
1987
        self.ctcpMakeReply(nick, [('FINGER', reply)])
 
1988
 
 
1989
    def ctcpQuery_VERSION(self, user, channel, data):
 
1990
        if data is not None:
 
1991
            self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
 
1992
                               % (user, data))
 
1993
 
 
1994
        if self.versionName:
 
1995
            nick = string.split(user,"!")[0]
 
1996
            self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
 
1997
                                       (self.versionName,
 
1998
                                        self.versionNum or '',
 
1999
                                        self.versionEnv or ''))])
 
2000
 
 
2001
    def ctcpQuery_SOURCE(self, user, channel, data):
 
2002
        if data is not None:
 
2003
            self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
 
2004
                               % (user, data))
 
2005
        if self.sourceURL:
 
2006
            nick = string.split(user,"!")[0]
 
2007
            # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
 
2008
            # replies should be responded to with the location of an anonymous
 
2009
            # FTP server in host:directory:file format.  I'm taking the liberty
 
2010
            # of bringing it into the 21st century by sending a URL instead.
 
2011
            self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
 
2012
                                      ('SOURCE', None)])
 
2013
 
 
2014
    def ctcpQuery_USERINFO(self, user, channel, data):
 
2015
        if data is not None:
 
2016
            self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
 
2017
                               % (user, data))
 
2018
        if self.userinfo:
 
2019
            nick = string.split(user,"!")[0]
 
2020
            self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
 
2021
 
 
2022
    def ctcpQuery_CLIENTINFO(self, user, channel, data):
 
2023
        """A master index of what CTCP tags this client knows.
 
2024
 
 
2025
        If no arguments are provided, respond with a list of known tags.
 
2026
        If an argument is provided, provide human-readable help on
 
2027
        the usage of that tag.
 
2028
        """
 
2029
 
 
2030
        nick = string.split(user,"!")[0]
 
2031
        if not data:
 
2032
            # XXX: prefixedMethodNames gets methods from my *class*,
 
2033
            # but it's entirely possible that this *instance* has more
 
2034
            # methods.
 
2035
            names = reflect.prefixedMethodNames(self.__class__,
 
2036
                                                'ctcpQuery_')
 
2037
 
 
2038
            self.ctcpMakeReply(nick, [('CLIENTINFO',
 
2039
                                       string.join(names, ' '))])
 
2040
        else:
 
2041
            args = string.split(data)
 
2042
            method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
 
2043
            if not method:
 
2044
                self.ctcpMakeReply(nick, [('ERRMSG',
 
2045
                                           "CLIENTINFO %s :"
 
2046
                                           "Unknown query '%s'"
 
2047
                                           % (data, args[0]))])
 
2048
                return
 
2049
            doc = getattr(method, '__doc__', '')
 
2050
            self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
 
2051
 
 
2052
 
 
2053
    def ctcpQuery_ERRMSG(self, user, channel, data):
 
2054
        # Yeah, this seems strange, but that's what the spec says to do
 
2055
        # when faced with an ERRMSG query (not a reply).
 
2056
        nick = string.split(user,"!")[0]
 
2057
        self.ctcpMakeReply(nick, [('ERRMSG',
 
2058
                                   "%s :No error has occoured." % data)])
 
2059
 
 
2060
    def ctcpQuery_TIME(self, user, channel, data):
 
2061
        if data is not None:
 
2062
            self.quirkyMessage("Why did %s send '%s' with a TIME query?"
 
2063
                               % (user, data))
 
2064
        nick = string.split(user,"!")[0]
 
2065
        self.ctcpMakeReply(nick,
 
2066
                           [('TIME', ':%s' %
 
2067
                             time.asctime(time.localtime(time.time())))])
 
2068
 
 
2069
    def ctcpQuery_DCC(self, user, channel, data):
 
2070
        """Initiate a Direct Client Connection
 
2071
        """
 
2072
 
 
2073
        if not data: return
 
2074
        dcctype = data.split(None, 1)[0].upper()
 
2075
        handler = getattr(self, "dcc_" + dcctype, None)
 
2076
        if handler:
 
2077
            if self.dcc_sessions is None:
 
2078
                self.dcc_sessions = []
 
2079
            data = data[len(dcctype)+1:]
 
2080
            handler(user, channel, data)
 
2081
        else:
 
2082
            nick = string.split(user,"!")[0]
 
2083
            self.ctcpMakeReply(nick, [('ERRMSG',
 
2084
                                       "DCC %s :Unknown DCC type '%s'"
 
2085
                                       % (data, dcctype))])
 
2086
            self.quirkyMessage("%s offered unknown DCC type %s"
 
2087
                               % (user, dcctype))
 
2088
 
 
2089
    def dcc_SEND(self, user, channel, data):
 
2090
        # Use splitQuoted for those who send files with spaces in the names.
 
2091
        data = text.splitQuoted(data)
 
2092
        if len(data) < 3:
 
2093
            raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
 
2094
 
 
2095
        (filename, address, port) = data[:3]
 
2096
 
 
2097
        address = dccParseAddress(address)
 
2098
        try:
 
2099
            port = int(port)
 
2100
        except ValueError:
 
2101
            raise IRCBadMessage, "Indecipherable port %r" % (port,)
 
2102
 
 
2103
        size = -1
 
2104
        if len(data) >= 4:
 
2105
            try:
 
2106
                size = int(data[3])
 
2107
            except ValueError:
 
2108
                pass
 
2109
 
 
2110
        # XXX Should we bother passing this data?
 
2111
        self.dccDoSend(user, address, port, filename, size, data)
 
2112
 
 
2113
    def dcc_ACCEPT(self, user, channel, data):
 
2114
        data = text.splitQuoted(data)
 
2115
        if len(data) < 3:
 
2116
            raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
 
2117
        (filename, port, resumePos) = data[:3]
 
2118
        try:
 
2119
            port = int(port)
 
2120
            resumePos = int(resumePos)
 
2121
        except ValueError:
 
2122
            return
 
2123
 
 
2124
        self.dccDoAcceptResume(user, filename, port, resumePos)
 
2125
 
 
2126
    def dcc_RESUME(self, user, channel, data):
 
2127
        data = text.splitQuoted(data)
 
2128
        if len(data) < 3:
 
2129
            raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
 
2130
        (filename, port, resumePos) = data[:3]
 
2131
        try:
 
2132
            port = int(port)
 
2133
            resumePos = int(resumePos)
 
2134
        except ValueError:
 
2135
            return
 
2136
        self.dccDoResume(user, filename, port, resumePos)
 
2137
 
 
2138
    def dcc_CHAT(self, user, channel, data):
 
2139
        data = text.splitQuoted(data)
 
2140
        if len(data) < 3:
 
2141
            raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
 
2142
 
 
2143
        (filename, address, port) = data[:3]
 
2144
 
 
2145
        address = dccParseAddress(address)
 
2146
        try:
 
2147
            port = int(port)
 
2148
        except ValueError:
 
2149
            raise IRCBadMessage, "Indecipherable port %r" % (port,)
 
2150
 
 
2151
        self.dccDoChat(user, channel, address, port, data)
 
2152
 
 
2153
    ### The dccDo methods are the slightly higher-level siblings of
 
2154
    ### common dcc_ methods; the arguments have been parsed for them.
 
2155
 
 
2156
    def dccDoSend(self, user, address, port, fileName, size, data):
 
2157
        """Called when I receive a DCC SEND offer from a client.
 
2158
 
 
2159
        By default, I do nothing here."""
 
2160
        ## filename = path.basename(arg)
 
2161
        ## protocol = DccFileReceive(filename, size,
 
2162
        ##                           (user,channel,data),self.dcc_destdir)
 
2163
        ## reactor.clientTCP(address, port, protocol)
 
2164
        ## self.dcc_sessions.append(protocol)
 
2165
        pass
 
2166
 
 
2167
    def dccDoResume(self, user, file, port, resumePos):
 
2168
        """Called when a client is trying to resume an offered file
 
2169
        via DCC send.  It should be either replied to with a DCC
 
2170
        ACCEPT or ignored (default)."""
 
2171
        pass
 
2172
 
 
2173
    def dccDoAcceptResume(self, user, file, port, resumePos):
 
2174
        """Called when a client has verified and accepted a DCC resume
 
2175
        request made by us.  By default it will do nothing."""
 
2176
        pass
 
2177
 
 
2178
    def dccDoChat(self, user, channel, address, port, data):
 
2179
        pass
 
2180
        #factory = DccChatFactory(self, queryData=(user, channel, data))
 
2181
        #reactor.connectTCP(address, port, factory)
 
2182
        #self.dcc_sessions.append(factory)
 
2183
 
 
2184
    #def ctcpQuery_SED(self, user, data):
 
2185
    #    """Simple Encryption Doodoo
 
2186
    #
 
2187
    #    Feel free to implement this, but no specification is available.
 
2188
    #    """
 
2189
    #    raise NotImplementedError
 
2190
 
 
2191
    def ctcpUnknownQuery(self, user, channel, tag, data):
 
2192
        nick = string.split(user,"!")[0]
 
2193
        self.ctcpMakeReply(nick, [('ERRMSG',
 
2194
                                   "%s %s: Unknown query '%s'"
 
2195
                                   % (tag, data, tag))])
 
2196
 
 
2197
        log.msg("Unknown CTCP query from %s: %s %s\n"
 
2198
                 % (user, tag, data))
 
2199
 
 
2200
    def ctcpMakeReply(self, user, messages):
 
2201
        """
 
2202
        Send one or more C{extended messages} as a CTCP reply.
 
2203
 
 
2204
        @type messages: a list of extended messages.  An extended
 
2205
        message is a (tag, data) tuple, where 'data' may be C{None}.
 
2206
        """
 
2207
        self.notice(user, ctcpStringify(messages))
 
2208
 
 
2209
    ### client CTCP query commands
 
2210
 
 
2211
    def ctcpMakeQuery(self, user, messages):
 
2212
        """
 
2213
        Send one or more C{extended messages} as a CTCP query.
 
2214
 
 
2215
        @type messages: a list of extended messages.  An extended
 
2216
        message is a (tag, data) tuple, where 'data' may be C{None}.
 
2217
        """
 
2218
        self.msg(user, ctcpStringify(messages))
 
2219
 
 
2220
    ### Receiving a response to a CTCP query (presumably to one we made)
 
2221
    ### You may want to add methods here, or override UnknownReply.
 
2222
 
 
2223
    def ctcpReply(self, user, channel, messages):
 
2224
        """
 
2225
        Dispatch method for any CTCP replies received.
 
2226
        """
 
2227
        for m in messages:
 
2228
            method = getattr(self, "ctcpReply_%s" % m[0], None)
 
2229
            if method:
 
2230
                method(user, channel, m[1])
 
2231
            else:
 
2232
                self.ctcpUnknownReply(user, channel, m[0], m[1])
 
2233
 
 
2234
    def ctcpReply_PING(self, user, channel, data):
 
2235
        nick = user.split('!', 1)[0]
 
2236
        if (not self._pings) or (not self._pings.has_key((nick, data))):
 
2237
            raise IRCBadMessage,\
 
2238
                  "Bogus PING response from %s: %s" % (user, data)
 
2239
 
 
2240
        t0 = self._pings[(nick, data)]
 
2241
        self.pong(user, time.time() - t0)
 
2242
 
 
2243
    def ctcpUnknownReply(self, user, channel, tag, data):
 
2244
        """Called when a fitting ctcpReply_ method is not found.
 
2245
 
 
2246
        XXX: If the client makes arbitrary CTCP queries,
 
2247
        this method should probably show the responses to
 
2248
        them instead of treating them as anomolies.
 
2249
        """
 
2250
        log.msg("Unknown CTCP reply from %s: %s %s\n"
 
2251
                 % (user, tag, data))
 
2252
 
 
2253
    ### Error handlers
 
2254
    ### You may override these with something more appropriate to your UI.
 
2255
 
 
2256
    def badMessage(self, line, excType, excValue, tb):
 
2257
        """When I get a message that's so broken I can't use it.
 
2258
        """
 
2259
        log.msg(line)
 
2260
        log.msg(string.join(traceback.format_exception(excType,
 
2261
                                                        excValue,
 
2262
                                                        tb),''))
 
2263
 
 
2264
    def quirkyMessage(self, s):
 
2265
        """This is called when I receive a message which is peculiar,
 
2266
        but not wholly indecipherable.
 
2267
        """
 
2268
        log.msg(s + '\n')
 
2269
 
 
2270
    ### Protocool methods
 
2271
 
 
2272
    def connectionMade(self):
 
2273
        self.supported = ServerSupportedFeatures()
 
2274
        self._queue = []
 
2275
        if self.performLogin:
 
2276
            self.register(self.nickname)
 
2277
 
 
2278
    def dataReceived(self, data):
 
2279
        basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
 
2280
 
 
2281
    def lineReceived(self, line):
 
2282
        line = lowDequote(line)
 
2283
        try:
 
2284
            prefix, command, params = parsemsg(line)
 
2285
            if numeric_to_symbolic.has_key(command):
 
2286
                command = numeric_to_symbolic[command]
 
2287
            self.handleCommand(command, prefix, params)
 
2288
        except IRCBadMessage:
 
2289
            self.badMessage(line, *sys.exc_info())
 
2290
 
 
2291
 
 
2292
    def getUserModeParams(self):
 
2293
        """
 
2294
        Get user modes that require parameters for correct parsing.
 
2295
 
 
2296
        @rtype: C{[str, str]}
 
2297
        @return C{[add, remove]}
 
2298
        """
 
2299
        return ['', '']
 
2300
 
 
2301
 
 
2302
    def getChannelModeParams(self):
 
2303
        """
 
2304
        Get channel modes that require parameters for correct parsing.
 
2305
 
 
2306
        @rtype: C{[str, str]}
 
2307
        @return C{[add, remove]}
 
2308
        """
 
2309
        # PREFIX modes are treated as "type B" CHANMODES, they always take
 
2310
        # parameter.
 
2311
        params = ['', '']
 
2312
        prefixes = self.supported.getFeature('PREFIX', {})
 
2313
        params[0] = params[1] = ''.join(prefixes.iterkeys())
 
2314
 
 
2315
        chanmodes = self.supported.getFeature('CHANMODES')
 
2316
        if chanmodes is not None:
 
2317
            params[0] += chanmodes.get('addressModes', '')
 
2318
            params[0] += chanmodes.get('param', '')
 
2319
            params[1] = params[0]
 
2320
            params[0] += chanmodes.get('setParam', '')
 
2321
        return params
 
2322
 
 
2323
 
 
2324
    def handleCommand(self, command, prefix, params):
 
2325
        """Determine the function to call for the given command and call
 
2326
        it with the given arguments.
 
2327
        """
 
2328
        method = getattr(self, "irc_%s" % command, None)
 
2329
        try:
 
2330
            if method is not None:
 
2331
                method(prefix, params)
 
2332
            else:
 
2333
                self.irc_unknown(prefix, command, params)
 
2334
        except:
 
2335
            log.deferr()
 
2336
 
 
2337
 
 
2338
    def __getstate__(self):
 
2339
        dct = self.__dict__.copy()
 
2340
        dct['dcc_sessions'] = None
 
2341
        dct['_pings'] = None
 
2342
        return dct
 
2343
 
 
2344
 
 
2345
def dccParseAddress(address):
 
2346
    if '.' in address:
 
2347
        pass
 
2348
    else:
 
2349
        try:
 
2350
            address = long(address)
 
2351
        except ValueError:
 
2352
            raise IRCBadMessage,\
 
2353
                  "Indecipherable address %r" % (address,)
 
2354
        else:
 
2355
            address = (
 
2356
                (address >> 24) & 0xFF,
 
2357
                (address >> 16) & 0xFF,
 
2358
                (address >> 8) & 0xFF,
 
2359
                address & 0xFF,
 
2360
                )
 
2361
            address = '.'.join(map(str,address))
 
2362
    return address
 
2363
 
 
2364
 
 
2365
class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
 
2366
    """Bare protocol to receive a Direct Client Connection SEND stream.
 
2367
 
 
2368
    This does enough to keep the other guy talking, but you'll want to
 
2369
    extend my dataReceived method to *do* something with the data I get.
 
2370
    """
 
2371
 
 
2372
    bytesReceived = 0
 
2373
 
 
2374
    def __init__(self, resumeOffset=0):
 
2375
        self.bytesReceived = resumeOffset
 
2376
        self.resume = (resumeOffset != 0)
 
2377
 
 
2378
    def dataReceived(self, data):
 
2379
        """Called when data is received.
 
2380
 
 
2381
        Warning: This just acknowledges to the remote host that the
 
2382
        data has been received; it doesn't *do* anything with the
 
2383
        data, so you'll want to override this.
 
2384
        """
 
2385
        self.bytesReceived = self.bytesReceived + len(data)
 
2386
        self.transport.write(struct.pack('!i', self.bytesReceived))
 
2387
 
 
2388
 
 
2389
class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
 
2390
    """Protocol for an outgoing Direct Client Connection SEND.
 
2391
    """
 
2392
 
 
2393
    blocksize = 1024
 
2394
    file = None
 
2395
    bytesSent = 0
 
2396
    completed = 0
 
2397
    connected = 0
 
2398
 
 
2399
    def __init__(self, file):
 
2400
        if type(file) is types.StringType:
 
2401
            self.file = open(file, 'r')
 
2402
 
 
2403
    def connectionMade(self):
 
2404
        self.connected = 1
 
2405
        self.sendBlock()
 
2406
 
 
2407
    def dataReceived(self, data):
 
2408
        # XXX: Do we need to check to see if len(data) != fmtsize?
 
2409
 
 
2410
        bytesShesGot = struct.unpack("!I", data)
 
2411
        if bytesShesGot < self.bytesSent:
 
2412
            # Wait for her.
 
2413
            # XXX? Add some checks to see if we've stalled out?
 
2414
            return
 
2415
        elif bytesShesGot > self.bytesSent:
 
2416
            # self.transport.log("DCC SEND %s: She says she has %d bytes "
 
2417
            #                    "but I've only sent %d.  I'm stopping "
 
2418
            #                    "this screwy transfer."
 
2419
            #                    % (self.file,
 
2420
            #                       bytesShesGot, self.bytesSent))
 
2421
            self.transport.loseConnection()
 
2422
            return
 
2423
 
 
2424
        self.sendBlock()
 
2425
 
 
2426
    def sendBlock(self):
 
2427
        block = self.file.read(self.blocksize)
 
2428
        if block:
 
2429
            self.transport.write(block)
 
2430
            self.bytesSent = self.bytesSent + len(block)
 
2431
        else:
 
2432
            # Nothing more to send, transfer complete.
 
2433
            self.transport.loseConnection()
 
2434
            self.completed = 1
 
2435
 
 
2436
    def connectionLost(self, reason):
 
2437
        self.connected = 0
 
2438
        if hasattr(self.file, "close"):
 
2439
            self.file.close()
 
2440
 
 
2441
 
 
2442
class DccSendFactory(protocol.Factory):
 
2443
    protocol = DccSendProtocol
 
2444
    def __init__(self, file):
 
2445
        self.file = file
 
2446
 
 
2447
    def buildProtocol(self, connection):
 
2448
        p = self.protocol(self.file)
 
2449
        p.factory = self
 
2450
        return p
 
2451
 
 
2452
 
 
2453
def fileSize(file):
 
2454
    """I'll try my damndest to determine the size of this file object.
 
2455
    """
 
2456
    size = None
 
2457
    if hasattr(file, "fileno"):
 
2458
        fileno = file.fileno()
 
2459
        try:
 
2460
            stat_ = os.fstat(fileno)
 
2461
            size = stat_[stat.ST_SIZE]
 
2462
        except:
 
2463
            pass
 
2464
        else:
 
2465
            return size
 
2466
 
 
2467
    if hasattr(file, "name") and path.exists(file.name):
 
2468
        try:
 
2469
            size = path.getsize(file.name)
 
2470
        except:
 
2471
            pass
 
2472
        else:
 
2473
            return size
 
2474
 
 
2475
    if hasattr(file, "seek") and hasattr(file, "tell"):
 
2476
        try:
 
2477
            try:
 
2478
                file.seek(0, 2)
 
2479
                size = file.tell()
 
2480
            finally:
 
2481
                file.seek(0, 0)
 
2482
        except:
 
2483
            pass
 
2484
        else:
 
2485
            return size
 
2486
 
 
2487
    return size
 
2488
 
 
2489
class DccChat(basic.LineReceiver, styles.Ephemeral):
 
2490
    """Direct Client Connection protocol type CHAT.
 
2491
 
 
2492
    DCC CHAT is really just your run o' the mill basic.LineReceiver
 
2493
    protocol.  This class only varies from that slightly, accepting
 
2494
    either LF or CR LF for a line delimeter for incoming messages
 
2495
    while always using CR LF for outgoing.
 
2496
 
 
2497
    The lineReceived method implemented here uses the DCC connection's
 
2498
    'client' attribute (provided upon construction) to deliver incoming
 
2499
    lines from the DCC chat via IRCClient's normal privmsg interface.
 
2500
    That's something of a spoof, which you may well want to override.
 
2501
    """
 
2502
 
 
2503
    queryData = None
 
2504
    delimiter = CR + NL
 
2505
    client = None
 
2506
    remoteParty = None
 
2507
    buffer = ""
 
2508
 
 
2509
    def __init__(self, client, queryData=None):
 
2510
        """Initialize a new DCC CHAT session.
 
2511
 
 
2512
        queryData is a 3-tuple of
 
2513
        (fromUser, targetUserOrChannel, data)
 
2514
        as received by the CTCP query.
 
2515
 
 
2516
        (To be honest, fromUser is the only thing that's currently
 
2517
        used here. targetUserOrChannel is potentially useful, while
 
2518
        the 'data' argument is soley for informational purposes.)
 
2519
        """
 
2520
        self.client = client
 
2521
        if queryData:
 
2522
            self.queryData = queryData
 
2523
            self.remoteParty = self.queryData[0]
 
2524
 
 
2525
    def dataReceived(self, data):
 
2526
        self.buffer = self.buffer + data
 
2527
        lines = string.split(self.buffer, LF)
 
2528
        # Put the (possibly empty) element after the last LF back in the
 
2529
        # buffer
 
2530
        self.buffer = lines.pop()
 
2531
 
 
2532
        for line in lines:
 
2533
            if line[-1] == CR:
 
2534
                line = line[:-1]
 
2535
            self.lineReceived(line)
 
2536
 
 
2537
    def lineReceived(self, line):
 
2538
        log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
 
2539
        self.client.privmsg(self.remoteParty,
 
2540
                            self.client.nickname, line)
 
2541
 
 
2542
 
 
2543
class DccChatFactory(protocol.ClientFactory):
 
2544
    protocol = DccChat
 
2545
    noisy = 0
 
2546
    def __init__(self, client, queryData):
 
2547
        self.client = client
 
2548
        self.queryData = queryData
 
2549
 
 
2550
    def buildProtocol(self, addr):
 
2551
        p = self.protocol(client=self.client, queryData=self.queryData)
 
2552
        p.factory = self
 
2553
 
 
2554
    def clientConnectionFailed(self, unused_connector, unused_reason):
 
2555
        self.client.dcc_sessions.remove(self)
 
2556
 
 
2557
    def clientConnectionLost(self, unused_connector, unused_reason):
 
2558
        self.client.dcc_sessions.remove(self)
 
2559
 
 
2560
 
 
2561
def dccDescribe(data):
 
2562
    """Given the data chunk from a DCC query, return a descriptive string.
 
2563
    """
 
2564
 
 
2565
    orig_data = data
 
2566
    data = string.split(data)
 
2567
    if len(data) < 4:
 
2568
        return orig_data
 
2569
 
 
2570
    (dcctype, arg, address, port) = data[:4]
 
2571
 
 
2572
    if '.' in address:
 
2573
        pass
 
2574
    else:
 
2575
        try:
 
2576
            address = long(address)
 
2577
        except ValueError:
 
2578
            pass
 
2579
        else:
 
2580
            address = (
 
2581
                (address >> 24) & 0xFF,
 
2582
                (address >> 16) & 0xFF,
 
2583
                (address >> 8) & 0xFF,
 
2584
                address & 0xFF,
 
2585
                )
 
2586
            # The mapping to 'int' is to get rid of those accursed
 
2587
            # "L"s which python 1.5.2 puts on the end of longs.
 
2588
            address = string.join(map(str,map(int,address)), ".")
 
2589
 
 
2590
    if dcctype == 'SEND':
 
2591
        filename = arg
 
2592
 
 
2593
        size_txt = ''
 
2594
        if len(data) >= 5:
 
2595
            try:
 
2596
                size = int(data[4])
 
2597
                size_txt = ' of size %d bytes' % (size,)
 
2598
            except ValueError:
 
2599
                pass
 
2600
 
 
2601
        dcc_text = ("SEND for file '%s'%s at host %s, port %s"
 
2602
                    % (filename, size_txt, address, port))
 
2603
    elif dcctype == 'CHAT':
 
2604
        dcc_text = ("CHAT for host %s, port %s"
 
2605
                    % (address, port))
 
2606
    else:
 
2607
        dcc_text = orig_data
 
2608
 
 
2609
    return dcc_text
 
2610
 
 
2611
 
 
2612
class DccFileReceive(DccFileReceiveBasic):
 
2613
    """Higher-level coverage for getting a file from DCC SEND.
 
2614
 
 
2615
    I allow you to change the file's name and destination directory.
 
2616
    I won't overwrite an existing file unless I've been told it's okay
 
2617
    to do so. If passed the resumeOffset keyword argument I will attempt to
 
2618
    resume the file from that amount of bytes.
 
2619
 
 
2620
    XXX: I need to let the client know when I am finished.
 
2621
    XXX: I need to decide how to keep a progress indicator updated.
 
2622
    XXX: Client needs a way to tell me "Do not finish until I say so."
 
2623
    XXX: I need to make sure the client understands if the file cannot be written.
 
2624
    """
 
2625
 
 
2626
    filename = 'dcc'
 
2627
    fileSize = -1
 
2628
    destDir = '.'
 
2629
    overwrite = 0
 
2630
    fromUser = None
 
2631
    queryData = None
 
2632
 
 
2633
    def __init__(self, filename, fileSize=-1, queryData=None,
 
2634
                 destDir='.', resumeOffset=0):
 
2635
        DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
 
2636
        self.filename = filename
 
2637
        self.destDir = destDir
 
2638
        self.fileSize = fileSize
 
2639
 
 
2640
        if queryData:
 
2641
            self.queryData = queryData
 
2642
            self.fromUser = self.queryData[0]
 
2643
 
 
2644
    def set_directory(self, directory):
 
2645
        """Set the directory where the downloaded file will be placed.
 
2646
 
 
2647
        May raise OSError if the supplied directory path is not suitable.
 
2648
        """
 
2649
        if not path.exists(directory):
 
2650
            raise OSError(errno.ENOENT, "You see no directory there.",
 
2651
                          directory)
 
2652
        if not path.isdir(directory):
 
2653
            raise OSError(errno.ENOTDIR, "You cannot put a file into "
 
2654
                          "something which is not a directory.",
 
2655
                          directory)
 
2656
        if not os.access(directory, os.X_OK | os.W_OK):
 
2657
            raise OSError(errno.EACCES,
 
2658
                          "This directory is too hard to write in to.",
 
2659
                          directory)
 
2660
        self.destDir = directory
 
2661
 
 
2662
    def set_filename(self, filename):
 
2663
        """Change the name of the file being transferred.
 
2664
 
 
2665
        This replaces the file name provided by the sender.
 
2666
        """
 
2667
        self.filename = filename
 
2668
 
 
2669
    def set_overwrite(self, boolean):
 
2670
        """May I overwrite existing files?
 
2671
        """
 
2672
        self.overwrite = boolean
 
2673
 
 
2674
 
 
2675
    # Protocol-level methods.
 
2676
 
 
2677
    def connectionMade(self):
 
2678
        dst = path.abspath(path.join(self.destDir,self.filename))
 
2679
        exists = path.exists(dst)
 
2680
        if self.resume and exists:
 
2681
            # I have been told I want to resume, and a file already
 
2682
            # exists - Here we go
 
2683
            self.file = open(dst, 'ab')
 
2684
            log.msg("Attempting to resume %s - starting from %d bytes" %
 
2685
                    (self.file, self.file.tell()))
 
2686
        elif self.overwrite or not exists:
 
2687
            self.file = open(dst, 'wb')
 
2688
        else:
 
2689
            raise OSError(errno.EEXIST,
 
2690
                          "There's a file in the way.  "
 
2691
                          "Perhaps that's why you cannot open it.",
 
2692
                          dst)
 
2693
 
 
2694
    def dataReceived(self, data):
 
2695
        self.file.write(data)
 
2696
        DccFileReceiveBasic.dataReceived(self, data)
 
2697
 
 
2698
        # XXX: update a progress indicator here?
 
2699
 
 
2700
    def connectionLost(self, reason):
 
2701
        """When the connection is lost, I close the file.
 
2702
        """
 
2703
        self.connected = 0
 
2704
        logmsg = ("%s closed." % (self,))
 
2705
        if self.fileSize > 0:
 
2706
            logmsg = ("%s  %d/%d bytes received"
 
2707
                      % (logmsg, self.bytesReceived, self.fileSize))
 
2708
            if self.bytesReceived == self.fileSize:
 
2709
                pass # Hooray!
 
2710
            elif self.bytesReceived < self.fileSize:
 
2711
                logmsg = ("%s (Warning: %d bytes short)"
 
2712
                          % (logmsg, self.fileSize - self.bytesReceived))
 
2713
            else:
 
2714
                logmsg = ("%s (file larger than expected)"
 
2715
                          % (logmsg,))
 
2716
        else:
 
2717
            logmsg = ("%s  %d bytes received"
 
2718
                      % (logmsg, self.bytesReceived))
 
2719
 
 
2720
        if hasattr(self, 'file'):
 
2721
            logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
 
2722
            if hasattr(self.file, 'close'): self.file.close()
 
2723
 
 
2724
        # self.transport.log(logmsg)
 
2725
 
 
2726
    def __str__(self):
 
2727
        if not self.connected:
 
2728
            return "<Unconnected DccFileReceive object at %x>" % (id(self),)
 
2729
        from_ = self.transport.getPeer()
 
2730
        if self.fromUser:
 
2731
            from_ = "%s (%s)" % (self.fromUser, from_)
 
2732
 
 
2733
        s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
 
2734
        return s
 
2735
 
 
2736
    def __repr__(self):
 
2737
        s = ("<%s at %x: GET %s>"
 
2738
             % (self.__class__, id(self), self.filename))
 
2739
        return s
 
2740
 
 
2741
 
 
2742
# CTCP constants and helper functions
 
2743
 
 
2744
X_DELIM = chr(001)
 
2745
 
 
2746
def ctcpExtract(message):
 
2747
    """Extract CTCP data from a string.
 
2748
 
 
2749
    Returns a dictionary with two items:
 
2750
 
 
2751
       - C{'extended'}: a list of CTCP (tag, data) tuples
 
2752
       - C{'normal'}: a list of strings which were not inside a CTCP delimeter
 
2753
    """
 
2754
 
 
2755
    extended_messages = []
 
2756
    normal_messages = []
 
2757
    retval = {'extended': extended_messages,
 
2758
              'normal': normal_messages }
 
2759
 
 
2760
    messages = string.split(message, X_DELIM)
 
2761
    odd = 0
 
2762
 
 
2763
    # X1 extended data X2 nomal data X3 extended data X4 normal...
 
2764
    while messages:
 
2765
        if odd:
 
2766
            extended_messages.append(messages.pop(0))
 
2767
        else:
 
2768
            normal_messages.append(messages.pop(0))
 
2769
        odd = not odd
 
2770
 
 
2771
    extended_messages[:] = filter(None, extended_messages)
 
2772
    normal_messages[:] = filter(None, normal_messages)
 
2773
 
 
2774
    extended_messages[:] = map(ctcpDequote, extended_messages)
 
2775
    for i in xrange(len(extended_messages)):
 
2776
        m = string.split(extended_messages[i], SPC, 1)
 
2777
        tag = m[0]
 
2778
        if len(m) > 1:
 
2779
            data = m[1]
 
2780
        else:
 
2781
            data = None
 
2782
 
 
2783
        extended_messages[i] = (tag, data)
 
2784
 
 
2785
    return retval
 
2786
 
 
2787
# CTCP escaping
 
2788
 
 
2789
M_QUOTE= chr(020)
 
2790
 
 
2791
mQuoteTable = {
 
2792
    NUL: M_QUOTE + '0',
 
2793
    NL: M_QUOTE + 'n',
 
2794
    CR: M_QUOTE + 'r',
 
2795
    M_QUOTE: M_QUOTE + M_QUOTE
 
2796
    }
 
2797
 
 
2798
mDequoteTable = {}
 
2799
for k, v in mQuoteTable.items():
 
2800
    mDequoteTable[v[-1]] = k
 
2801
del k, v
 
2802
 
 
2803
mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
 
2804
 
 
2805
def lowQuote(s):
 
2806
    for c in (M_QUOTE, NUL, NL, CR):
 
2807
        s = string.replace(s, c, mQuoteTable[c])
 
2808
    return s
 
2809
 
 
2810
def lowDequote(s):
 
2811
    def sub(matchobj, mDequoteTable=mDequoteTable):
 
2812
        s = matchobj.group()[1]
 
2813
        try:
 
2814
            s = mDequoteTable[s]
 
2815
        except KeyError:
 
2816
            s = s
 
2817
        return s
 
2818
 
 
2819
    return mEscape_re.sub(sub, s)
 
2820
 
 
2821
X_QUOTE = '\\'
 
2822
 
 
2823
xQuoteTable = {
 
2824
    X_DELIM: X_QUOTE + 'a',
 
2825
    X_QUOTE: X_QUOTE + X_QUOTE
 
2826
    }
 
2827
 
 
2828
xDequoteTable = {}
 
2829
 
 
2830
for k, v in xQuoteTable.items():
 
2831
    xDequoteTable[v[-1]] = k
 
2832
 
 
2833
xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
 
2834
 
 
2835
def ctcpQuote(s):
 
2836
    for c in (X_QUOTE, X_DELIM):
 
2837
        s = string.replace(s, c, xQuoteTable[c])
 
2838
    return s
 
2839
 
 
2840
def ctcpDequote(s):
 
2841
    def sub(matchobj, xDequoteTable=xDequoteTable):
 
2842
        s = matchobj.group()[1]
 
2843
        try:
 
2844
            s = xDequoteTable[s]
 
2845
        except KeyError:
 
2846
            s = s
 
2847
        return s
 
2848
 
 
2849
    return xEscape_re.sub(sub, s)
 
2850
 
 
2851
def ctcpStringify(messages):
 
2852
    """
 
2853
    @type messages: a list of extended messages.  An extended
 
2854
    message is a (tag, data) tuple, where 'data' may be C{None}, a
 
2855
    string, or a list of strings to be joined with whitespace.
 
2856
 
 
2857
    @returns: String
 
2858
    """
 
2859
    coded_messages = []
 
2860
    for (tag, data) in messages:
 
2861
        if data:
 
2862
            if not isinstance(data, types.StringType):
 
2863
                try:
 
2864
                    # data as list-of-strings
 
2865
                    data = " ".join(map(str, data))
 
2866
                except TypeError:
 
2867
                    # No?  Then use it's %s representation.
 
2868
                    pass
 
2869
            m = "%s %s" % (tag, data)
 
2870
        else:
 
2871
            m = str(tag)
 
2872
        m = ctcpQuote(m)
 
2873
        m = "%s%s%s" % (X_DELIM, m, X_DELIM)
 
2874
        coded_messages.append(m)
 
2875
 
 
2876
    line = string.join(coded_messages, '')
 
2877
    return line
 
2878
 
 
2879
 
 
2880
# Constants (from RFC 2812)
 
2881
RPL_WELCOME = '001'
 
2882
RPL_YOURHOST = '002'
 
2883
RPL_CREATED = '003'
 
2884
RPL_MYINFO = '004'
 
2885
RPL_ISUPPORT = '005'
 
2886
RPL_BOUNCE = '010'
 
2887
RPL_USERHOST = '302'
 
2888
RPL_ISON = '303'
 
2889
RPL_AWAY = '301'
 
2890
RPL_UNAWAY = '305'
 
2891
RPL_NOWAWAY = '306'
 
2892
RPL_WHOISUSER = '311'
 
2893
RPL_WHOISSERVER = '312'
 
2894
RPL_WHOISOPERATOR = '313'
 
2895
RPL_WHOISIDLE = '317'
 
2896
RPL_ENDOFWHOIS = '318'
 
2897
RPL_WHOISCHANNELS = '319'
 
2898
RPL_WHOWASUSER = '314'
 
2899
RPL_ENDOFWHOWAS = '369'
 
2900
RPL_LISTSTART = '321'
 
2901
RPL_LIST = '322'
 
2902
RPL_LISTEND = '323'
 
2903
RPL_UNIQOPIS = '325'
 
2904
RPL_CHANNELMODEIS = '324'
 
2905
RPL_NOTOPIC = '331'
 
2906
RPL_TOPIC = '332'
 
2907
RPL_INVITING = '341'
 
2908
RPL_SUMMONING = '342'
 
2909
RPL_INVITELIST = '346'
 
2910
RPL_ENDOFINVITELIST = '347'
 
2911
RPL_EXCEPTLIST = '348'
 
2912
RPL_ENDOFEXCEPTLIST = '349'
 
2913
RPL_VERSION = '351'
 
2914
RPL_WHOREPLY = '352'
 
2915
RPL_ENDOFWHO = '315'
 
2916
RPL_NAMREPLY = '353'
 
2917
RPL_ENDOFNAMES = '366'
 
2918
RPL_LINKS = '364'
 
2919
RPL_ENDOFLINKS = '365'
 
2920
RPL_BANLIST = '367'
 
2921
RPL_ENDOFBANLIST = '368'
 
2922
RPL_INFO = '371'
 
2923
RPL_ENDOFINFO = '374'
 
2924
RPL_MOTDSTART = '375'
 
2925
RPL_MOTD = '372'
 
2926
RPL_ENDOFMOTD = '376'
 
2927
RPL_YOUREOPER = '381'
 
2928
RPL_REHASHING = '382'
 
2929
RPL_YOURESERVICE = '383'
 
2930
RPL_TIME = '391'
 
2931
RPL_USERSSTART = '392'
 
2932
RPL_USERS = '393'
 
2933
RPL_ENDOFUSERS = '394'
 
2934
RPL_NOUSERS = '395'
 
2935
RPL_TRACELINK = '200'
 
2936
RPL_TRACECONNECTING = '201'
 
2937
RPL_TRACEHANDSHAKE = '202'
 
2938
RPL_TRACEUNKNOWN = '203'
 
2939
RPL_TRACEOPERATOR = '204'
 
2940
RPL_TRACEUSER = '205'
 
2941
RPL_TRACESERVER = '206'
 
2942
RPL_TRACESERVICE = '207'
 
2943
RPL_TRACENEWTYPE = '208'
 
2944
RPL_TRACECLASS = '209'
 
2945
RPL_TRACERECONNECT = '210'
 
2946
RPL_TRACELOG = '261'
 
2947
RPL_TRACEEND = '262'
 
2948
RPL_STATSLINKINFO = '211'
 
2949
RPL_STATSCOMMANDS = '212'
 
2950
RPL_ENDOFSTATS = '219'
 
2951
RPL_STATSUPTIME = '242'
 
2952
RPL_STATSOLINE = '243'
 
2953
RPL_UMODEIS = '221'
 
2954
RPL_SERVLIST = '234'
 
2955
RPL_SERVLISTEND = '235'
 
2956
RPL_LUSERCLIENT = '251'
 
2957
RPL_LUSEROP = '252'
 
2958
RPL_LUSERUNKNOWN = '253'
 
2959
RPL_LUSERCHANNELS = '254'
 
2960
RPL_LUSERME = '255'
 
2961
RPL_ADMINME = '256'
 
2962
RPL_ADMINLOC = '257'
 
2963
RPL_ADMINLOC = '258'
 
2964
RPL_ADMINEMAIL = '259'
 
2965
RPL_TRYAGAIN = '263'
 
2966
ERR_NOSUCHNICK = '401'
 
2967
ERR_NOSUCHSERVER = '402'
 
2968
ERR_NOSUCHCHANNEL = '403'
 
2969
ERR_CANNOTSENDTOCHAN = '404'
 
2970
ERR_TOOMANYCHANNELS = '405'
 
2971
ERR_WASNOSUCHNICK = '406'
 
2972
ERR_TOOMANYTARGETS = '407'
 
2973
ERR_NOSUCHSERVICE = '408'
 
2974
ERR_NOORIGIN = '409'
 
2975
ERR_NORECIPIENT = '411'
 
2976
ERR_NOTEXTTOSEND = '412'
 
2977
ERR_NOTOPLEVEL = '413'
 
2978
ERR_WILDTOPLEVEL = '414'
 
2979
ERR_BADMASK = '415'
 
2980
ERR_UNKNOWNCOMMAND = '421'
 
2981
ERR_NOMOTD = '422'
 
2982
ERR_NOADMININFO = '423'
 
2983
ERR_FILEERROR = '424'
 
2984
ERR_NONICKNAMEGIVEN = '431'
 
2985
ERR_ERRONEUSNICKNAME = '432'
 
2986
ERR_NICKNAMEINUSE = '433'
 
2987
ERR_NICKCOLLISION = '436'
 
2988
ERR_UNAVAILRESOURCE = '437'
 
2989
ERR_USERNOTINCHANNEL = '441'
 
2990
ERR_NOTONCHANNEL = '442'
 
2991
ERR_USERONCHANNEL = '443'
 
2992
ERR_NOLOGIN = '444'
 
2993
ERR_SUMMONDISABLED = '445'
 
2994
ERR_USERSDISABLED = '446'
 
2995
ERR_NOTREGISTERED = '451'
 
2996
ERR_NEEDMOREPARAMS = '461'
 
2997
ERR_ALREADYREGISTRED = '462'
 
2998
ERR_NOPERMFORHOST = '463'
 
2999
ERR_PASSWDMISMATCH = '464'
 
3000
ERR_YOUREBANNEDCREEP = '465'
 
3001
ERR_YOUWILLBEBANNED = '466'
 
3002
ERR_KEYSET = '467'
 
3003
ERR_CHANNELISFULL = '471'
 
3004
ERR_UNKNOWNMODE = '472'
 
3005
ERR_INVITEONLYCHAN = '473'
 
3006
ERR_BANNEDFROMCHAN = '474'
 
3007
ERR_BADCHANNELKEY = '475'
 
3008
ERR_BADCHANMASK = '476'
 
3009
ERR_NOCHANMODES = '477'
 
3010
ERR_BANLISTFULL = '478'
 
3011
ERR_NOPRIVILEGES = '481'
 
3012
ERR_CHANOPRIVSNEEDED = '482'
 
3013
ERR_CANTKILLSERVER = '483'
 
3014
ERR_RESTRICTED = '484'
 
3015
ERR_UNIQOPPRIVSNEEDED = '485'
 
3016
ERR_NOOPERHOST = '491'
 
3017
ERR_NOSERVICEHOST = '492'
 
3018
ERR_UMODEUNKNOWNFLAG = '501'
 
3019
ERR_USERSDONTMATCH = '502'
 
3020
 
 
3021
# And hey, as long as the strings are already intern'd...
 
3022
symbolic_to_numeric = {
 
3023
    "RPL_WELCOME": '001',
 
3024
    "RPL_YOURHOST": '002',
 
3025
    "RPL_CREATED": '003',
 
3026
    "RPL_MYINFO": '004',
 
3027
    "RPL_ISUPPORT": '005',
 
3028
    "RPL_BOUNCE": '010',
 
3029
    "RPL_USERHOST": '302',
 
3030
    "RPL_ISON": '303',
 
3031
    "RPL_AWAY": '301',
 
3032
    "RPL_UNAWAY": '305',
 
3033
    "RPL_NOWAWAY": '306',
 
3034
    "RPL_WHOISUSER": '311',
 
3035
    "RPL_WHOISSERVER": '312',
 
3036
    "RPL_WHOISOPERATOR": '313',
 
3037
    "RPL_WHOISIDLE": '317',
 
3038
    "RPL_ENDOFWHOIS": '318',
 
3039
    "RPL_WHOISCHANNELS": '319',
 
3040
    "RPL_WHOWASUSER": '314',
 
3041
    "RPL_ENDOFWHOWAS": '369',
 
3042
    "RPL_LISTSTART": '321',
 
3043
    "RPL_LIST": '322',
 
3044
    "RPL_LISTEND": '323',
 
3045
    "RPL_UNIQOPIS": '325',
 
3046
    "RPL_CHANNELMODEIS": '324',
 
3047
    "RPL_NOTOPIC": '331',
 
3048
    "RPL_TOPIC": '332',
 
3049
    "RPL_INVITING": '341',
 
3050
    "RPL_SUMMONING": '342',
 
3051
    "RPL_INVITELIST": '346',
 
3052
    "RPL_ENDOFINVITELIST": '347',
 
3053
    "RPL_EXCEPTLIST": '348',
 
3054
    "RPL_ENDOFEXCEPTLIST": '349',
 
3055
    "RPL_VERSION": '351',
 
3056
    "RPL_WHOREPLY": '352',
 
3057
    "RPL_ENDOFWHO": '315',
 
3058
    "RPL_NAMREPLY": '353',
 
3059
    "RPL_ENDOFNAMES": '366',
 
3060
    "RPL_LINKS": '364',
 
3061
    "RPL_ENDOFLINKS": '365',
 
3062
    "RPL_BANLIST": '367',
 
3063
    "RPL_ENDOFBANLIST": '368',
 
3064
    "RPL_INFO": '371',
 
3065
    "RPL_ENDOFINFO": '374',
 
3066
    "RPL_MOTDSTART": '375',
 
3067
    "RPL_MOTD": '372',
 
3068
    "RPL_ENDOFMOTD": '376',
 
3069
    "RPL_YOUREOPER": '381',
 
3070
    "RPL_REHASHING": '382',
 
3071
    "RPL_YOURESERVICE": '383',
 
3072
    "RPL_TIME": '391',
 
3073
    "RPL_USERSSTART": '392',
 
3074
    "RPL_USERS": '393',
 
3075
    "RPL_ENDOFUSERS": '394',
 
3076
    "RPL_NOUSERS": '395',
 
3077
    "RPL_TRACELINK": '200',
 
3078
    "RPL_TRACECONNECTING": '201',
 
3079
    "RPL_TRACEHANDSHAKE": '202',
 
3080
    "RPL_TRACEUNKNOWN": '203',
 
3081
    "RPL_TRACEOPERATOR": '204',
 
3082
    "RPL_TRACEUSER": '205',
 
3083
    "RPL_TRACESERVER": '206',
 
3084
    "RPL_TRACESERVICE": '207',
 
3085
    "RPL_TRACENEWTYPE": '208',
 
3086
    "RPL_TRACECLASS": '209',
 
3087
    "RPL_TRACERECONNECT": '210',
 
3088
    "RPL_TRACELOG": '261',
 
3089
    "RPL_TRACEEND": '262',
 
3090
    "RPL_STATSLINKINFO": '211',
 
3091
    "RPL_STATSCOMMANDS": '212',
 
3092
    "RPL_ENDOFSTATS": '219',
 
3093
    "RPL_STATSUPTIME": '242',
 
3094
    "RPL_STATSOLINE": '243',
 
3095
    "RPL_UMODEIS": '221',
 
3096
    "RPL_SERVLIST": '234',
 
3097
    "RPL_SERVLISTEND": '235',
 
3098
    "RPL_LUSERCLIENT": '251',
 
3099
    "RPL_LUSEROP": '252',
 
3100
    "RPL_LUSERUNKNOWN": '253',
 
3101
    "RPL_LUSERCHANNELS": '254',
 
3102
    "RPL_LUSERME": '255',
 
3103
    "RPL_ADMINME": '256',
 
3104
    "RPL_ADMINLOC": '257',
 
3105
    "RPL_ADMINLOC": '258',
 
3106
    "RPL_ADMINEMAIL": '259',
 
3107
    "RPL_TRYAGAIN": '263',
 
3108
    "ERR_NOSUCHNICK": '401',
 
3109
    "ERR_NOSUCHSERVER": '402',
 
3110
    "ERR_NOSUCHCHANNEL": '403',
 
3111
    "ERR_CANNOTSENDTOCHAN": '404',
 
3112
    "ERR_TOOMANYCHANNELS": '405',
 
3113
    "ERR_WASNOSUCHNICK": '406',
 
3114
    "ERR_TOOMANYTARGETS": '407',
 
3115
    "ERR_NOSUCHSERVICE": '408',
 
3116
    "ERR_NOORIGIN": '409',
 
3117
    "ERR_NORECIPIENT": '411',
 
3118
    "ERR_NOTEXTTOSEND": '412',
 
3119
    "ERR_NOTOPLEVEL": '413',
 
3120
    "ERR_WILDTOPLEVEL": '414',
 
3121
    "ERR_BADMASK": '415',
 
3122
    "ERR_UNKNOWNCOMMAND": '421',
 
3123
    "ERR_NOMOTD": '422',
 
3124
    "ERR_NOADMININFO": '423',
 
3125
    "ERR_FILEERROR": '424',
 
3126
    "ERR_NONICKNAMEGIVEN": '431',
 
3127
    "ERR_ERRONEUSNICKNAME": '432',
 
3128
    "ERR_NICKNAMEINUSE": '433',
 
3129
    "ERR_NICKCOLLISION": '436',
 
3130
    "ERR_UNAVAILRESOURCE": '437',
 
3131
    "ERR_USERNOTINCHANNEL": '441',
 
3132
    "ERR_NOTONCHANNEL": '442',
 
3133
    "ERR_USERONCHANNEL": '443',
 
3134
    "ERR_NOLOGIN": '444',
 
3135
    "ERR_SUMMONDISABLED": '445',
 
3136
    "ERR_USERSDISABLED": '446',
 
3137
    "ERR_NOTREGISTERED": '451',
 
3138
    "ERR_NEEDMOREPARAMS": '461',
 
3139
    "ERR_ALREADYREGISTRED": '462',
 
3140
    "ERR_NOPERMFORHOST": '463',
 
3141
    "ERR_PASSWDMISMATCH": '464',
 
3142
    "ERR_YOUREBANNEDCREEP": '465',
 
3143
    "ERR_YOUWILLBEBANNED": '466',
 
3144
    "ERR_KEYSET": '467',
 
3145
    "ERR_CHANNELISFULL": '471',
 
3146
    "ERR_UNKNOWNMODE": '472',
 
3147
    "ERR_INVITEONLYCHAN": '473',
 
3148
    "ERR_BANNEDFROMCHAN": '474',
 
3149
    "ERR_BADCHANNELKEY": '475',
 
3150
    "ERR_BADCHANMASK": '476',
 
3151
    "ERR_NOCHANMODES": '477',
 
3152
    "ERR_BANLISTFULL": '478',
 
3153
    "ERR_NOPRIVILEGES": '481',
 
3154
    "ERR_CHANOPRIVSNEEDED": '482',
 
3155
    "ERR_CANTKILLSERVER": '483',
 
3156
    "ERR_RESTRICTED": '484',
 
3157
    "ERR_UNIQOPPRIVSNEEDED": '485',
 
3158
    "ERR_NOOPERHOST": '491',
 
3159
    "ERR_NOSERVICEHOST": '492',
 
3160
    "ERR_UMODEUNKNOWNFLAG": '501',
 
3161
    "ERR_USERSDONTMATCH": '502',
 
3162
}
 
3163
 
 
3164
numeric_to_symbolic = {}
 
3165
for k, v in symbolic_to_numeric.items():
 
3166
    numeric_to_symbolic[v] = k