~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/words/protocols/irc.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

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-2005 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
 
 
6
"""Internet Relay Chat Protocol for client and server.
 
7
 
 
8
Stability: semi-stable.
 
9
 
 
10
Future Plans
 
11
============
 
12
 
 
13
The way the IRCClient class works here encourages people to implement
 
14
IRC clients by subclassing the ephemeral protocol class, and it tends
 
15
to end up with way more state than it should for an object which will
 
16
be destroyed as soon as the TCP transport drops.  Someone oughta do
 
17
something about that, ya know?
 
18
 
 
19
The DCC support needs to have more hooks for the client for it to be
 
20
able to ask the user things like \"Do you want to accept this session?\"
 
21
and \"Transfer #2 is 67% done.\" and otherwise manage the DCC sessions.
 
22
 
 
23
Test coverage needs to be better.
 
24
 
 
25
@author: U{Kevin Turner<mailto:acapnotic@twistedmatrix.com>}
 
26
 
 
27
@see: RFC 1459: Internet Relay Chat Protocol
 
28
@see: RFC 2812: Internet Relay Chat: Client Protocol
 
29
@see: U{The Client-To-Client-Protocol
 
30
<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
 
31
"""
 
32
 
 
33
__version__ = '$Revision: 1.94 $'[11:-2]
 
34
 
 
35
from twisted.internet import reactor, protocol
 
36
from twisted.persisted import styles
 
37
from twisted.protocols import basic
 
38
from twisted.python import log, reflect, text
 
39
 
 
40
# System Imports
 
41
 
 
42
import errno
 
43
import os
 
44
import random
 
45
import re
 
46
import stat
 
47
import string
 
48
import struct
 
49
import sys
 
50
import time
 
51
import types
 
52
import traceback
 
53
import socket
 
54
 
 
55
from os import path
 
56
 
 
57
NUL = chr(0)
 
58
CR = chr(015)
 
59
NL = chr(012)
 
60
LF = NL
 
61
SPC = chr(040)
 
62
 
 
63
CHANNEL_PREFIXES = '&#!+'
 
64
 
 
65
class IRCBadMessage(Exception):
 
66
    pass
 
67
 
 
68
class IRCPasswordMismatch(Exception):
 
69
    pass
 
70
 
 
71
def parsemsg(s):
 
72
    """Breaks a message from an IRC server into its prefix, command, and arguments.
 
73
    """
 
74
    prefix = ''
 
75
    trailing = []
 
76
    if not s:
 
77
        raise IRCBadMessage("Empty line.")
 
78
    if s[0] == ':':
 
79
        prefix, s = s[1:].split(' ', 1)
 
80
    if s.find(' :') != -1:
 
81
        s, trailing = s.split(' :', 1)
 
82
        args = s.split()
 
83
        args.append(trailing)
 
84
    else:
 
85
        args = s.split()
 
86
    command = args.pop(0)
 
87
    return prefix, command, args
 
88
 
 
89
 
 
90
def split(str, length = 80):
 
91
    """I break a message into multiple lines.
 
92
 
 
93
    I prefer to break at whitespace near str[length].  I also break at \\n.
 
94
 
 
95
    @returns: list of strings
 
96
    """
 
97
    if length <= 0:
 
98
        raise ValueError("Length must be a number greater than zero")
 
99
    r = []
 
100
    while len(str) > length:
 
101
        w, n = str[:length].rfind(' '), str[:length].find('\n')
 
102
        if w == -1 and n == -1:
 
103
            line, str = str[:length], str[length:]
 
104
        else:
 
105
            i = n == -1 and w or n
 
106
            line, str = str[:i], str[i+1:]
 
107
        r.append(line)
 
108
    if len(str):
 
109
        r.extend(str.split('\n'))
 
110
    return r
 
111
 
 
112
class IRC(protocol.Protocol):
 
113
    """Internet Relay Chat server protocol.
 
114
    """
 
115
 
 
116
    buffer = ""
 
117
    hostname = None
 
118
 
 
119
    encoding = None
 
120
 
 
121
    def connectionMade(self):
 
122
        self.channels = []
 
123
        if self.hostname is None:
 
124
            self.hostname = socket.getfqdn()
 
125
 
 
126
 
 
127
    def sendLine(self, line):
 
128
        if self.encoding is not None:
 
129
            if isinstance(line, unicode):
 
130
                line = line.encode(self.encoding)
 
131
        self.transport.write("%s%s%s" % (line, CR, LF))
 
132
 
 
133
 
 
134
    def sendMessage(self, command, *parameter_list, **prefix):
 
135
        """Send a line formatted as an IRC message.
 
136
 
 
137
        First argument is the command, all subsequent arguments
 
138
        are parameters to that command.  If a prefix is desired,
 
139
        it may be specified with the keyword argument 'prefix'.
 
140
        """
 
141
 
 
142
        if not command:
 
143
            raise ValueError, "IRC message requires a command."
 
144
 
 
145
        if ' ' in command or command[0] == ':':
 
146
            # Not the ONLY way to screw up, but provides a little
 
147
            # sanity checking to catch likely dumb mistakes.
 
148
            raise ValueError, "Somebody screwed up, 'cuz this doesn't" \
 
149
                  " look like a command to me: %s" % command
 
150
 
 
151
        line = string.join([command] + list(parameter_list))
 
152
        if prefix.has_key('prefix'):
 
153
            line = ":%s %s" % (prefix['prefix'], line)
 
154
        self.sendLine(line)
 
155
 
 
156
        if len(parameter_list) > 15:
 
157
            log.msg("Message has %d parameters (RFC allows 15):\n%s" %
 
158
                    (len(parameter_list), line))
 
159
 
 
160
 
 
161
    def dataReceived(self, data):
 
162
        """This hack is to support mIRC, which sends LF only,
 
163
        even though the RFC says CRLF.  (Also, the flexibility
 
164
        of LineReceiver to turn "line mode" on and off was not
 
165
        required.)
 
166
        """
 
167
        lines = (self.buffer + data).split(LF)
 
168
        # Put the (possibly empty) element after the last LF back in the
 
169
        # buffer
 
170
        self.buffer = lines.pop()
 
171
 
 
172
        for line in lines:
 
173
            if len(line) <= 2:
 
174
                # This is a blank line, at best.
 
175
                continue
 
176
            if line[-1] == CR:
 
177
                line = line[:-1]
 
178
            prefix, command, params = parsemsg(line)
 
179
            # mIRC is a big pile of doo-doo
 
180
            command = command.upper()
 
181
            # DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
 
182
 
 
183
            self.handleCommand(command, prefix, params)
 
184
 
 
185
 
 
186
    def handleCommand(self, command, prefix, params):
 
187
        """Determine the function to call for the given command and call
 
188
        it with the given arguments.
 
189
        """
 
190
        method = getattr(self, "irc_%s" % command, None)
 
191
        try:
 
192
            if method is not None:
 
193
                method(prefix, params)
 
194
            else:
 
195
                self.irc_unknown(prefix, command, params)
 
196
        except:
 
197
            log.deferr()
 
198
 
 
199
 
 
200
    def irc_unknown(self, prefix, command, params):
 
201
        """Implement me!"""
 
202
        raise NotImplementedError(command, prefix, params)
 
203
 
 
204
 
 
205
    # Helper methods
 
206
    def privmsg(self, sender, recip, message):
 
207
        """Send a message to a channel or user
 
208
 
 
209
        @type sender: C{str} or C{unicode}
 
210
        @param sender: Who is sending this message.  Should be of the form
 
211
        username!ident@hostmask (unless you know better!).
 
212
 
 
213
        @type recip: C{str} or C{unicode}
 
214
        @param recip: The recipient of this message.  If a channel, it
 
215
        must start with a channel prefix.
 
216
 
 
217
        @type message: C{str} or C{unicode}
 
218
        @param message: The message being sent.
 
219
        """
 
220
        self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message)))
 
221
 
 
222
 
 
223
    def notice(self, sender, recip, message):
 
224
        """Send a \"notice\" to a channel or user.
 
225
 
 
226
        Notices differ from privmsgs in that the RFC claims they are different.
 
227
        Robots are supposed to send notices and not respond to them.  Clients
 
228
        typically display notices differently from privmsgs.
 
229
 
 
230
        @type sender: C{str} or C{unicode}
 
231
        @param sender: Who is sending this message.  Should be of the form
 
232
        username!ident@hostmask (unless you know better!).
 
233
 
 
234
        @type recip: C{str} or C{unicode}
 
235
        @param recip: The recipient of this message.  If a channel, it
 
236
        must start with a channel prefix.
 
237
 
 
238
        @type message: C{str} or C{unicode}
 
239
        @param message: The message being sent.
 
240
        """
 
241
        self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message))
 
242
 
 
243
 
 
244
    def action(self, sender, recip, message):
 
245
        """Send an action to a channel or user.
 
246
 
 
247
        @type sender: C{str} or C{unicode}
 
248
        @param sender: Who is sending this message.  Should be of the form
 
249
        username!ident@hostmask (unless you know better!).
 
250
 
 
251
        @type recip: C{str} or C{unicode}
 
252
        @param recip: The recipient of this message.  If a channel, it
 
253
        must start with a channel prefix.
 
254
 
 
255
        @type message: C{str} or C{unicode}
 
256
        @param message: The action being sent.
 
257
        """
 
258
        self.sendLine(":%s ACTION %s :%s" % (sender, recip, message))
 
259
 
 
260
 
 
261
    def topic(self, user, channel, topic, author=None):
 
262
        """Send the topic to a user.
 
263
 
 
264
        @type user: C{str} or C{unicode}
 
265
        @param user: The user receiving the topic.  Only their nick name, not
 
266
        the full hostmask.
 
267
 
 
268
        @type channel: C{str} or C{unicode}
 
269
        @param channel: The channel for which this is the topic.
 
270
 
 
271
        @type topic: C{str} or C{unicode} or C{None}
 
272
        @param topic: The topic string, unquoted, or None if there is
 
273
        no topic.
 
274
 
 
275
        @type author: C{str} or C{unicode}
 
276
        @param author: If the topic is being changed, the full username and hostmask
 
277
        of the person changing it.
 
278
        """
 
279
        if author is None:
 
280
            if topic is None:
 
281
                self.sendLine(':%s %s %s %s :%s' % (
 
282
                    self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.'))
 
283
            else:
 
284
                self.sendLine(":%s %s %s %s :%s" % (
 
285
                    self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)))
 
286
        else:
 
287
            self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)))
 
288
 
 
289
 
 
290
    def topicAuthor(self, user, channel, author, date):
 
291
        """
 
292
        Send the author of and time at which a topic was set for the given
 
293
        channel.
 
294
 
 
295
        This sends a 333 reply message, which is not part of the IRC RFC.
 
296
 
 
297
        @type user: C{str} or C{unicode}
 
298
        @param user: The user receiving the topic.  Only their nick name, not
 
299
        the full hostmask.
 
300
 
 
301
        @type channel: C{str} or C{unicode}
 
302
        @param channel: The channel for which this information is relevant.
 
303
 
 
304
        @type author: C{str} or C{unicode}
 
305
        @param author: The nickname (without hostmask) of the user who last
 
306
        set the topic.
 
307
 
 
308
        @type date: C{int}
 
309
        @param date: A POSIX timestamp (number of seconds since the epoch)
 
310
        at which the topic was last set.
 
311
        """
 
312
        self.sendLine(':%s %d %s %s %s %d' % (
 
313
            self.hostname, 333, user, channel, author, date))
 
314
 
 
315
 
 
316
    def names(self, user, channel, names):
 
317
        """Send the names of a channel's participants to a user.
 
318
 
 
319
        @type user: C{str} or C{unicode}
 
320
        @param user: The user receiving the name list.  Only their nick
 
321
        name, not the full hostmask.
 
322
 
 
323
        @type channel: C{str} or C{unicode}
 
324
        @param channel: The channel for which this is the namelist.
 
325
 
 
326
        @type names: C{list} of C{str} or C{unicode}
 
327
        @param names: The names to send.
 
328
        """
 
329
        # XXX If unicode is given, these limits are not quite correct
 
330
        prefixLength = len(channel) + len(user) + 10
 
331
        namesLength = 512 - prefixLength
 
332
 
 
333
        L = []
 
334
        count = 0
 
335
        for n in names:
 
336
            if count + len(n) + 1 > namesLength:
 
337
                self.sendLine(":%s %s %s = %s :%s" % (
 
338
                    self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
 
339
                L = [n]
 
340
                count = len(n)
 
341
            else:
 
342
                L.append(n)
 
343
                count += len(n) + 1
 
344
        if L:
 
345
            self.sendLine(":%s %s %s = %s :%s" % (
 
346
                self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
 
347
        self.sendLine(":%s %s %s %s :End of /NAMES list" % (
 
348
            self.hostname, RPL_ENDOFNAMES, user, channel))
 
349
 
 
350
 
 
351
    def who(self, user, channel, memberInfo):
 
352
        """
 
353
        Send a list of users participating in a channel.
 
354
 
 
355
        @type user: C{str} or C{unicode}
 
356
        @param user: The user receiving this member information.  Only their
 
357
        nick name, not the full hostmask.
 
358
 
 
359
        @type channel: C{str} or C{unicode}
 
360
        @param channel: The channel for which this is the member
 
361
        information.
 
362
 
 
363
        @type memberInfo: C{list} of C{tuples}
 
364
        @param memberInfo: For each member of the given channel, a 7-tuple
 
365
        containing their username, their hostmask, the server to which they
 
366
        are connected, their nickname, the letter "H" or "G" (wtf do these
 
367
        mean?), the hopcount from C{user} to this member, and this member's
 
368
        real name.
 
369
        """
 
370
        for info in memberInfo:
 
371
            (username, hostmask, server, nickname, flag, hops, realName) = info
 
372
            assert flag in ("H", "G")
 
373
            self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % (
 
374
                self.hostname, RPL_WHOREPLY, user, channel,
 
375
                username, hostmask, server, nickname, flag, hops, realName))
 
376
 
 
377
        self.sendLine(":%s %s %s %s :End of /WHO list." % (
 
378
            self.hostname, RPL_ENDOFWHO, user, channel))
 
379
 
 
380
 
 
381
    def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels):
 
382
        """
 
383
        Send information about the state of a particular user.
 
384
 
 
385
        @type user: C{str} or C{unicode}
 
386
        @param user: The user receiving this information.  Only their nick
 
387
        name, not the full hostmask.
 
388
 
 
389
        @type nick: C{str} or C{unicode}
 
390
        @param nick: The nickname of the user this information describes.
 
391
 
 
392
        @type username: C{str} or C{unicode}
 
393
        @param username: The user's username (eg, ident response)
 
394
 
 
395
        @type hostname: C{str}
 
396
        @param hostname: The user's hostmask
 
397
 
 
398
        @type realName: C{str} or C{unicode}
 
399
        @param realName: The user's real name
 
400
 
 
401
        @type server: C{str} or C{unicode}
 
402
        @param server: The name of the server to which the user is connected
 
403
 
 
404
        @type serverInfo: C{str} or C{unicode}
 
405
        @param serverInfo: A descriptive string about that server
 
406
 
 
407
        @type oper: C{bool}
 
408
        @param oper: Indicates whether the user is an IRC operator
 
409
 
 
410
        @type idle: C{int}
 
411
        @param idle: The number of seconds since the user last sent a message
 
412
 
 
413
        @type signOn: C{int}
 
414
        @param signOn: A POSIX timestamp (number of seconds since the epoch)
 
415
        indicating the time the user signed on
 
416
 
 
417
        @type channels: C{list} of C{str} or C{unicode}
 
418
        @param channels: A list of the channels which the user is participating in
 
419
        """
 
420
        self.sendLine(":%s %s %s %s %s %s * :%s" % (
 
421
            self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName))
 
422
        self.sendLine(":%s %s %s %s %s :%s" % (
 
423
            self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo))
 
424
        if oper:
 
425
            self.sendLine(":%s %s %s %s :is an IRC operator" % (
 
426
                self.hostname, RPL_WHOISOPERATOR, user, nick))
 
427
        self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % (
 
428
            self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn))
 
429
        self.sendLine(":%s %s %s %s :%s" % (
 
430
            self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels)))
 
431
        self.sendLine(":%s %s %s %s :End of WHOIS list." % (
 
432
            self.hostname, RPL_ENDOFWHOIS, user, nick))
 
433
 
 
434
 
 
435
    def join(self, who, where):
 
436
        """Send a join message.
 
437
 
 
438
        @type who: C{str} or C{unicode}
 
439
        @param who: The name of the user joining.  Should be of the form
 
440
        username!ident@hostmask (unless you know better!).
 
441
 
 
442
        @type where: C{str} or C{unicode}
 
443
        @param where: The channel the user is joining.
 
444
        """
 
445
        self.sendLine(":%s JOIN %s" % (who, where))
 
446
 
 
447
 
 
448
    def part(self, who, where, reason=None):
 
449
        """Send a part message.
 
450
 
 
451
        @type who: C{str} or C{unicode}
 
452
        @param who: The name of the user joining.  Should be of the form
 
453
        username!ident@hostmask (unless you know better!).
 
454
 
 
455
        @type where: C{str} or C{unicode}
 
456
        @param where: The channel the user is joining.
 
457
 
 
458
        @type reason: C{str} or C{unicode}
 
459
        @param reason: A string describing the misery which caused
 
460
        this poor soul to depart.
 
461
        """
 
462
        if reason:
 
463
            self.sendLine(":%s PART %s :%s" % (who, where, reason))
 
464
        else:
 
465
            self.sendLine(":%s PART %s" % (who, where))
 
466
 
 
467
 
 
468
    def channelMode(self, user, channel, mode, *args):
 
469
        """
 
470
        Send information about the mode of a channel.
 
471
 
 
472
        @type user: C{str} or C{unicode}
 
473
        @param user: The user receiving the name list.  Only their nick
 
474
        name, not the full hostmask.
 
475
 
 
476
        @type channel: C{str} or C{unicode}
 
477
        @param channel: The channel for which this is the namelist.
 
478
 
 
479
        @type mode: C{str}
 
480
        @param mode: A string describing this channel's modes.
 
481
 
 
482
        @param args: Any additional arguments required by the modes.
 
483
        """
 
484
        self.sendLine(":%s %s %s %s %s %s" % (
 
485
            self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args)))
 
486
 
 
487
 
 
488
class IRCClient(basic.LineReceiver):
 
489
    """Internet Relay Chat client protocol, with sprinkles.
 
490
 
 
491
    In addition to providing an interface for an IRC client protocol,
 
492
    this class also contains reasonable implementations of many common
 
493
    CTCP methods.
 
494
 
 
495
    TODO
 
496
    ====
 
497
     - Limit the length of messages sent (because the IRC server probably
 
498
       does).
 
499
     - Add flood protection/rate limiting for my CTCP replies.
 
500
     - NickServ cooperation.  (a mix-in?)
 
501
     - Heartbeat.  The transport may die in such a way that it does not realize
 
502
       it is dead until it is written to.  Sending something (like \"PING
 
503
       this.irc-host.net\") during idle peroids would alleviate that.  If
 
504
       you're concerned with the stability of the host as well as that of the
 
505
       transport, you might care to watch for the corresponding PONG.
 
506
 
 
507
    @ivar nickname: Nickname the client will use.
 
508
    @ivar password: Password used to log on to the server.  May be C{None}.
 
509
    @ivar realname: Supplied to the server during login as the \"Real name\"
 
510
        or \"ircname\".  May be C{None}.
 
511
    @ivar username: Supplied to the server during login as the \"User name\".
 
512
        May be C{None}
 
513
 
 
514
    @ivar userinfo: Sent in reply to a X{USERINFO} CTCP query.  If C{None}, no
 
515
        USERINFO reply will be sent.
 
516
        \"This is used to transmit a string which is settable by
 
517
        the user (and never should be set by the client).\"
 
518
    @ivar fingerReply: Sent in reply to a X{FINGER} CTCP query.  If C{None}, no
 
519
        FINGER reply will be sent.
 
520
    @type fingerReply: Callable or String
 
521
 
 
522
    @ivar versionName: CTCP VERSION reply, client name.  If C{None}, no VERSION
 
523
        reply will be sent.
 
524
    @ivar versionNum: CTCP VERSION reply, client version,
 
525
    @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
 
526
 
 
527
    @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
 
528
        client may be found.  If C{None}, no SOURCE reply will be sent.
 
529
 
 
530
    @ivar lineRate: Minimum delay between lines sent to the server.  If
 
531
        C{None}, no delay will be imposed.
 
532
    @type lineRate: Number of Seconds.
 
533
    """
 
534
 
 
535
    motd = ""
 
536
    nickname = 'irc'
 
537
    password = None
 
538
    realname = None
 
539
    username = None
 
540
    ### Responses to various CTCP queries.
 
541
 
 
542
    userinfo = None
 
543
    # fingerReply is a callable returning a string, or a str()able object.
 
544
    fingerReply = None
 
545
    versionName = None
 
546
    versionNum = None
 
547
    versionEnv = None
 
548
 
 
549
    sourceURL = "http://twistedmatrix.com/downloads/"
 
550
 
 
551
    dcc_destdir = '.'
 
552
    dcc_sessions = None
 
553
 
 
554
    # If this is false, no attempt will be made to identify
 
555
    # ourself to the server.
 
556
    performLogin = 1
 
557
 
 
558
    lineRate = None
 
559
    _queue = None
 
560
    _queueEmptying = None
 
561
 
 
562
    delimiter = '\n' # '\r\n' will also work (see dataReceived)
 
563
 
 
564
    __pychecker__ = 'unusednames=params,prefix,channel'
 
565
 
 
566
 
 
567
    def _reallySendLine(self, line):
 
568
        return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
 
569
 
 
570
    def sendLine(self, line):
 
571
        if self.lineRate is None:
 
572
            self._reallySendLine(line)
 
573
        else:
 
574
            self._queue.append(line)
 
575
            if not self._queueEmptying:
 
576
                self._sendLine()
 
577
 
 
578
    def _sendLine(self):
 
579
        if self._queue:
 
580
            self._reallySendLine(self._queue.pop(0))
 
581
            self._queueEmptying = reactor.callLater(self.lineRate,
 
582
                                                    self._sendLine)
 
583
        else:
 
584
            self._queueEmptying = None
 
585
 
 
586
 
 
587
    ### Interface level client->user output methods
 
588
    ###
 
589
    ### You'll want to override these.
 
590
 
 
591
    ### Methods relating to the server itself
 
592
 
 
593
    def created(self, when):
 
594
        """Called with creation date information about the server, usually at logon.
 
595
 
 
596
        @type when: C{str}
 
597
        @param when: A string describing when the server was created, probably.
 
598
        """
 
599
 
 
600
    def yourHost(self, info):
 
601
        """Called with daemon information about the server, usually at logon.
 
602
 
 
603
        @type info: C{str}
 
604
        @param when: A string describing what software the server is running, probably.
 
605
        """
 
606
 
 
607
    def myInfo(self, servername, version, umodes, cmodes):
 
608
        """Called with information about the server, usually at logon.
 
609
 
 
610
        @type servername: C{str}
 
611
        @param servername: The hostname of this server.
 
612
 
 
613
        @type version: C{str}
 
614
        @param version: A description of what software this server runs.
 
615
 
 
616
        @type umodes: C{str}
 
617
        @param umodes: All the available user modes.
 
618
 
 
619
        @type cmodes: C{str}
 
620
        @param cmodes: All the available channel modes.
 
621
        """
 
622
 
 
623
    def luserClient(self, info):
 
624
        """Called with information about the number of connections, usually at logon.
 
625
 
 
626
        @type info: C{str}
 
627
        @param info: A description of the number of clients and servers
 
628
        connected to the network, probably.
 
629
        """
 
630
 
 
631
    def bounce(self, info):
 
632
        """Called with information about where the client should reconnect.
 
633
 
 
634
        @type info: C{str}
 
635
        @param info: A plaintext description of the address that should be
 
636
        connected to.
 
637
        """
 
638
 
 
639
    def isupport(self, options):
 
640
        """Called with various information about what the server supports.
 
641
 
 
642
        @type options: C{list} of C{str}
 
643
        @param options: Descriptions of features or limits of the server, possibly
 
644
        in the form "NAME=VALUE".
 
645
        """
 
646
 
 
647
    def luserChannels(self, channels):
 
648
        """Called with the number of channels existant on the server.
 
649
 
 
650
        @type channels: C{int}
 
651
        """
 
652
 
 
653
    def luserOp(self, ops):
 
654
        """Called with the number of ops logged on to the server.
 
655
 
 
656
        @type ops: C{int}
 
657
        """
 
658
 
 
659
    def luserMe(self, info):
 
660
        """Called with information about the server connected to.
 
661
 
 
662
        @type info: C{str}
 
663
        @param info: A plaintext string describing the number of users and servers
 
664
        connected to this server.
 
665
        """
 
666
 
 
667
    ### Methods involving me directly
 
668
 
 
669
    def privmsg(self, user, channel, message):
 
670
        """Called when I have a message from a user to me or a channel.
 
671
        """
 
672
        pass
 
673
 
 
674
    def joined(self, channel):
 
675
        """Called when I finish joining a channel.
 
676
 
 
677
        channel has the starting character (# or &) intact.
 
678
        """
 
679
        pass
 
680
 
 
681
    def left(self, channel):
 
682
        """Called when I have left a channel.
 
683
 
 
684
        channel has the starting character (# or &) intact.
 
685
        """
 
686
        pass
 
687
 
 
688
    def noticed(self, user, channel, message):
 
689
        """Called when I have a notice from a user to me or a channel.
 
690
 
 
691
        By default, this is equivalent to IRCClient.privmsg, but if your
 
692
        client makes any automated replies, you must override this!
 
693
        From the RFC::
 
694
 
 
695
            The difference between NOTICE and PRIVMSG is that
 
696
            automatic replies MUST NEVER be sent in response to a
 
697
            NOTICE message. [...] The object of this rule is to avoid
 
698
            loops between clients automatically sending something in
 
699
            response to something it received.
 
700
        """
 
701
        self.privmsg(user, channel, message)
 
702
 
 
703
    def modeChanged(self, user, channel, set, modes, args):
 
704
        """Called when a channel's modes are changed
 
705
 
 
706
        @type user: C{str}
 
707
        @param user: The user and hostmask which instigated this change.
 
708
 
 
709
        @type channel: C{str}
 
710
        @param channel: The channel for which the modes are changing.
 
711
 
 
712
        @type set: C{bool} or C{int}
 
713
        @param set: true if the mode is being added, false if it is being
 
714
        removed.
 
715
 
 
716
        @type modes: C{str}
 
717
        @param modes: The mode or modes which are being changed.
 
718
 
 
719
        @type args: C{tuple}
 
720
        @param args: Any additional information required for the mode
 
721
        change.
 
722
        """
 
723
 
 
724
    def pong(self, user, secs):
 
725
        """Called with the results of a CTCP PING query.
 
726
        """
 
727
        pass
 
728
 
 
729
    def signedOn(self):
 
730
        """Called after sucessfully signing on to the server.
 
731
        """
 
732
        pass
 
733
 
 
734
    def kickedFrom(self, channel, kicker, message):
 
735
        """Called when I am kicked from a channel.
 
736
        """
 
737
        pass
 
738
 
 
739
    def nickChanged(self, nick):
 
740
        """Called when my nick has been changed.
 
741
        """
 
742
        self.nickname = nick
 
743
 
 
744
 
 
745
    ### Things I observe other people doing in a channel.
 
746
 
 
747
    def userJoined(self, user, channel):
 
748
        """Called when I see another user joining a channel.
 
749
        """
 
750
        pass
 
751
 
 
752
    def userLeft(self, user, channel):
 
753
        """Called when I see another user leaving a channel.
 
754
        """
 
755
        pass
 
756
 
 
757
    def userQuit(self, user, quitMessage):
 
758
        """Called when I see another user disconnect from the network.
 
759
        """
 
760
        pass
 
761
 
 
762
    def userKicked(self, kickee, channel, kicker, message):
 
763
        """Called when I observe someone else being kicked from a channel.
 
764
        """
 
765
        pass
 
766
 
 
767
    def action(self, user, channel, data):
 
768
        """Called when I see a user perform an ACTION on a channel.
 
769
        """
 
770
        pass
 
771
 
 
772
    def topicUpdated(self, user, channel, newTopic):
 
773
        """In channel, user changed the topic to newTopic.
 
774
 
 
775
        Also called when first joining a channel.
 
776
        """
 
777
        pass
 
778
 
 
779
    def userRenamed(self, oldname, newname):
 
780
        """A user changed their name from oldname to newname.
 
781
        """
 
782
        pass
 
783
 
 
784
    ### Information from the server.
 
785
 
 
786
    def receivedMOTD(self, motd):
 
787
        """I received a message-of-the-day banner from the server.
 
788
 
 
789
        motd is a list of strings, where each string was sent as a seperate
 
790
        message from the server. To display, you might want to use::
 
791
 
 
792
            string.join(motd, '\\n')
 
793
 
 
794
        to get a nicely formatted string.
 
795
        """
 
796
        pass
 
797
 
 
798
    ### user input commands, client->server
 
799
    ### Your client will want to invoke these.
 
800
 
 
801
    def join(self, channel, key=None):
 
802
        if channel[0] not in '&#!+': channel = '#' + channel
 
803
        if key:
 
804
            self.sendLine("JOIN %s %s" % (channel, key))
 
805
        else:
 
806
            self.sendLine("JOIN %s" % (channel,))
 
807
 
 
808
    def leave(self, channel, reason=None):
 
809
        if channel[0] not in '&#!+': channel = '#' + channel
 
810
        if reason:
 
811
            self.sendLine("PART %s :%s" % (channel, reason))
 
812
        else:
 
813
            self.sendLine("PART %s" % (channel,))
 
814
 
 
815
    def kick(self, channel, user, reason=None):
 
816
        if channel[0] not in '&#!+': channel = '#' + channel
 
817
        if reason:
 
818
            self.sendLine("KICK %s %s :%s" % (channel, user, reason))
 
819
        else:
 
820
            self.sendLine("KICK %s %s" % (channel, user))
 
821
 
 
822
    part = leave
 
823
 
 
824
    def topic(self, channel, topic=None):
 
825
        """Attempt to set the topic of the given channel, or ask what it is.
 
826
 
 
827
        If topic is None, then I sent a topic query instead of trying to set
 
828
        the topic. The server should respond with a TOPIC message containing
 
829
        the current topic of the given channel.
 
830
        """
 
831
        # << TOPIC #xtestx :fff
 
832
        if channel[0] not in '&#!+': channel = '#' + channel
 
833
        if topic != None:
 
834
            self.sendLine("TOPIC %s :%s" % (channel, topic))
 
835
        else:
 
836
            self.sendLine("TOPIC %s" % (channel,))
 
837
 
 
838
    def mode(self, chan, set, modes, limit = None, user = None, mask = None):
 
839
        """Change the modes on a user or channel."""
 
840
        if set:
 
841
            line = 'MODE %s +%s' % (chan, modes)
 
842
        else:
 
843
            line = 'MODE %s -%s' % (chan, modes)
 
844
        if limit is not None:
 
845
            line = '%s %d' % (line, limit)
 
846
        elif user is not None:
 
847
            line = '%s %s' % (line, user)
 
848
        elif mask is not None:
 
849
            line = '%s %s' % (line, mask)
 
850
        self.sendLine(line)
 
851
 
 
852
 
 
853
    def say(self, channel, message, length = None):
 
854
        if channel[0] not in '&#!+': channel = '#' + channel
 
855
        self.msg(channel, message, length)
 
856
 
 
857
    def msg(self, user, message, length = None):
 
858
        """Send a message to a user or channel.
 
859
 
 
860
        @type user: C{str}
 
861
        @param user: The username or channel name to which to direct the
 
862
        message.
 
863
 
 
864
        @type message: C{str}
 
865
        @param message: The text to send
 
866
 
 
867
        @type length: C{int}
 
868
        @param length: The maximum number of octets to send at a time.  This
 
869
        has the effect of turning a single call to msg() into multiple
 
870
        commands to the server.  This is useful when long messages may be
 
871
        sent that would otherwise cause the server to kick us off or silently
 
872
        truncate the text we are sending.  If None is passed, the entire
 
873
        message is always send in one command.
 
874
        """
 
875
 
 
876
        fmt = "PRIVMSG %s :%%s" % (user,)
 
877
 
 
878
        if length is None:
 
879
            self.sendLine(fmt % (message,))
 
880
        else:
 
881
            # NOTE: minimumLength really equals len(fmt) - 2 (for '%s') + n
 
882
            # where n is how many bytes sendLine sends to end the line.
 
883
            # n was magic numbered to 2, I think incorrectly
 
884
            minimumLength = len(fmt)
 
885
            if length <= minimumLength:
 
886
                raise ValueError("Maximum length must exceed %d for message "
 
887
                                 "to %s" % (minimumLength, user))
 
888
            lines = split(message, length - minimumLength)
 
889
            map(lambda line, self=self, fmt=fmt: self.sendLine(fmt % line),
 
890
                lines)
 
891
 
 
892
    def notice(self, user, message):
 
893
        self.sendLine("NOTICE %s :%s" % (user, message))
 
894
 
 
895
    def away(self, message=''):
 
896
        self.sendLine("AWAY :%s" % message)
 
897
 
 
898
    def register(self, nickname, hostname='foo', servername='bar'):
 
899
        if self.password is not None:
 
900
            self.sendLine("PASS %s" % self.password)
 
901
        self.setNick(nickname)
 
902
        if self.username is None:
 
903
            self.username = nickname
 
904
        self.sendLine("USER %s foo bar :%s" % (self.username, self.realname))
 
905
 
 
906
    def setNick(self, nickname):
 
907
        self.nickname = nickname
 
908
        self.sendLine("NICK %s" % nickname)
 
909
 
 
910
    def quit(self, message = ''):
 
911
        self.sendLine("QUIT :%s" % message)
 
912
 
 
913
    ### user input commands, client->client
 
914
 
 
915
    def me(self, channel, action):
 
916
        """Strike a pose.
 
917
        """
 
918
        if channel[0] not in '&#!+': channel = '#' + channel
 
919
        self.ctcpMakeQuery(channel, [('ACTION', action)])
 
920
 
 
921
    _pings = None
 
922
    _MAX_PINGRING = 12
 
923
 
 
924
    def ping(self, user, text = None):
 
925
        """Measure round-trip delay to another IRC client.
 
926
        """
 
927
        if self._pings is None:
 
928
            self._pings = {}
 
929
 
 
930
        if text is None:
 
931
            chars = string.letters + string.digits + string.punctuation
 
932
            key = ''.join([random.choice(chars) for i in range(12)])
 
933
        else:
 
934
            key = str(text)
 
935
        self._pings[(user, key)] = time.time()
 
936
        self.ctcpMakeQuery(user, [('PING', key)])
 
937
 
 
938
        if len(self._pings) > self._MAX_PINGRING:
 
939
            # Remove some of the oldest entries.
 
940
            byValue = [(v, k) for (k, v) in self._pings.items()]
 
941
            byValue.sort()
 
942
            excess = self._MAX_PINGRING - len(self._pings)
 
943
            for i in xrange(excess):
 
944
                del self._pings[byValue[i][1]]
 
945
 
 
946
    def dccSend(self, user, file):
 
947
        if type(file) == types.StringType:
 
948
            file = open(file, 'r')
 
949
 
 
950
        size = fileSize(file)
 
951
 
 
952
        name = getattr(file, "name", "file@%s" % (id(file),))
 
953
 
 
954
        factory = DccSendFactory(file)
 
955
        port = reactor.listenTCP(0, factory, 1)
 
956
 
 
957
        raise NotImplementedError,(
 
958
            "XXX!!! Help!  I need to bind a socket, have it listen, and tell me its address.  "
 
959
            "(and stop accepting once we've made a single connection.)")
 
960
 
 
961
        my_address = struct.pack("!I", my_address)
 
962
 
 
963
        args = ['SEND', name, my_address, str(port)]
 
964
 
 
965
        if not (size is None):
 
966
            args.append(size)
 
967
 
 
968
        args = string.join(args, ' ')
 
969
 
 
970
        self.ctcpMakeQuery(user, [('DCC', args)])
 
971
 
 
972
    def dccResume(self, user, fileName, port, resumePos):
 
973
        """Send a DCC RESUME request to another user."""
 
974
        self.ctcpMakeQuery(user, [
 
975
            ('DCC', ['RESUME', fileName, port, resumePos])])
 
976
 
 
977
    def dccAcceptResume(self, user, fileName, port, resumePos):
 
978
        """Send a DCC ACCEPT response to clients who have requested a resume.
 
979
        """
 
980
        self.ctcpMakeQuery(user, [
 
981
            ('DCC', ['ACCEPT', fileName, port, resumePos])])
 
982
 
 
983
    ### server->client messages
 
984
    ### You might want to fiddle with these,
 
985
    ### but it is safe to leave them alone.
 
986
 
 
987
    def irc_ERR_NICKNAMEINUSE(self, prefix, params):
 
988
        self.register(self.nickname+'_')
 
989
 
 
990
    def irc_ERR_PASSWDMISMATCH(self, prefix, params):
 
991
        raise IRCPasswordMismatch("Password Incorrect.")
 
992
 
 
993
    def irc_RPL_WELCOME(self, prefix, params):
 
994
        self.signedOn()
 
995
 
 
996
    def irc_JOIN(self, prefix, params):
 
997
        nick = string.split(prefix,'!')[0]
 
998
        channel = params[-1]
 
999
        if nick == self.nickname:
 
1000
            self.joined(channel)
 
1001
        else:
 
1002
            self.userJoined(nick, channel)
 
1003
 
 
1004
    def irc_PART(self, prefix, params):
 
1005
        nick = string.split(prefix,'!')[0]
 
1006
        channel = params[0]
 
1007
        if nick == self.nickname:
 
1008
            self.left(channel)
 
1009
        else:
 
1010
            self.userLeft(nick, channel)
 
1011
 
 
1012
    def irc_QUIT(self, prefix, params):
 
1013
        nick = string.split(prefix,'!')[0]
 
1014
        self.userQuit(nick, params[0])
 
1015
 
 
1016
    def irc_MODE(self, prefix, params):
 
1017
        channel, rest = params[0], params[1:]
 
1018
        set = rest[0][0] == '+'
 
1019
        modes = rest[0][1:]
 
1020
        args = rest[1:]
 
1021
        self.modeChanged(prefix, channel, set, modes, tuple(args))
 
1022
 
 
1023
    def irc_PING(self, prefix, params):
 
1024
        self.sendLine("PONG %s" % params[-1])
 
1025
 
 
1026
    def irc_PRIVMSG(self, prefix, params):
 
1027
        user = prefix
 
1028
        channel = params[0]
 
1029
        message = params[-1]
 
1030
 
 
1031
        if not message: return # don't raise an exception if some idiot sends us a blank message
 
1032
 
 
1033
        if message[0]==X_DELIM:
 
1034
            m = ctcpExtract(message)
 
1035
            if m['extended']:
 
1036
                self.ctcpQuery(user, channel, m['extended'])
 
1037
 
 
1038
            if not m['normal']:
 
1039
                return
 
1040
 
 
1041
            message = string.join(m['normal'], ' ')
 
1042
 
 
1043
        self.privmsg(user, channel, message)
 
1044
 
 
1045
    def irc_NOTICE(self, prefix, params):
 
1046
        user = prefix
 
1047
        channel = params[0]
 
1048
        message = params[-1]
 
1049
 
 
1050
        if message[0]==X_DELIM:
 
1051
            m = ctcpExtract(message)
 
1052
            if m['extended']:
 
1053
                self.ctcpReply(user, channel, m['extended'])
 
1054
 
 
1055
            if not m['normal']:
 
1056
                return
 
1057
 
 
1058
            message = string.join(m['normal'], ' ')
 
1059
 
 
1060
        self.noticed(user, channel, message)
 
1061
 
 
1062
    def irc_NICK(self, prefix, params):
 
1063
        nick = string.split(prefix,'!', 1)[0]
 
1064
        if nick == self.nickname:
 
1065
            self.nickChanged(params[0])
 
1066
        else:
 
1067
            self.userRenamed(nick, params[0])
 
1068
 
 
1069
    def irc_KICK(self, prefix, params):
 
1070
        """Kicked?  Who?  Not me, I hope.
 
1071
        """
 
1072
        kicker = string.split(prefix,'!')[0]
 
1073
        channel = params[0]
 
1074
        kicked = params[1]
 
1075
        message = params[-1]
 
1076
        if string.lower(kicked) == string.lower(self.nickname):
 
1077
            # Yikes!
 
1078
            self.kickedFrom(channel, kicker, message)
 
1079
        else:
 
1080
            self.userKicked(kicked, channel, kicker, message)
 
1081
 
 
1082
    def irc_TOPIC(self, prefix, params):
 
1083
        """Someone in the channel set the topic.
 
1084
        """
 
1085
        user = string.split(prefix, '!')[0]
 
1086
        channel = params[0]
 
1087
        newtopic = params[1]
 
1088
        self.topicUpdated(user, channel, newtopic)
 
1089
 
 
1090
    def irc_RPL_TOPIC(self, prefix, params):
 
1091
        """I just joined the channel, and the server is telling me the current topic.
 
1092
        """
 
1093
        user = string.split(prefix, '!')[0]
 
1094
        channel = params[1]
 
1095
        newtopic = params[2]
 
1096
        self.topicUpdated(user, channel, newtopic)
 
1097
 
 
1098
    def irc_RPL_NOTOPIC(self, prefix, params):
 
1099
        user = string.split(prefix, '!')[0]
 
1100
        channel = params[1]
 
1101
        newtopic = ""
 
1102
        self.topicUpdated(user, channel, newtopic)
 
1103
 
 
1104
    def irc_RPL_MOTDSTART(self, prefix, params):
 
1105
        if params[-1].startswith("- "):
 
1106
            params[-1] = params[-1][2:]
 
1107
        self.motd = [params[-1]]
 
1108
 
 
1109
    def irc_RPL_MOTD(self, prefix, params):
 
1110
        if params[-1].startswith("- "):
 
1111
            params[-1] = params[-1][2:]
 
1112
        self.motd.append(params[-1])
 
1113
 
 
1114
    def irc_RPL_ENDOFMOTD(self, prefix, params):
 
1115
        self.receivedMOTD(self.motd)
 
1116
 
 
1117
    def irc_RPL_CREATED(self, prefix, params):
 
1118
        self.created(params[1])
 
1119
 
 
1120
    def irc_RPL_YOURHOST(self, prefix, params):
 
1121
        self.yourHost(params[1])
 
1122
 
 
1123
    def irc_RPL_MYINFO(self, prefix, params):
 
1124
        info = params[1].split(None, 3)
 
1125
        while len(info) < 4:
 
1126
            info.append(None)
 
1127
        self.myInfo(*info)
 
1128
 
 
1129
    def irc_RPL_BOUNCE(self, prefix, params):
 
1130
        # 005 is doubly assigned.  Piece of crap dirty trash protocol.
 
1131
        if params[-1] == "are available on this server":
 
1132
            self.isupport(params[1:-1])
 
1133
        else:
 
1134
            self.bounce(params[1])
 
1135
 
 
1136
    def irc_RPL_LUSERCLIENT(self, prefix, params):
 
1137
        self.luserClient(params[1])
 
1138
 
 
1139
    def irc_RPL_LUSEROP(self, prefix, params):
 
1140
        try:
 
1141
            self.luserOp(int(params[1]))
 
1142
        except ValueError:
 
1143
            pass
 
1144
 
 
1145
    def irc_RPL_LUSERCHANNELS(self, prefix, params):
 
1146
        try:
 
1147
            self.luserChannels(int(params[1]))
 
1148
        except ValueError:
 
1149
            pass
 
1150
 
 
1151
    def irc_RPL_LUSERME(self, prefix, params):
 
1152
        self.luserMe(params[1])
 
1153
 
 
1154
    def irc_unknown(self, prefix, command, params):
 
1155
        pass
 
1156
 
 
1157
    ### Receiving a CTCP query from another party
 
1158
    ### It is safe to leave these alone.
 
1159
 
 
1160
    def ctcpQuery(self, user, channel, messages):
 
1161
        """Dispatch method for any CTCP queries received.
 
1162
        """
 
1163
        for m in messages:
 
1164
            method = getattr(self, "ctcpQuery_%s" % m[0], None)
 
1165
            if method:
 
1166
                method(user, channel, m[1])
 
1167
            else:
 
1168
                self.ctcpUnknownQuery(user, channel, m[0], m[1])
 
1169
 
 
1170
    def ctcpQuery_ACTION(self, user, channel, data):
 
1171
        self.action(user, channel, data)
 
1172
 
 
1173
    def ctcpQuery_PING(self, user, channel, data):
 
1174
        nick = string.split(user,"!")[0]
 
1175
        self.ctcpMakeReply(nick, [("PING", data)])
 
1176
 
 
1177
    def ctcpQuery_FINGER(self, user, channel, data):
 
1178
        if data is not None:
 
1179
            self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
 
1180
                               % (user, data))
 
1181
        if not self.fingerReply:
 
1182
            return
 
1183
 
 
1184
        if callable(self.fingerReply):
 
1185
            reply = self.fingerReply()
 
1186
        else:
 
1187
            reply = str(self.fingerReply)
 
1188
 
 
1189
        nick = string.split(user,"!")[0]
 
1190
        self.ctcpMakeReply(nick, [('FINGER', reply)])
 
1191
 
 
1192
    def ctcpQuery_VERSION(self, user, channel, data):
 
1193
        if data is not None:
 
1194
            self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
 
1195
                               % (user, data))
 
1196
 
 
1197
        if self.versionName:
 
1198
            nick = string.split(user,"!")[0]
 
1199
            self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
 
1200
                                       (self.versionName,
 
1201
                                        self.versionNum,
 
1202
                                        self.versionEnv))])
 
1203
 
 
1204
    def ctcpQuery_SOURCE(self, user, channel, data):
 
1205
        if data is not None:
 
1206
            self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
 
1207
                               % (user, data))
 
1208
        if self.sourceURL:
 
1209
            nick = string.split(user,"!")[0]
 
1210
            # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
 
1211
            # replies should be responded to with the location of an anonymous
 
1212
            # FTP server in host:directory:file format.  I'm taking the liberty
 
1213
            # of bringing it into the 21st century by sending a URL instead.
 
1214
            self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
 
1215
                                      ('SOURCE', None)])
 
1216
 
 
1217
    def ctcpQuery_USERINFO(self, user, channel, data):
 
1218
        if data is not None:
 
1219
            self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
 
1220
                               % (user, data))
 
1221
        if self.userinfo:
 
1222
            nick = string.split(user,"!")[0]
 
1223
            self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
 
1224
 
 
1225
    def ctcpQuery_CLIENTINFO(self, user, channel, data):
 
1226
        """A master index of what CTCP tags this client knows.
 
1227
 
 
1228
        If no arguments are provided, respond with a list of known tags.
 
1229
        If an argument is provided, provide human-readable help on
 
1230
        the usage of that tag.
 
1231
        """
 
1232
 
 
1233
        nick = string.split(user,"!")[0]
 
1234
        if not data:
 
1235
            # XXX: prefixedMethodNames gets methods from my *class*,
 
1236
            # but it's entirely possible that this *instance* has more
 
1237
            # methods.
 
1238
            names = reflect.prefixedMethodNames(self.__class__,
 
1239
                                                'ctcpQuery_')
 
1240
 
 
1241
            self.ctcpMakeReply(nick, [('CLIENTINFO',
 
1242
                                       string.join(names, ' '))])
 
1243
        else:
 
1244
            args = string.split(data)
 
1245
            method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
 
1246
            if not method:
 
1247
                self.ctcpMakeReply(nick, [('ERRMSG',
 
1248
                                           "CLIENTINFO %s :"
 
1249
                                           "Unknown query '%s'"
 
1250
                                           % (data, args[0]))])
 
1251
                return
 
1252
            doc = getattr(method, '__doc__', '')
 
1253
            self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
 
1254
 
 
1255
 
 
1256
    def ctcpQuery_ERRMSG(self, user, channel, data):
 
1257
        # Yeah, this seems strange, but that's what the spec says to do
 
1258
        # when faced with an ERRMSG query (not a reply).
 
1259
        nick = string.split(user,"!")[0]
 
1260
        self.ctcpMakeReply(nick, [('ERRMSG',
 
1261
                                   "%s :No error has occoured." % data)])
 
1262
 
 
1263
    def ctcpQuery_TIME(self, user, channel, data):
 
1264
        if data is not None:
 
1265
            self.quirkyMessage("Why did %s send '%s' with a TIME query?"
 
1266
                               % (user, data))
 
1267
        nick = string.split(user,"!")[0]
 
1268
        self.ctcpMakeReply(nick,
 
1269
                           [('TIME', ':%s' %
 
1270
                             time.asctime(time.localtime(time.time())))])
 
1271
 
 
1272
    def ctcpQuery_DCC(self, user, channel, data):
 
1273
        """Initiate a Direct Client Connection
 
1274
        """
 
1275
 
 
1276
        if not data: return
 
1277
        dcctype = data.split(None, 1)[0].upper()
 
1278
        handler = getattr(self, "dcc_" + dcctype, None)
 
1279
        if handler:
 
1280
            if self.dcc_sessions is None:
 
1281
                self.dcc_sessions = []
 
1282
            data = data[len(dcctype)+1:]
 
1283
            handler(user, channel, data)
 
1284
        else:
 
1285
            nick = string.split(user,"!")[0]
 
1286
            self.ctcpMakeReply(nick, [('ERRMSG',
 
1287
                                       "DCC %s :Unknown DCC type '%s'"
 
1288
                                       % (data, dcctype))])
 
1289
            self.quirkyMessage("%s offered unknown DCC type %s"
 
1290
                               % (user, dcctype))
 
1291
 
 
1292
    def dcc_SEND(self, user, channel, data):
 
1293
        # Use splitQuoted for those who send files with spaces in the names.
 
1294
        data = text.splitQuoted(data)
 
1295
        if len(data) < 3:
 
1296
            raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
 
1297
 
 
1298
        (filename, address, port) = data[:3]
 
1299
 
 
1300
        address = dccParseAddress(address)
 
1301
        try:
 
1302
            port = int(port)
 
1303
        except ValueError:
 
1304
            raise IRCBadMessage, "Indecipherable port %r" % (port,)
 
1305
 
 
1306
        size = -1
 
1307
        if len(data) >= 4:
 
1308
            try:
 
1309
                size = int(data[3])
 
1310
            except ValueError:
 
1311
                pass
 
1312
 
 
1313
        # XXX Should we bother passing this data?
 
1314
        self.dccDoSend(user, address, port, filename, size, data)
 
1315
 
 
1316
    def dcc_ACCEPT(self, user, channel, data):
 
1317
        data = text.splitQuoted(data)
 
1318
        if len(data) < 3:
 
1319
            raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
 
1320
        (filename, port, resumePos) = data[:3]
 
1321
        try:
 
1322
            port = int(port)
 
1323
            resumePos = int(resumePos)
 
1324
        except ValueError:
 
1325
            return
 
1326
 
 
1327
        self.dccDoAcceptResume(user, filename, port, resumePos)
 
1328
 
 
1329
    def dcc_RESUME(self, user, channel, data):
 
1330
        data = text.splitQuoted(data)
 
1331
        if len(data) < 3:
 
1332
            raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
 
1333
        (filename, port, resumePos) = data[:3]
 
1334
        try:
 
1335
            port = int(port)
 
1336
            resumePos = int(resumePos)
 
1337
        except ValueError:
 
1338
            return
 
1339
        self.dccDoResume(user, filename, port, resumePos)
 
1340
 
 
1341
    def dcc_CHAT(self, user, channel, data):
 
1342
        data = text.splitQuoted(data)
 
1343
        if len(data) < 3:
 
1344
            raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
 
1345
 
 
1346
        (filename, address, port) = data[:3]
 
1347
 
 
1348
        address = dccParseAddress(address)
 
1349
        try:
 
1350
            port = int(port)
 
1351
        except ValueError:
 
1352
            raise IRCBadMessage, "Indecipherable port %r" % (port,)
 
1353
 
 
1354
        self.dccDoChat(user, channel, address, port, data)
 
1355
 
 
1356
    ### The dccDo methods are the slightly higher-level siblings of
 
1357
    ### common dcc_ methods; the arguments have been parsed for them.
 
1358
 
 
1359
    def dccDoSend(self, user, address, port, fileName, size, data):
 
1360
        """Called when I receive a DCC SEND offer from a client.
 
1361
 
 
1362
        By default, I do nothing here."""
 
1363
        ## filename = path.basename(arg)
 
1364
        ## protocol = DccFileReceive(filename, size,
 
1365
        ##                           (user,channel,data),self.dcc_destdir)
 
1366
        ## reactor.clientTCP(address, port, protocol)
 
1367
        ## self.dcc_sessions.append(protocol)
 
1368
        pass
 
1369
 
 
1370
    def dccDoResume(self, user, file, port, resumePos):
 
1371
        """Called when a client is trying to resume an offered file
 
1372
        via DCC send.  It should be either replied to with a DCC
 
1373
        ACCEPT or ignored (default)."""
 
1374
        pass
 
1375
 
 
1376
    def dccDoAcceptResume(self, user, file, port, resumePos):
 
1377
        """Called when a client has verified and accepted a DCC resume
 
1378
        request made by us.  By default it will do nothing."""
 
1379
        pass
 
1380
 
 
1381
    def dccDoChat(self, user, channel, address, port, data):
 
1382
        pass
 
1383
        #factory = DccChatFactory(self, queryData=(user, channel, data))
 
1384
        #reactor.connectTCP(address, port, factory)
 
1385
        #self.dcc_sessions.append(factory)
 
1386
 
 
1387
    #def ctcpQuery_SED(self, user, data):
 
1388
    #    """Simple Encryption Doodoo
 
1389
    #
 
1390
    #    Feel free to implement this, but no specification is available.
 
1391
    #    """
 
1392
    #    raise NotImplementedError
 
1393
 
 
1394
    def ctcpUnknownQuery(self, user, channel, tag, data):
 
1395
        nick = string.split(user,"!")[0]
 
1396
        self.ctcpMakeReply(nick, [('ERRMSG',
 
1397
                                   "%s %s: Unknown query '%s'"
 
1398
                                   % (tag, data, tag))])
 
1399
 
 
1400
        log.msg("Unknown CTCP query from %s: %s %s\n"
 
1401
                 % (user, tag, data))
 
1402
 
 
1403
    def ctcpMakeReply(self, user, messages):
 
1404
        """Send one or more X{extended messages} as a CTCP reply.
 
1405
 
 
1406
        @type messages: a list of extended messages.  An extended
 
1407
        message is a (tag, data) tuple, where 'data' may be C{None}.
 
1408
        """
 
1409
        self.notice(user, ctcpStringify(messages))
 
1410
 
 
1411
    ### client CTCP query commands
 
1412
 
 
1413
    def ctcpMakeQuery(self, user, messages):
 
1414
        """Send one or more X{extended messages} as a CTCP query.
 
1415
 
 
1416
        @type messages: a list of extended messages.  An extended
 
1417
        message is a (tag, data) tuple, where 'data' may be C{None}.
 
1418
        """
 
1419
        self.msg(user, ctcpStringify(messages))
 
1420
 
 
1421
    ### Receiving a response to a CTCP query (presumably to one we made)
 
1422
    ### You may want to add methods here, or override UnknownReply.
 
1423
 
 
1424
    def ctcpReply(self, user, channel, messages):
 
1425
        """Dispatch method for any CTCP replies received.
 
1426
        """
 
1427
        for m in messages:
 
1428
            method = getattr(self, "ctcpReply_%s" % m[0], None)
 
1429
            if method:
 
1430
                method(user, channel, m[1])
 
1431
            else:
 
1432
                self.ctcpUnknownReply(user, channel, m[0], m[1])
 
1433
 
 
1434
    def ctcpReply_PING(self, user, channel, data):
 
1435
        nick = user.split('!', 1)[0]
 
1436
        if (not self._pings) or (not self._pings.has_key((nick, data))):
 
1437
            raise IRCBadMessage,\
 
1438
                  "Bogus PING response from %s: %s" % (user, data)
 
1439
 
 
1440
        t0 = self._pings[(nick, data)]
 
1441
        self.pong(user, time.time() - t0)
 
1442
 
 
1443
    def ctcpUnknownReply(self, user, channel, tag, data):
 
1444
        """Called when a fitting ctcpReply_ method is not found.
 
1445
 
 
1446
        XXX: If the client makes arbitrary CTCP queries,
 
1447
        this method should probably show the responses to
 
1448
        them instead of treating them as anomolies.
 
1449
        """
 
1450
        log.msg("Unknown CTCP reply from %s: %s %s\n"
 
1451
                 % (user, tag, data))
 
1452
 
 
1453
    ### Error handlers
 
1454
    ### You may override these with something more appropriate to your UI.
 
1455
 
 
1456
    def badMessage(self, line, excType, excValue, tb):
 
1457
        """When I get a message that's so broken I can't use it.
 
1458
        """
 
1459
        log.msg(line)
 
1460
        log.msg(string.join(traceback.format_exception(excType,
 
1461
                                                        excValue,
 
1462
                                                        tb),''))
 
1463
 
 
1464
    def quirkyMessage(self, s):
 
1465
        """This is called when I receive a message which is peculiar,
 
1466
        but not wholly indecipherable.
 
1467
        """
 
1468
        log.msg(s + '\n')
 
1469
 
 
1470
    ### Protocool methods
 
1471
 
 
1472
    def connectionMade(self):
 
1473
        self._queue = []
 
1474
        if self.performLogin:
 
1475
            self.register(self.nickname)
 
1476
 
 
1477
    def dataReceived(self, data):
 
1478
        basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
 
1479
 
 
1480
    def lineReceived(self, line):
 
1481
        line = lowDequote(line)
 
1482
        try:
 
1483
            prefix, command, params = parsemsg(line)
 
1484
            if numeric_to_symbolic.has_key(command):
 
1485
                command = numeric_to_symbolic[command]
 
1486
            self.handleCommand(command, prefix, params)
 
1487
        except IRCBadMessage:
 
1488
            self.badMessage(line, *sys.exc_info())
 
1489
 
 
1490
 
 
1491
    def handleCommand(self, command, prefix, params):
 
1492
        """Determine the function to call for the given command and call
 
1493
        it with the given arguments.
 
1494
        """
 
1495
        method = getattr(self, "irc_%s" % command, None)
 
1496
        try:
 
1497
            if method is not None:
 
1498
                method(prefix, params)
 
1499
            else:
 
1500
                self.irc_unknown(prefix, command, params)
 
1501
        except:
 
1502
            log.deferr()
 
1503
 
 
1504
 
 
1505
    def __getstate__(self):
 
1506
        dct = self.__dict__.copy()
 
1507
        dct['dcc_sessions'] = None
 
1508
        dct['_pings'] = None
 
1509
        return dct
 
1510
 
 
1511
 
 
1512
def dccParseAddress(address):
 
1513
    if '.' in address:
 
1514
        pass
 
1515
    else:
 
1516
        try:
 
1517
            address = long(address)
 
1518
        except ValueError:
 
1519
            raise IRCBadMessage,\
 
1520
                  "Indecipherable address %r" % (address,)
 
1521
        else:
 
1522
            address = (
 
1523
                (address >> 24) & 0xFF,
 
1524
                (address >> 16) & 0xFF,
 
1525
                (address >> 8) & 0xFF,
 
1526
                address & 0xFF,
 
1527
                )
 
1528
            address = '.'.join(map(str,address))
 
1529
    return address
 
1530
 
 
1531
 
 
1532
class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
 
1533
    """Bare protocol to receive a Direct Client Connection SEND stream.
 
1534
 
 
1535
    This does enough to keep the other guy talking, but you'll want to
 
1536
    extend my dataReceived method to *do* something with the data I get.
 
1537
    """
 
1538
 
 
1539
    bytesReceived = 0
 
1540
 
 
1541
    def __init__(self, resumeOffset=0):
 
1542
        self.bytesReceived = resumeOffset
 
1543
        self.resume = (resumeOffset != 0)
 
1544
 
 
1545
    def dataReceived(self, data):
 
1546
        """Called when data is received.
 
1547
 
 
1548
        Warning: This just acknowledges to the remote host that the
 
1549
        data has been received; it doesn't *do* anything with the
 
1550
        data, so you'll want to override this.
 
1551
        """
 
1552
        self.bytesReceived = self.bytesReceived + len(data)
 
1553
        self.transport.write(struct.pack('!i', self.bytesReceived))
 
1554
 
 
1555
 
 
1556
class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
 
1557
    """Protocol for an outgoing Direct Client Connection SEND.
 
1558
    """
 
1559
 
 
1560
    blocksize = 1024
 
1561
    file = None
 
1562
    bytesSent = 0
 
1563
    completed = 0
 
1564
    connected = 0
 
1565
 
 
1566
    def __init__(self, file):
 
1567
        if type(file) is types.StringType:
 
1568
            self.file = open(file, 'r')
 
1569
 
 
1570
    def connectionMade(self):
 
1571
        self.connected = 1
 
1572
        self.sendBlock()
 
1573
 
 
1574
    def dataReceived(self, data):
 
1575
        # XXX: Do we need to check to see if len(data) != fmtsize?
 
1576
 
 
1577
        bytesShesGot = struct.unpack("!I", data)
 
1578
        if bytesShesGot < self.bytesSent:
 
1579
            # Wait for her.
 
1580
            # XXX? Add some checks to see if we've stalled out?
 
1581
            return
 
1582
        elif bytesShesGot > self.bytesSent:
 
1583
            # self.transport.log("DCC SEND %s: She says she has %d bytes "
 
1584
            #                    "but I've only sent %d.  I'm stopping "
 
1585
            #                    "this screwy transfer."
 
1586
            #                    % (self.file,
 
1587
            #                       bytesShesGot, self.bytesSent))
 
1588
            self.transport.loseConnection()
 
1589
            return
 
1590
 
 
1591
        self.sendBlock()
 
1592
 
 
1593
    def sendBlock(self):
 
1594
        block = self.file.read(self.blocksize)
 
1595
        if block:
 
1596
            self.transport.write(block)
 
1597
            self.bytesSent = self.bytesSent + len(block)
 
1598
        else:
 
1599
            # Nothing more to send, transfer complete.
 
1600
            self.transport.loseConnection()
 
1601
            self.completed = 1
 
1602
 
 
1603
    def connectionLost(self, reason):
 
1604
        self.connected = 0
 
1605
        if hasattr(self.file, "close"):
 
1606
            self.file.close()
 
1607
 
 
1608
 
 
1609
class DccSendFactory(protocol.Factory):
 
1610
    protocol = DccSendProtocol
 
1611
    def __init__(self, file):
 
1612
        self.file = file
 
1613
 
 
1614
    def buildProtocol(self, connection):
 
1615
        p = self.protocol(self.file)
 
1616
        p.factory = self
 
1617
        return p
 
1618
 
 
1619
 
 
1620
def fileSize(file):
 
1621
    """I'll try my damndest to determine the size of this file object.
 
1622
    """
 
1623
    size = None
 
1624
    if hasattr(file, "fileno"):
 
1625
        fileno = file.fileno()
 
1626
        try:
 
1627
            stat_ = os.fstat(fileno)
 
1628
            size = stat_[stat.ST_SIZE]
 
1629
        except:
 
1630
            pass
 
1631
        else:
 
1632
            return size
 
1633
 
 
1634
    if hasattr(file, "name") and path.exists(file.name):
 
1635
        try:
 
1636
            size = path.getsize(file.name)
 
1637
        except:
 
1638
            pass
 
1639
        else:
 
1640
            return size
 
1641
 
 
1642
    if hasattr(file, "seek") and hasattr(file, "tell"):
 
1643
        try:
 
1644
            try:
 
1645
                file.seek(0, 2)
 
1646
                size = file.tell()
 
1647
            finally:
 
1648
                file.seek(0, 0)
 
1649
        except:
 
1650
            pass
 
1651
        else:
 
1652
            return size
 
1653
 
 
1654
    return size
 
1655
 
 
1656
class DccChat(basic.LineReceiver, styles.Ephemeral):
 
1657
    """Direct Client Connection protocol type CHAT.
 
1658
 
 
1659
    DCC CHAT is really just your run o' the mill basic.LineReceiver
 
1660
    protocol.  This class only varies from that slightly, accepting
 
1661
    either LF or CR LF for a line delimeter for incoming messages
 
1662
    while always using CR LF for outgoing.
 
1663
 
 
1664
    The lineReceived method implemented here uses the DCC connection's
 
1665
    'client' attribute (provided upon construction) to deliver incoming
 
1666
    lines from the DCC chat via IRCClient's normal privmsg interface.
 
1667
    That's something of a spoof, which you may well want to override.
 
1668
    """
 
1669
 
 
1670
    queryData = None
 
1671
    delimiter = CR + NL
 
1672
    client = None
 
1673
    remoteParty = None
 
1674
    buffer = ""
 
1675
 
 
1676
    def __init__(self, client, queryData=None):
 
1677
        """Initialize a new DCC CHAT session.
 
1678
 
 
1679
        queryData is a 3-tuple of
 
1680
        (fromUser, targetUserOrChannel, data)
 
1681
        as received by the CTCP query.
 
1682
 
 
1683
        (To be honest, fromUser is the only thing that's currently
 
1684
        used here. targetUserOrChannel is potentially useful, while
 
1685
        the 'data' argument is soley for informational purposes.)
 
1686
        """
 
1687
        self.client = client
 
1688
        if queryData:
 
1689
            self.queryData = queryData
 
1690
            self.remoteParty = self.queryData[0]
 
1691
 
 
1692
    def dataReceived(self, data):
 
1693
        self.buffer = self.buffer + data
 
1694
        lines = string.split(self.buffer, LF)
 
1695
        # Put the (possibly empty) element after the last LF back in the
 
1696
        # buffer
 
1697
        self.buffer = lines.pop()
 
1698
 
 
1699
        for line in lines:
 
1700
            if line[-1] == CR:
 
1701
                line = line[:-1]
 
1702
            self.lineReceived(line)
 
1703
 
 
1704
    def lineReceived(self, line):
 
1705
        log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
 
1706
        self.client.privmsg(self.remoteParty,
 
1707
                            self.client.nickname, line)
 
1708
 
 
1709
 
 
1710
class DccChatFactory(protocol.ClientFactory):
 
1711
    protocol = DccChat
 
1712
    noisy = 0
 
1713
    def __init__(self, client, queryData):
 
1714
        self.client = client
 
1715
        self.queryData = queryData
 
1716
 
 
1717
    def buildProtocol(self, addr):
 
1718
        p = self.protocol(client=self.client, queryData=self.queryData)
 
1719
        p.factory = self
 
1720
 
 
1721
    def clientConnectionFailed(self, unused_connector, unused_reason):
 
1722
        self.client.dcc_sessions.remove(self)
 
1723
 
 
1724
    def clientConnectionLost(self, unused_connector, unused_reason):
 
1725
        self.client.dcc_sessions.remove(self)
 
1726
 
 
1727
 
 
1728
def dccDescribe(data):
 
1729
    """Given the data chunk from a DCC query, return a descriptive string.
 
1730
    """
 
1731
 
 
1732
    orig_data = data
 
1733
    data = string.split(data)
 
1734
    if len(data) < 4:
 
1735
        return orig_data
 
1736
 
 
1737
    (dcctype, arg, address, port) = data[:4]
 
1738
 
 
1739
    if '.' in address:
 
1740
        pass
 
1741
    else:
 
1742
        try:
 
1743
            address = long(address)
 
1744
        except ValueError:
 
1745
            pass
 
1746
        else:
 
1747
            address = (
 
1748
                (address >> 24) & 0xFF,
 
1749
                (address >> 16) & 0xFF,
 
1750
                (address >> 8) & 0xFF,
 
1751
                address & 0xFF,
 
1752
                )
 
1753
            # The mapping to 'int' is to get rid of those accursed
 
1754
            # "L"s which python 1.5.2 puts on the end of longs.
 
1755
            address = string.join(map(str,map(int,address)), ".")
 
1756
 
 
1757
    if dcctype == 'SEND':
 
1758
        filename = arg
 
1759
 
 
1760
        size_txt = ''
 
1761
        if len(data) >= 5:
 
1762
            try:
 
1763
                size = int(data[4])
 
1764
                size_txt = ' of size %d bytes' % (size,)
 
1765
            except ValueError:
 
1766
                pass
 
1767
 
 
1768
        dcc_text = ("SEND for file '%s'%s at host %s, port %s"
 
1769
                    % (filename, size_txt, address, port))
 
1770
    elif dcctype == 'CHAT':
 
1771
        dcc_text = ("CHAT for host %s, port %s"
 
1772
                    % (address, port))
 
1773
    else:
 
1774
        dcc_text = orig_data
 
1775
 
 
1776
    return dcc_text
 
1777
 
 
1778
 
 
1779
class DccFileReceive(DccFileReceiveBasic):
 
1780
    """Higher-level coverage for getting a file from DCC SEND.
 
1781
 
 
1782
    I allow you to change the file's name and destination directory.
 
1783
    I won't overwrite an existing file unless I've been told it's okay
 
1784
    to do so. If passed the resumeOffset keyword argument I will attempt to
 
1785
    resume the file from that amount of bytes.
 
1786
 
 
1787
    XXX: I need to let the client know when I am finished.
 
1788
    XXX: I need to decide how to keep a progress indicator updated.
 
1789
    XXX: Client needs a way to tell me \"Do not finish until I say so.\"
 
1790
    XXX: I need to make sure the client understands if the file cannot be written.
 
1791
    """
 
1792
 
 
1793
    filename = 'dcc'
 
1794
    fileSize = -1
 
1795
    destDir = '.'
 
1796
    overwrite = 0
 
1797
    fromUser = None
 
1798
    queryData = None
 
1799
 
 
1800
    def __init__(self, filename, fileSize=-1, queryData=None,
 
1801
                 destDir='.', resumeOffset=0):
 
1802
        DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
 
1803
        self.filename = filename
 
1804
        self.destDir = destDir
 
1805
        self.fileSize = fileSize
 
1806
 
 
1807
        if queryData:
 
1808
            self.queryData = queryData
 
1809
            self.fromUser = self.queryData[0]
 
1810
 
 
1811
    def set_directory(self, directory):
 
1812
        """Set the directory where the downloaded file will be placed.
 
1813
 
 
1814
        May raise OSError if the supplied directory path is not suitable.
 
1815
        """
 
1816
        if not path.exists(directory):
 
1817
            raise OSError(errno.ENOENT, "You see no directory there.",
 
1818
                          directory)
 
1819
        if not path.isdir(directory):
 
1820
            raise OSError(errno.ENOTDIR, "You cannot put a file into "
 
1821
                          "something which is not a directory.",
 
1822
                          directory)
 
1823
        if not os.access(directory, os.X_OK | os.W_OK):
 
1824
            raise OSError(errno.EACCES,
 
1825
                          "This directory is too hard to write in to.",
 
1826
                          directory)
 
1827
        self.destDir = directory
 
1828
 
 
1829
    def set_filename(self, filename):
 
1830
        """Change the name of the file being transferred.
 
1831
 
 
1832
        This replaces the file name provided by the sender.
 
1833
        """
 
1834
        self.filename = filename
 
1835
 
 
1836
    def set_overwrite(self, boolean):
 
1837
        """May I overwrite existing files?
 
1838
        """
 
1839
        self.overwrite = boolean
 
1840
 
 
1841
 
 
1842
    # Protocol-level methods.
 
1843
 
 
1844
    def connectionMade(self):
 
1845
        dst = path.abspath(path.join(self.destDir,self.filename))
 
1846
        exists = path.exists(dst)
 
1847
        if self.resume and exists:
 
1848
            # I have been told I want to resume, and a file already
 
1849
            # exists - Here we go
 
1850
            self.file = open(dst, 'ab')
 
1851
            log.msg("Attempting to resume %s - starting from %d bytes" %
 
1852
                    (self.file, self.file.tell()))
 
1853
        elif self.overwrite or not exists:
 
1854
            self.file = open(dst, 'wb')
 
1855
        else:
 
1856
            raise OSError(errno.EEXIST,
 
1857
                          "There's a file in the way.  "
 
1858
                          "Perhaps that's why you cannot open it.",
 
1859
                          dst)
 
1860
 
 
1861
    def dataReceived(self, data):
 
1862
        self.file.write(data)
 
1863
        DccFileReceiveBasic.dataReceived(self, data)
 
1864
 
 
1865
        # XXX: update a progress indicator here?
 
1866
 
 
1867
    def connectionLost(self, reason):
 
1868
        """When the connection is lost, I close the file.
 
1869
        """
 
1870
        self.connected = 0
 
1871
        logmsg = ("%s closed." % (self,))
 
1872
        if self.fileSize > 0:
 
1873
            logmsg = ("%s  %d/%d bytes received"
 
1874
                      % (logmsg, self.bytesReceived, self.fileSize))
 
1875
            if self.bytesReceived == self.fileSize:
 
1876
                pass # Hooray!
 
1877
            elif self.bytesReceived < self.fileSize:
 
1878
                logmsg = ("%s (Warning: %d bytes short)"
 
1879
                          % (logmsg, self.fileSize - self.bytesReceived))
 
1880
            else:
 
1881
                logmsg = ("%s (file larger than expected)"
 
1882
                          % (logmsg,))
 
1883
        else:
 
1884
            logmsg = ("%s  %d bytes received"
 
1885
                      % (logmsg, self.bytesReceived))
 
1886
 
 
1887
        if hasattr(self, 'file'):
 
1888
            logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
 
1889
            if hasattr(self.file, 'close'): self.file.close()
 
1890
 
 
1891
        # self.transport.log(logmsg)
 
1892
 
 
1893
    def __str__(self):
 
1894
        if not self.connected:
 
1895
            return "<Unconnected DccFileReceive object at %x>" % (id(self),)
 
1896
        from_ = self.transport.getPeer()
 
1897
        if self.fromUser:
 
1898
            from_ = "%s (%s)" % (self.fromUser, from_)
 
1899
 
 
1900
        s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
 
1901
        return s
 
1902
 
 
1903
    def __repr__(self):
 
1904
        s = ("<%s at %x: GET %s>"
 
1905
             % (self.__class__, id(self), self.filename))
 
1906
        return s
 
1907
 
 
1908
 
 
1909
# CTCP constants and helper functions
 
1910
 
 
1911
X_DELIM = chr(001)
 
1912
 
 
1913
def ctcpExtract(message):
 
1914
    """Extract CTCP data from a string.
 
1915
 
 
1916
    Returns a dictionary with two items:
 
1917
 
 
1918
       - C{'extended'}: a list of CTCP (tag, data) tuples
 
1919
       - C{'normal'}: a list of strings which were not inside a CTCP delimeter
 
1920
    """
 
1921
 
 
1922
    extended_messages = []
 
1923
    normal_messages = []
 
1924
    retval = {'extended': extended_messages,
 
1925
              'normal': normal_messages }
 
1926
 
 
1927
    messages = string.split(message, X_DELIM)
 
1928
    odd = 0
 
1929
 
 
1930
    # X1 extended data X2 nomal data X3 extended data X4 normal...
 
1931
    while messages:
 
1932
        if odd:
 
1933
            extended_messages.append(messages.pop(0))
 
1934
        else:
 
1935
            normal_messages.append(messages.pop(0))
 
1936
        odd = not odd
 
1937
 
 
1938
    extended_messages[:] = filter(None, extended_messages)
 
1939
    normal_messages[:] = filter(None, normal_messages)
 
1940
 
 
1941
    extended_messages[:] = map(ctcpDequote, extended_messages)
 
1942
    for i in xrange(len(extended_messages)):
 
1943
        m = string.split(extended_messages[i], SPC, 1)
 
1944
        tag = m[0]
 
1945
        if len(m) > 1:
 
1946
            data = m[1]
 
1947
        else:
 
1948
            data = None
 
1949
 
 
1950
        extended_messages[i] = (tag, data)
 
1951
 
 
1952
    return retval
 
1953
 
 
1954
# CTCP escaping
 
1955
 
 
1956
M_QUOTE= chr(020)
 
1957
 
 
1958
mQuoteTable = {
 
1959
    NUL: M_QUOTE + '0',
 
1960
    NL: M_QUOTE + 'n',
 
1961
    CR: M_QUOTE + 'r',
 
1962
    M_QUOTE: M_QUOTE + M_QUOTE
 
1963
    }
 
1964
 
 
1965
mDequoteTable = {}
 
1966
for k, v in mQuoteTable.items():
 
1967
    mDequoteTable[v[-1]] = k
 
1968
del k, v
 
1969
 
 
1970
mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
 
1971
 
 
1972
def lowQuote(s):
 
1973
    for c in (M_QUOTE, NUL, NL, CR):
 
1974
        s = string.replace(s, c, mQuoteTable[c])
 
1975
    return s
 
1976
 
 
1977
def lowDequote(s):
 
1978
    def sub(matchobj, mDequoteTable=mDequoteTable):
 
1979
        s = matchobj.group()[1]
 
1980
        try:
 
1981
            s = mDequoteTable[s]
 
1982
        except KeyError:
 
1983
            s = s
 
1984
        return s
 
1985
 
 
1986
    return mEscape_re.sub(sub, s)
 
1987
 
 
1988
X_QUOTE = '\\'
 
1989
 
 
1990
xQuoteTable = {
 
1991
    X_DELIM: X_QUOTE + 'a',
 
1992
    X_QUOTE: X_QUOTE + X_QUOTE
 
1993
    }
 
1994
 
 
1995
xDequoteTable = {}
 
1996
 
 
1997
for k, v in xQuoteTable.items():
 
1998
    xDequoteTable[v[-1]] = k
 
1999
 
 
2000
xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
 
2001
 
 
2002
def ctcpQuote(s):
 
2003
    for c in (X_QUOTE, X_DELIM):
 
2004
        s = string.replace(s, c, xQuoteTable[c])
 
2005
    return s
 
2006
 
 
2007
def ctcpDequote(s):
 
2008
    def sub(matchobj, xDequoteTable=xDequoteTable):
 
2009
        s = matchobj.group()[1]
 
2010
        try:
 
2011
            s = xDequoteTable[s]
 
2012
        except KeyError:
 
2013
            s = s
 
2014
        return s
 
2015
 
 
2016
    return xEscape_re.sub(sub, s)
 
2017
 
 
2018
def ctcpStringify(messages):
 
2019
    """
 
2020
    @type messages: a list of extended messages.  An extended
 
2021
    message is a (tag, data) tuple, where 'data' may be C{None}, a
 
2022
    string, or a list of strings to be joined with whitespace.
 
2023
 
 
2024
    @returns: String
 
2025
    """
 
2026
    coded_messages = []
 
2027
    for (tag, data) in messages:
 
2028
        if data:
 
2029
            if not isinstance(data, types.StringType):
 
2030
                try:
 
2031
                    # data as list-of-strings
 
2032
                    data = " ".join(map(str, data))
 
2033
                except TypeError:
 
2034
                    # No?  Then use it's %s representation.
 
2035
                    pass
 
2036
            m = "%s %s" % (tag, data)
 
2037
        else:
 
2038
            m = str(tag)
 
2039
        m = ctcpQuote(m)
 
2040
        m = "%s%s%s" % (X_DELIM, m, X_DELIM)
 
2041
        coded_messages.append(m)
 
2042
 
 
2043
    line = string.join(coded_messages, '')
 
2044
    return line
 
2045
 
 
2046
 
 
2047
# Constants (from RFC 2812)
 
2048
RPL_WELCOME = '001'
 
2049
RPL_YOURHOST = '002'
 
2050
RPL_CREATED = '003'
 
2051
RPL_MYINFO = '004'
 
2052
RPL_BOUNCE = '005'
 
2053
RPL_USERHOST = '302'
 
2054
RPL_ISON = '303'
 
2055
RPL_AWAY = '301'
 
2056
RPL_UNAWAY = '305'
 
2057
RPL_NOWAWAY = '306'
 
2058
RPL_WHOISUSER = '311'
 
2059
RPL_WHOISSERVER = '312'
 
2060
RPL_WHOISOPERATOR = '313'
 
2061
RPL_WHOISIDLE = '317'
 
2062
RPL_ENDOFWHOIS = '318'
 
2063
RPL_WHOISCHANNELS = '319'
 
2064
RPL_WHOWASUSER = '314'
 
2065
RPL_ENDOFWHOWAS = '369'
 
2066
RPL_LISTSTART = '321'
 
2067
RPL_LIST = '322'
 
2068
RPL_LISTEND = '323'
 
2069
RPL_UNIQOPIS = '325'
 
2070
RPL_CHANNELMODEIS = '324'
 
2071
RPL_NOTOPIC = '331'
 
2072
RPL_TOPIC = '332'
 
2073
RPL_INVITING = '341'
 
2074
RPL_SUMMONING = '342'
 
2075
RPL_INVITELIST = '346'
 
2076
RPL_ENDOFINVITELIST = '347'
 
2077
RPL_EXCEPTLIST = '348'
 
2078
RPL_ENDOFEXCEPTLIST = '349'
 
2079
RPL_VERSION = '351'
 
2080
RPL_WHOREPLY = '352'
 
2081
RPL_ENDOFWHO = '315'
 
2082
RPL_NAMREPLY = '353'
 
2083
RPL_ENDOFNAMES = '366'
 
2084
RPL_LINKS = '364'
 
2085
RPL_ENDOFLINKS = '365'
 
2086
RPL_BANLIST = '367'
 
2087
RPL_ENDOFBANLIST = '368'
 
2088
RPL_INFO = '371'
 
2089
RPL_ENDOFINFO = '374'
 
2090
RPL_MOTDSTART = '375'
 
2091
RPL_MOTD = '372'
 
2092
RPL_ENDOFMOTD = '376'
 
2093
RPL_YOUREOPER = '381'
 
2094
RPL_REHASHING = '382'
 
2095
RPL_YOURESERVICE = '383'
 
2096
RPL_TIME = '391'
 
2097
RPL_USERSSTART = '392'
 
2098
RPL_USERS = '393'
 
2099
RPL_ENDOFUSERS = '394'
 
2100
RPL_NOUSERS = '395'
 
2101
RPL_TRACELINK = '200'
 
2102
RPL_TRACECONNECTING = '201'
 
2103
RPL_TRACEHANDSHAKE = '202'
 
2104
RPL_TRACEUNKNOWN = '203'
 
2105
RPL_TRACEOPERATOR = '204'
 
2106
RPL_TRACEUSER = '205'
 
2107
RPL_TRACESERVER = '206'
 
2108
RPL_TRACESERVICE = '207'
 
2109
RPL_TRACENEWTYPE = '208'
 
2110
RPL_TRACECLASS = '209'
 
2111
RPL_TRACERECONNECT = '210'
 
2112
RPL_TRACELOG = '261'
 
2113
RPL_TRACEEND = '262'
 
2114
RPL_STATSLINKINFO = '211'
 
2115
RPL_STATSCOMMANDS = '212'
 
2116
RPL_ENDOFSTATS = '219'
 
2117
RPL_STATSUPTIME = '242'
 
2118
RPL_STATSOLINE = '243'
 
2119
RPL_UMODEIS = '221'
 
2120
RPL_SERVLIST = '234'
 
2121
RPL_SERVLISTEND = '235'
 
2122
RPL_LUSERCLIENT = '251'
 
2123
RPL_LUSEROP = '252'
 
2124
RPL_LUSERUNKNOWN = '253'
 
2125
RPL_LUSERCHANNELS = '254'
 
2126
RPL_LUSERME = '255'
 
2127
RPL_ADMINME = '256'
 
2128
RPL_ADMINLOC = '257'
 
2129
RPL_ADMINLOC = '258'
 
2130
RPL_ADMINEMAIL = '259'
 
2131
RPL_TRYAGAIN = '263'
 
2132
ERR_NOSUCHNICK = '401'
 
2133
ERR_NOSUCHSERVER = '402'
 
2134
ERR_NOSUCHCHANNEL = '403'
 
2135
ERR_CANNOTSENDTOCHAN = '404'
 
2136
ERR_TOOMANYCHANNELS = '405'
 
2137
ERR_WASNOSUCHNICK = '406'
 
2138
ERR_TOOMANYTARGETS = '407'
 
2139
ERR_NOSUCHSERVICE = '408'
 
2140
ERR_NOORIGIN = '409'
 
2141
ERR_NORECIPIENT = '411'
 
2142
ERR_NOTEXTTOSEND = '412'
 
2143
ERR_NOTOPLEVEL = '413'
 
2144
ERR_WILDTOPLEVEL = '414'
 
2145
ERR_BADMASK = '415'
 
2146
ERR_UNKNOWNCOMMAND = '421'
 
2147
ERR_NOMOTD = '422'
 
2148
ERR_NOADMININFO = '423'
 
2149
ERR_FILEERROR = '424'
 
2150
ERR_NONICKNAMEGIVEN = '431'
 
2151
ERR_ERRONEUSNICKNAME = '432'
 
2152
ERR_NICKNAMEINUSE = '433'
 
2153
ERR_NICKCOLLISION = '436'
 
2154
ERR_UNAVAILRESOURCE = '437'
 
2155
ERR_USERNOTINCHANNEL = '441'
 
2156
ERR_NOTONCHANNEL = '442'
 
2157
ERR_USERONCHANNEL = '443'
 
2158
ERR_NOLOGIN = '444'
 
2159
ERR_SUMMONDISABLED = '445'
 
2160
ERR_USERSDISABLED = '446'
 
2161
ERR_NOTREGISTERED = '451'
 
2162
ERR_NEEDMOREPARAMS = '461'
 
2163
ERR_ALREADYREGISTRED = '462'
 
2164
ERR_NOPERMFORHOST = '463'
 
2165
ERR_PASSWDMISMATCH = '464'
 
2166
ERR_YOUREBANNEDCREEP = '465'
 
2167
ERR_YOUWILLBEBANNED = '466'
 
2168
ERR_KEYSET = '467'
 
2169
ERR_CHANNELISFULL = '471'
 
2170
ERR_UNKNOWNMODE = '472'
 
2171
ERR_INVITEONLYCHAN = '473'
 
2172
ERR_BANNEDFROMCHAN = '474'
 
2173
ERR_BADCHANNELKEY = '475'
 
2174
ERR_BADCHANMASK = '476'
 
2175
ERR_NOCHANMODES = '477'
 
2176
ERR_BANLISTFULL = '478'
 
2177
ERR_NOPRIVILEGES = '481'
 
2178
ERR_CHANOPRIVSNEEDED = '482'
 
2179
ERR_CANTKILLSERVER = '483'
 
2180
ERR_RESTRICTED = '484'
 
2181
ERR_UNIQOPPRIVSNEEDED = '485'
 
2182
ERR_NOOPERHOST = '491'
 
2183
ERR_NOSERVICEHOST = '492'
 
2184
ERR_UMODEUNKNOWNFLAG = '501'
 
2185
ERR_USERSDONTMATCH = '502'
 
2186
 
 
2187
# And hey, as long as the strings are already intern'd...
 
2188
symbolic_to_numeric = {
 
2189
    "RPL_WELCOME": '001',
 
2190
    "RPL_YOURHOST": '002',
 
2191
    "RPL_CREATED": '003',
 
2192
    "RPL_MYINFO": '004',
 
2193
    "RPL_BOUNCE": '005',
 
2194
    "RPL_USERHOST": '302',
 
2195
    "RPL_ISON": '303',
 
2196
    "RPL_AWAY": '301',
 
2197
    "RPL_UNAWAY": '305',
 
2198
    "RPL_NOWAWAY": '306',
 
2199
    "RPL_WHOISUSER": '311',
 
2200
    "RPL_WHOISSERVER": '312',
 
2201
    "RPL_WHOISOPERATOR": '313',
 
2202
    "RPL_WHOISIDLE": '317',
 
2203
    "RPL_ENDOFWHOIS": '318',
 
2204
    "RPL_WHOISCHANNELS": '319',
 
2205
    "RPL_WHOWASUSER": '314',
 
2206
    "RPL_ENDOFWHOWAS": '369',
 
2207
    "RPL_LISTSTART": '321',
 
2208
    "RPL_LIST": '322',
 
2209
    "RPL_LISTEND": '323',
 
2210
    "RPL_UNIQOPIS": '325',
 
2211
    "RPL_CHANNELMODEIS": '324',
 
2212
    "RPL_NOTOPIC": '331',
 
2213
    "RPL_TOPIC": '332',
 
2214
    "RPL_INVITING": '341',
 
2215
    "RPL_SUMMONING": '342',
 
2216
    "RPL_INVITELIST": '346',
 
2217
    "RPL_ENDOFINVITELIST": '347',
 
2218
    "RPL_EXCEPTLIST": '348',
 
2219
    "RPL_ENDOFEXCEPTLIST": '349',
 
2220
    "RPL_VERSION": '351',
 
2221
    "RPL_WHOREPLY": '352',
 
2222
    "RPL_ENDOFWHO": '315',
 
2223
    "RPL_NAMREPLY": '353',
 
2224
    "RPL_ENDOFNAMES": '366',
 
2225
    "RPL_LINKS": '364',
 
2226
    "RPL_ENDOFLINKS": '365',
 
2227
    "RPL_BANLIST": '367',
 
2228
    "RPL_ENDOFBANLIST": '368',
 
2229
    "RPL_INFO": '371',
 
2230
    "RPL_ENDOFINFO": '374',
 
2231
    "RPL_MOTDSTART": '375',
 
2232
    "RPL_MOTD": '372',
 
2233
    "RPL_ENDOFMOTD": '376',
 
2234
    "RPL_YOUREOPER": '381',
 
2235
    "RPL_REHASHING": '382',
 
2236
    "RPL_YOURESERVICE": '383',
 
2237
    "RPL_TIME": '391',
 
2238
    "RPL_USERSSTART": '392',
 
2239
    "RPL_USERS": '393',
 
2240
    "RPL_ENDOFUSERS": '394',
 
2241
    "RPL_NOUSERS": '395',
 
2242
    "RPL_TRACELINK": '200',
 
2243
    "RPL_TRACECONNECTING": '201',
 
2244
    "RPL_TRACEHANDSHAKE": '202',
 
2245
    "RPL_TRACEUNKNOWN": '203',
 
2246
    "RPL_TRACEOPERATOR": '204',
 
2247
    "RPL_TRACEUSER": '205',
 
2248
    "RPL_TRACESERVER": '206',
 
2249
    "RPL_TRACESERVICE": '207',
 
2250
    "RPL_TRACENEWTYPE": '208',
 
2251
    "RPL_TRACECLASS": '209',
 
2252
    "RPL_TRACERECONNECT": '210',
 
2253
    "RPL_TRACELOG": '261',
 
2254
    "RPL_TRACEEND": '262',
 
2255
    "RPL_STATSLINKINFO": '211',
 
2256
    "RPL_STATSCOMMANDS": '212',
 
2257
    "RPL_ENDOFSTATS": '219',
 
2258
    "RPL_STATSUPTIME": '242',
 
2259
    "RPL_STATSOLINE": '243',
 
2260
    "RPL_UMODEIS": '221',
 
2261
    "RPL_SERVLIST": '234',
 
2262
    "RPL_SERVLISTEND": '235',
 
2263
    "RPL_LUSERCLIENT": '251',
 
2264
    "RPL_LUSEROP": '252',
 
2265
    "RPL_LUSERUNKNOWN": '253',
 
2266
    "RPL_LUSERCHANNELS": '254',
 
2267
    "RPL_LUSERME": '255',
 
2268
    "RPL_ADMINME": '256',
 
2269
    "RPL_ADMINLOC": '257',
 
2270
    "RPL_ADMINLOC": '258',
 
2271
    "RPL_ADMINEMAIL": '259',
 
2272
    "RPL_TRYAGAIN": '263',
 
2273
    "ERR_NOSUCHNICK": '401',
 
2274
    "ERR_NOSUCHSERVER": '402',
 
2275
    "ERR_NOSUCHCHANNEL": '403',
 
2276
    "ERR_CANNOTSENDTOCHAN": '404',
 
2277
    "ERR_TOOMANYCHANNELS": '405',
 
2278
    "ERR_WASNOSUCHNICK": '406',
 
2279
    "ERR_TOOMANYTARGETS": '407',
 
2280
    "ERR_NOSUCHSERVICE": '408',
 
2281
    "ERR_NOORIGIN": '409',
 
2282
    "ERR_NORECIPIENT": '411',
 
2283
    "ERR_NOTEXTTOSEND": '412',
 
2284
    "ERR_NOTOPLEVEL": '413',
 
2285
    "ERR_WILDTOPLEVEL": '414',
 
2286
    "ERR_BADMASK": '415',
 
2287
    "ERR_UNKNOWNCOMMAND": '421',
 
2288
    "ERR_NOMOTD": '422',
 
2289
    "ERR_NOADMININFO": '423',
 
2290
    "ERR_FILEERROR": '424',
 
2291
    "ERR_NONICKNAMEGIVEN": '431',
 
2292
    "ERR_ERRONEUSNICKNAME": '432',
 
2293
    "ERR_NICKNAMEINUSE": '433',
 
2294
    "ERR_NICKCOLLISION": '436',
 
2295
    "ERR_UNAVAILRESOURCE": '437',
 
2296
    "ERR_USERNOTINCHANNEL": '441',
 
2297
    "ERR_NOTONCHANNEL": '442',
 
2298
    "ERR_USERONCHANNEL": '443',
 
2299
    "ERR_NOLOGIN": '444',
 
2300
    "ERR_SUMMONDISABLED": '445',
 
2301
    "ERR_USERSDISABLED": '446',
 
2302
    "ERR_NOTREGISTERED": '451',
 
2303
    "ERR_NEEDMOREPARAMS": '461',
 
2304
    "ERR_ALREADYREGISTRED": '462',
 
2305
    "ERR_NOPERMFORHOST": '463',
 
2306
    "ERR_PASSWDMISMATCH": '464',
 
2307
    "ERR_YOUREBANNEDCREEP": '465',
 
2308
    "ERR_YOUWILLBEBANNED": '466',
 
2309
    "ERR_KEYSET": '467',
 
2310
    "ERR_CHANNELISFULL": '471',
 
2311
    "ERR_UNKNOWNMODE": '472',
 
2312
    "ERR_INVITEONLYCHAN": '473',
 
2313
    "ERR_BANNEDFROMCHAN": '474',
 
2314
    "ERR_BADCHANNELKEY": '475',
 
2315
    "ERR_BADCHANMASK": '476',
 
2316
    "ERR_NOCHANMODES": '477',
 
2317
    "ERR_BANLISTFULL": '478',
 
2318
    "ERR_NOPRIVILEGES": '481',
 
2319
    "ERR_CHANOPRIVSNEEDED": '482',
 
2320
    "ERR_CANTKILLSERVER": '483',
 
2321
    "ERR_RESTRICTED": '484',
 
2322
    "ERR_UNIQOPPRIVSNEEDED": '485',
 
2323
    "ERR_NOOPERHOST": '491',
 
2324
    "ERR_NOSERVICEHOST": '492',
 
2325
    "ERR_UMODEUNKNOWNFLAG": '501',
 
2326
    "ERR_USERSDONTMATCH": '502',
 
2327
}
 
2328
 
 
2329
numeric_to_symbolic = {}
 
2330
for k, v in symbolic_to_numeric.items():
 
2331
    numeric_to_symbolic[v] = k