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

« back to all changes in this revision

Viewing changes to twisted/words/protocols/msn.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 -*-
 
2
# Copyright (c) 2001-2005 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
#
 
6
 
 
7
"""
 
8
MSNP8 Protocol (client only) - semi-experimental
 
9
 
 
10
Stability: unstable.
 
11
 
 
12
This module provides support for clients using the MSN Protocol (MSNP8).
 
13
There are basically 3 servers involved in any MSN session:
 
14
 
 
15
I{Dispatch server}
 
16
 
 
17
The DispatchClient class handles connections to the
 
18
dispatch server, which basically delegates users to a
 
19
suitable notification server.
 
20
 
 
21
You will want to subclass this and handle the gotNotificationReferral
 
22
method appropriately.
 
23
    
 
24
I{Notification Server}
 
25
 
 
26
The NotificationClient class handles connections to the
 
27
notification server, which acts as a session server
 
28
(state updates, message negotiation etc...)
 
29
 
 
30
I{Switcboard Server}
 
31
 
 
32
The SwitchboardClient handles connections to switchboard
 
33
servers which are used to conduct conversations with other users.
 
34
 
 
35
There are also two classes (FileSend and FileReceive) used
 
36
for file transfers.
 
37
 
 
38
Clients handle events in two ways.
 
39
 
 
40
  - each client request requiring a response will return a Deferred,
 
41
    the callback for same will be fired when the server sends the
 
42
    required response
 
43
  - Events which are not in response to any client request have
 
44
    respective methods which should be overridden and handled in
 
45
    an adequate manner
 
46
 
 
47
Most client request callbacks require more than one argument,
 
48
and since Deferreds can only pass the callback one result,
 
49
most of the time the callback argument will be a tuple of
 
50
values (documented in the respective request method).
 
51
To make reading/writing code easier, callbacks can be defined in
 
52
a number of ways to handle this 'cleanly'. One way would be to
 
53
define methods like: def callBack(self, (arg1, arg2, arg)): ...
 
54
another way would be to do something like:
 
55
d.addCallback(lambda result: myCallback(*result)).
 
56
 
 
57
If the server sends an error response to a client request,
 
58
the errback of the corresponding Deferred will be called,
 
59
the argument being the corresponding error code.
 
60
 
 
61
B{NOTE}:
 
62
Due to the lack of an official spec for MSNP8, extra checking
 
63
than may be deemed necessary often takes place considering the
 
64
server is never 'wrong'. Thus, if gotBadLine (in any of the 3
 
65
main clients) is called, or an MSNProtocolError is raised, it's
 
66
probably a good idea to submit a bug report. ;)
 
67
Use of this module requires that PyOpenSSL is installed.
 
68
 
 
69
TODO
 
70
====
 
71
- check message hooks with invalid x-msgsinvite messages.
 
72
- font handling
 
73
- switchboard factory
 
74
 
 
75
@author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
 
76
"""
 
77
 
 
78
from __future__ import nested_scopes
 
79
 
 
80
 
 
81
# Twisted imports
 
82
from twisted.internet import reactor
 
83
from twisted.internet.defer import Deferred
 
84
from twisted.internet.protocol import ClientFactory
 
85
from twisted.internet.ssl import ClientContextFactory
 
86
from twisted.python import failure, log
 
87
 
 
88
from twisted.protocols.basic import LineReceiver
 
89
from twisted.web.http import HTTPClient
 
90
 
 
91
# System imports
 
92
import types, operator, os, md5
 
93
from random import randint
 
94
from urllib import quote, unquote
 
95
 
 
96
MSN_PROTOCOL_VERSION = "MSNP8 CVR0"       # protocol version
 
97
MSN_PORT             = 1863               # default dispatch server port
 
98
MSN_MAX_MESSAGE      = 1664               # max message length
 
99
MSN_CHALLENGE_STR    = "Q1P7W2E4J9R8U3S5" # used for server challenges
 
100
MSN_CVR_STR          = "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(
 
101
 
 
102
# auth constants
 
103
LOGIN_SUCCESS  = 1
 
104
LOGIN_FAILURE  = 2
 
105
LOGIN_REDIRECT = 3
 
106
 
 
107
# list constants
 
108
FORWARD_LIST = 1
 
109
ALLOW_LIST   = 2
 
110
BLOCK_LIST   = 4
 
111
REVERSE_LIST = 8
 
112
 
 
113
# phone constants
 
114
HOME_PHONE   = "PHH"
 
115
WORK_PHONE   = "PHW"
 
116
MOBILE_PHONE = "PHM"
 
117
HAS_PAGER    = "MOB"
 
118
 
 
119
# status constants
 
120
STATUS_ONLINE  = 'NLN'
 
121
STATUS_OFFLINE = 'FLN'
 
122
STATUS_HIDDEN  = 'HDN'
 
123
STATUS_IDLE    = 'IDL'
 
124
STATUS_AWAY    = 'AWY'
 
125
STATUS_BUSY    = 'BSY'
 
126
STATUS_BRB     = 'BRB'
 
127
STATUS_PHONE   = 'PHN'
 
128
STATUS_LUNCH   = 'LUN'
 
129
 
 
130
CR = "\r"
 
131
LF = "\n"
 
132
 
 
133
def checkParamLen(num, expected, cmd, error=None):
 
134
    if error == None:
 
135
        error = "Invalid Number of Parameters for %s" % cmd
 
136
    if num != expected:
 
137
        raise MSNProtocolError, error
 
138
 
 
139
def _parseHeader(h, v):
 
140
    """
 
141
    Split a certin number of known
 
142
    header values with the format:
 
143
    field1=val,field2=val,field3=val into
 
144
    a dict mapping fields to values.
 
145
    @param h: the header's key
 
146
    @param v: the header's value as a string
 
147
    """
 
148
 
 
149
    if h in ('passporturls','authentication-info','www-authenticate'):
 
150
        v = v.replace('Passport1.4','').lstrip()
 
151
        fields = {}
 
152
        for fieldPair in v.split(','):
 
153
            try:
 
154
                field,value = fieldPair.split('=',1)
 
155
                fields[field.lower()] = value
 
156
            except ValueError:
 
157
                fields[field.lower()] = ''
 
158
        return fields
 
159
    else:
 
160
        return v
 
161
 
 
162
def _parsePrimitiveHost(host):
 
163
    # Ho Ho Ho
 
164
    h,p = host.replace('https://','').split('/',1)
 
165
    p = '/' + p
 
166
    return h,p
 
167
 
 
168
def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
 
169
    """
 
170
    This function is used internally and should not ever be called
 
171
    directly.
 
172
    """
 
173
    cb = Deferred()
 
174
    def _cb(server, auth):
 
175
        loginFac = ClientFactory()
 
176
        loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
 
177
        reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
 
178
 
 
179
    if cached:
 
180
        _cb(nexusServer, authData)
 
181
    else:
 
182
        fac = ClientFactory()
 
183
        d = Deferred()
 
184
        d.addCallbacks(_cb, callbackArgs=(authData,))
 
185
        d.addErrback(lambda f: cb.errback(f))
 
186
        fac.protocol = lambda : PassportNexus(d, nexusServer)
 
187
        reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
 
188
    return cb
 
189
 
 
190
 
 
191
class PassportNexus(HTTPClient):
 
192
    
 
193
    """
 
194
    Used to obtain the URL of a valid passport
 
195
    login HTTPS server.
 
196
 
 
197
    This class is used internally and should
 
198
    not be instantiated directly -- that is,
 
199
    The passport logging in process is handled
 
200
    transparantly by NotificationClient.
 
201
    """
 
202
 
 
203
    def __init__(self, deferred, host):
 
204
        self.deferred = deferred
 
205
        self.host, self.path = _parsePrimitiveHost(host)
 
206
 
 
207
    def connectionMade(self):
 
208
        HTTPClient.connectionMade(self)
 
209
        self.sendCommand('GET', self.path)
 
210
        self.sendHeader('Host', self.host)
 
211
        self.endHeaders()
 
212
        self.headers = {}
 
213
 
 
214
    def handleHeader(self, header, value):
 
215
        h = header.lower()
 
216
        self.headers[h] = _parseHeader(h, value)
 
217
 
 
218
    def handleEndHeaders(self):
 
219
        if self.connected:
 
220
            self.transport.loseConnection()
 
221
        if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
 
222
            self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
 
223
        self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
 
224
 
 
225
    def handleResponse(self, r):
 
226
        pass
 
227
 
 
228
class PassportLogin(HTTPClient):
 
229
    """
 
230
    This class is used internally to obtain
 
231
    a login ticket from a passport HTTPS
 
232
    server -- it should not be used directly.
 
233
    """
 
234
 
 
235
    _finished = 0
 
236
 
 
237
    def __init__(self, deferred, userHandle, passwd, host, authData):
 
238
        self.deferred = deferred
 
239
        self.userHandle = userHandle
 
240
        self.passwd = passwd
 
241
        self.authData = authData
 
242
        self.host, self.path = _parsePrimitiveHost(host)
 
243
 
 
244
    def connectionMade(self):
 
245
        self.sendCommand('GET', self.path)
 
246
        self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
 
247
                                         'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
 
248
        self.sendHeader('Host', self.host)
 
249
        self.endHeaders()
 
250
        self.headers = {}
 
251
 
 
252
    def handleHeader(self, header, value):
 
253
        h = header.lower()
 
254
        self.headers[h] = _parseHeader(h, value)
 
255
 
 
256
    def handleEndHeaders(self):
 
257
        if self._finished:
 
258
            return
 
259
        self._finished = 1 # I think we need this because of HTTPClient
 
260
        if self.connected:
 
261
            self.transport.loseConnection()
 
262
        authHeader = 'authentication-info'
 
263
        _interHeader = 'www-authenticate'
 
264
        if self.headers.has_key(_interHeader):
 
265
            authHeader = _interHeader
 
266
        try:
 
267
            info = self.headers[authHeader]
 
268
            status = info['da-status']
 
269
            handler = getattr(self, 'login_%s' % (status,), None)
 
270
            if handler:
 
271
                handler(info)
 
272
            else:
 
273
                raise Exception()
 
274
        except Exception, e:
 
275
            self.deferred.errback(failure.Failure(e))
 
276
 
 
277
    def handleResponse(self, r):
 
278
        pass
 
279
 
 
280
    def login_success(self, info):
 
281
        ticket = info['from-pp']
 
282
        ticket = ticket[1:len(ticket)-1]
 
283
        self.deferred.callback((LOGIN_SUCCESS, ticket))
 
284
 
 
285
    def login_failed(self, info):
 
286
        self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
 
287
 
 
288
    def login_redir(self, info):
 
289
        self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
 
290
 
 
291
 
 
292
class MSNProtocolError(Exception):
 
293
    """
 
294
    This Exception is basically used for debugging
 
295
    purposes, as the official MSN server should never
 
296
    send anything _wrong_ and nobody in their right
 
297
    mind would run their B{own} MSN server.
 
298
    If it is raised by default command handlers
 
299
    (handle_BLAH) the error will be logged.
 
300
    """
 
301
    pass
 
302
 
 
303
 
 
304
class MSNCommandFailed(Exception):
 
305
    """
 
306
    The server said that the command failed.
 
307
    """
 
308
 
 
309
    def __init__(self, errorCode):
 
310
        self.errorCode = errorCode
 
311
 
 
312
    def __str__(self):
 
313
        return ("Command failed: %s (error code %d)"
 
314
                % (errorCodes[self.errorCode], self.errorCode))
 
315
 
 
316
 
 
317
class MSNMessage:
 
318
    """
 
319
    I am the class used to represent an 'instant' message.
 
320
 
 
321
    @ivar userHandle: The user handle (passport) of the sender
 
322
                      (this is only used when receiving a message)
 
323
    @ivar screenName: The screen name of the sender (this is only used
 
324
                      when receiving a message)
 
325
    @ivar message: The message
 
326
    @ivar headers: The message headers
 
327
    @type headers: dict
 
328
    @ivar length: The message length (including headers and line endings)
 
329
    @ivar ack: This variable is used to tell the server how to respond
 
330
               once the message has been sent. If set to MESSAGE_ACK
 
331
               (default) the server will respond with an ACK upon receiving
 
332
               the message, if set to MESSAGE_NACK the server will respond
 
333
               with a NACK upon failure to receive the message.
 
334
               If set to MESSAGE_ACK_NONE the server will do nothing.
 
335
               This is relevant for the return value of
 
336
               SwitchboardClient.sendMessage (which will return
 
337
               a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK  
 
338
               and will fire when the respective ACK or NACK is received).
 
339
               If set to MESSAGE_ACK_NONE sendMessage will return None.
 
340
    """
 
341
    MESSAGE_ACK      = 'A'
 
342
    MESSAGE_NACK     = 'N'
 
343
    MESSAGE_ACK_NONE = 'U'
 
344
 
 
345
    ack = MESSAGE_ACK
 
346
 
 
347
    def __init__(self, length=0, userHandle="", screenName="", message=""):
 
348
        self.userHandle = userHandle
 
349
        self.screenName = screenName
 
350
        self.message = message
 
351
        self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
 
352
        self.length = length
 
353
        self.readPos = 0
 
354
 
 
355
    def _calcMessageLen(self):
 
356
        """
 
357
        used to calculte the number to send
 
358
        as the message length when sending a message.
 
359
        """
 
360
        return reduce(operator.add, [len(x[0]) + len(x[1]) + 4  for x in self.headers.items()]) + len(self.message) + 2
 
361
 
 
362
    def setHeader(self, header, value):
 
363
        """ set the desired header """
 
364
        self.headers[header] = value
 
365
 
 
366
    def getHeader(self, header):
 
367
        """
 
368
        get the desired header value
 
369
        @raise KeyError: if no such header exists.
 
370
        """
 
371
        return self.headers[header]
 
372
 
 
373
    def hasHeader(self, header):
 
374
        """ check to see if the desired header exists """
 
375
        return self.headers.has_key(header)
 
376
 
 
377
    def getMessage(self):
 
378
        """ return the message - not including headers """
 
379
        return self.message
 
380
 
 
381
    def setMessage(self, message):
 
382
        """ set the message text """
 
383
        self.message = message
 
384
 
 
385
class MSNContact:
 
386
    
 
387
    """
 
388
    This class represents a contact (user).
 
389
 
 
390
    @ivar userHandle: The contact's user handle (passport).
 
391
    @ivar screenName: The contact's screen name.
 
392
    @ivar groups: A list of all the group IDs which this
 
393
                  contact belongs to.
 
394
    @ivar lists: An integer representing the sum of all lists
 
395
                 that this contact belongs to.
 
396
    @ivar status: The contact's status code.
 
397
    @type status: str if contact's status is known, None otherwise.
 
398
 
 
399
    @ivar homePhone: The contact's home phone number.
 
400
    @type homePhone: str if known, otherwise None.
 
401
    @ivar workPhone: The contact's work phone number.
 
402
    @type workPhone: str if known, otherwise None.
 
403
    @ivar mobilePhone: The contact's mobile phone number.
 
404
    @type mobilePhone: str if known, otherwise None.
 
405
    @ivar hasPager: Whether or not this user has a mobile pager
 
406
                    (true=yes, false=no)
 
407
    """
 
408
    
 
409
    def __init__(self, userHandle="", screenName="", lists=0, groups=[], status=None):
 
410
        self.userHandle = userHandle
 
411
        self.screenName = screenName
 
412
        self.lists = lists
 
413
        self.groups = [] # if applicable
 
414
        self.status = status # current status
 
415
 
 
416
        # phone details
 
417
        self.homePhone   = None
 
418
        self.workPhone   = None
 
419
        self.mobilePhone = None
 
420
        self.hasPager    = None
 
421
 
 
422
    def setPhone(self, phoneType, value):
 
423
        """
 
424
        set phone numbers/values for this specific user.
 
425
        for phoneType check the *_PHONE constants and HAS_PAGER
 
426
        """
 
427
 
 
428
        t = phoneType.upper()
 
429
        if t == HOME_PHONE:
 
430
            self.homePhone = value
 
431
        elif t == WORK_PHONE:
 
432
            self.workPhone = value
 
433
        elif t == MOBILE_PHONE:
 
434
            self.mobilePhone = value
 
435
        elif t == HAS_PAGER:
 
436
            self.hasPager = value
 
437
        else:
 
438
            raise ValueError, "Invalid Phone Type"
 
439
 
 
440
    def addToList(self, listType):
 
441
        """
 
442
        Update the lists attribute to
 
443
        reflect being part of the
 
444
        given list.
 
445
        """
 
446
        self.lists |= listType
 
447
 
 
448
    def removeFromList(self, listType):
 
449
        """
 
450
        Update the lists attribute to
 
451
        reflect being removed from the
 
452
        given list.
 
453
        """
 
454
        self.lists ^= listType
 
455
 
 
456
class MSNContactList:
 
457
    """
 
458
    This class represents a basic MSN contact list.
 
459
 
 
460
    @ivar contacts: All contacts on my various lists
 
461
    @type contacts: dict (mapping user handles to MSNContact objects)
 
462
    @ivar version: The current contact list version (used for list syncing)
 
463
    @ivar groups: a mapping of group ids to group names
 
464
                  (groups can only exist on the forward list)
 
465
    @type groups: dict
 
466
 
 
467
    B{Note}:
 
468
    This is used only for storage and doesn't effect the
 
469
    server's contact list.
 
470
    """
 
471
 
 
472
    def __init__(self):
 
473
        self.contacts = {}
 
474
        self.version = 0
 
475
        self.groups = {}
 
476
        self.autoAdd = 0
 
477
        self.privacy = 0
 
478
 
 
479
    def _getContactsFromList(self, listType):
 
480
        """
 
481
        Obtain all contacts which belong
 
482
        to the given list type.
 
483
        """
 
484
        return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
 
485
 
 
486
    def addContact(self, contact):
 
487
        """
 
488
        Add a contact
 
489
        """
 
490
        self.contacts[contact.userHandle] = contact
 
491
 
 
492
    def remContact(self, userHandle):
 
493
        """
 
494
        Remove a contact
 
495
        """
 
496
        try:
 
497
            del self.contacts[userHandle]
 
498
        except KeyError:
 
499
            pass
 
500
 
 
501
    def getContact(self, userHandle):
 
502
        """
 
503
        Obtain the MSNContact object
 
504
        associated with the given
 
505
        userHandle.
 
506
        @return: the MSNContact object if
 
507
                 the user exists, or None.
 
508
        """
 
509
        try:
 
510
            return self.contacts[userHandle]
 
511
        except KeyError:
 
512
            return None
 
513
 
 
514
    def getBlockedContacts(self):
 
515
        """
 
516
        Obtain all the contacts on my block list
 
517
        """
 
518
        return self._getContactsFromList(BLOCK_LIST)
 
519
 
 
520
    def getAuthorizedContacts(self):
 
521
        """
 
522
        Obtain all the contacts on my auth list.
 
523
        (These are contacts which I have verified
 
524
        can view my state changes).
 
525
        """
 
526
        return self._getContactsFromList(ALLOW_LIST)
 
527
 
 
528
    def getReverseContacts(self):
 
529
        """
 
530
        Get all contacts on my reverse list.
 
531
        (These are contacts which have added me
 
532
        to their forward list).
 
533
        """
 
534
        return self._getContactsFromList(REVERSE_LIST)
 
535
 
 
536
    def getContacts(self):
 
537
        """
 
538
        Get all contacts on my forward list.
 
539
        (These are the contacts which I have added
 
540
        to my list).
 
541
        """
 
542
        return self._getContactsFromList(FORWARD_LIST)
 
543
 
 
544
    def setGroup(self, id, name):
 
545
        """
 
546
        Keep a mapping from the given id
 
547
        to the given name.
 
548
        """
 
549
        self.groups[id] = name
 
550
 
 
551
    def remGroup(self, id):
 
552
        """
 
553
        Removed the stored group
 
554
        mapping for the given id.
 
555
        """
 
556
        try:
 
557
            del self.groups[id]
 
558
        except KeyError:
 
559
            pass
 
560
        for c in self.contacts:
 
561
            if id in c.groups:
 
562
                c.groups.remove(id)
 
563
 
 
564
 
 
565
class MSNEventBase(LineReceiver):
 
566
    """
 
567
    This class provides support for handling / dispatching events and is the
 
568
    base class of the three main client protocols (DispatchClient,
 
569
    NotificationClient, SwitchboardClient)
 
570
    """
 
571
 
 
572
    def __init__(self):
 
573
        self.ids = {} # mapping of ids to Deferreds
 
574
        self.currentID = 0
 
575
        self.connected = 0
 
576
        self.setLineMode()
 
577
        self.currentMessage = None
 
578
 
 
579
    def connectionLost(self, reason):
 
580
        self.ids = {}
 
581
        self.connected = 0
 
582
 
 
583
    def connectionMade(self):
 
584
        self.connected = 1
 
585
 
 
586
    def _fireCallback(self, id, *args):
 
587
        """
 
588
        Fire the callback for the given id
 
589
        if one exists and return 1, else return false
 
590
        """
 
591
        if self.ids.has_key(id):
 
592
            self.ids[id][0].callback(args)
 
593
            del self.ids[id]
 
594
            return 1
 
595
        return 0
 
596
 
 
597
    def _nextTransactionID(self):
 
598
        """ return a usable transaction ID """
 
599
        self.currentID += 1
 
600
        if self.currentID > 1000:
 
601
            self.currentID = 1
 
602
        return self.currentID
 
603
 
 
604
    def _createIDMapping(self, data=None):
 
605
        """
 
606
        return a unique transaction ID that is mapped internally to a
 
607
        deferred .. also store arbitrary data if it is needed
 
608
        """
 
609
        id = self._nextTransactionID()
 
610
        d = Deferred()
 
611
        self.ids[id] = (d, data)
 
612
        return (id, d)
 
613
 
 
614
    def checkMessage(self, message):
 
615
        """
 
616
        process received messages to check for file invitations and
 
617
        typing notifications and other control type messages
 
618
        """
 
619
        raise NotImplementedError
 
620
 
 
621
    def lineReceived(self, line):
 
622
        if self.currentMessage:
 
623
            self.currentMessage.readPos += len(line+CR+LF)
 
624
            if line == "":
 
625
                self.setRawMode()
 
626
                if self.currentMessage.readPos == self.currentMessage.length:
 
627
                    self.rawDataReceived("") # :(
 
628
                return
 
629
            try:
 
630
                header, value = line.split(':')
 
631
            except ValueError:
 
632
                raise MSNProtocolError, "Invalid Message Header"
 
633
            self.currentMessage.setHeader(header, unquote(value).lstrip())
 
634
            return
 
635
        try:
 
636
            cmd, params = line.split(' ', 1)
 
637
        except ValueError:
 
638
            raise MSNProtocolError, "Invalid Message, %s" % repr(line)
 
639
 
 
640
        if len(cmd) != 3:
 
641
            raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
 
642
        if cmd.isdigit():
 
643
            errorCode = int(cmd)
 
644
            id = int(params.split()[0])
 
645
            if id in self.ids:
 
646
                self.ids[id][0].errback(MSNCommandFailed(errorCode))
 
647
                del self.ids[id]
 
648
                return
 
649
            else:       # we received an error which doesn't map to a sent command
 
650
                self.gotError(errorCode)
 
651
                return
 
652
 
 
653
        handler = getattr(self, "handle_%s" % cmd.upper(), None)
 
654
        if handler:
 
655
            try:
 
656
                handler(params.split())
 
657
            except MSNProtocolError, why:
 
658
                self.gotBadLine(line, why)
 
659
        else:
 
660
            self.handle_UNKNOWN(cmd, params.split())
 
661
 
 
662
    def rawDataReceived(self, data):
 
663
        extra = ""
 
664
        self.currentMessage.readPos += len(data)
 
665
        diff = self.currentMessage.readPos - self.currentMessage.length
 
666
        if diff > 0:
 
667
            self.currentMessage.message += data[:-diff]
 
668
            extra = data[-diff:]
 
669
        elif diff == 0:
 
670
            self.currentMessage.message += data
 
671
        else:
 
672
            self.currentMessage += data
 
673
            return
 
674
        del self.currentMessage.readPos
 
675
        m = self.currentMessage
 
676
        self.currentMessage = None
 
677
        self.setLineMode(extra)
 
678
        if not self.checkMessage(m):
 
679
            return
 
680
        self.gotMessage(m)
 
681
 
 
682
    ### protocol command handlers - no need to override these.
 
683
 
 
684
    def handle_MSG(self, params):
 
685
        checkParamLen(len(params), 3, 'MSG')
 
686
        try:
 
687
            messageLen = int(params[2])
 
688
        except ValueError:
 
689
            raise MSNProtocolError, "Invalid Parameter for MSG length argument"
 
690
        self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
 
691
 
 
692
    def handle_UNKNOWN(self, cmd, params):
 
693
        """ implement me in subclasses if you want to handle unknown events """
 
694
        log.msg("Received unknown command (%s), params: %s" % (cmd, params))
 
695
 
 
696
    ### callbacks
 
697
 
 
698
    def gotMessage(self, message):
 
699
        """
 
700
        called when we receive a message - override in notification
 
701
        and switchboard clients
 
702
        """
 
703
        raise NotImplementedError
 
704
 
 
705
    def gotBadLine(self, line, why):
 
706
        """ called when a handler notifies me that this line is broken """
 
707
        log.msg('Error in line: %s (%s)' % (line, why))
 
708
 
 
709
    def gotError(self, errorCode):
 
710
        """
 
711
        called when the server sends an error which is not in
 
712
        response to a sent command (ie. it has no matching transaction ID)
 
713
        """
 
714
        log.msg('Error %s' % (errorCodes[errorCode]))
 
715
 
 
716
class DispatchClient(MSNEventBase):
 
717
    """
 
718
    This class provides support for clients connecting to the dispatch server
 
719
    @ivar userHandle: your user handle (passport) needed before connecting.
 
720
    """
 
721
 
 
722
    # eventually this may become an attribute of the
 
723
    # factory.
 
724
    userHandle = ""
 
725
 
 
726
    def connectionMade(self):
 
727
        MSNEventBase.connectionMade(self)
 
728
        self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
 
729
 
 
730
    ### protocol command handlers ( there is no need to override these )
 
731
 
 
732
    def handle_VER(self, params):
 
733
        versions = params[1:]
 
734
        if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
 
735
            self.transport.loseConnection()
 
736
            raise MSNProtocolError, "Invalid version response"
 
737
        id = self._nextTransactionID()
 
738
        self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
 
739
 
 
740
    def handle_CVR(self, params):
 
741
        self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
 
742
 
 
743
    def handle_XFR(self, params):
 
744
        if len(params) < 4:
 
745
            raise MSNProtocolError, "Invalid number of parameters for XFR"
 
746
        id, refType, addr = params[:3]
 
747
        # was addr a host:port pair?
 
748
        try:
 
749
            host, port = addr.split(':')
 
750
        except ValueError:
 
751
            host = addr
 
752
            port = MSN_PORT
 
753
        if refType == "NS":
 
754
            self.gotNotificationReferral(host, int(port))
 
755
 
 
756
    ### callbacks
 
757
 
 
758
    def gotNotificationReferral(self, host, port):
 
759
        """
 
760
        called when we get a referral to the notification server.
 
761
 
 
762
        @param host: the notification server's hostname
 
763
        @param port: the port to connect to
 
764
        """
 
765
        pass
 
766
 
 
767
 
 
768
class NotificationClient(MSNEventBase):
 
769
    """
 
770
    This class provides support for clients connecting
 
771
    to the notification server.
 
772
    """
 
773
 
 
774
    factory = None # sssh pychecker
 
775
 
 
776
    def __init__(self, currentID=0):
 
777
        MSNEventBase.__init__(self)
 
778
        self.currentID = currentID
 
779
        self._state = ['DISCONNECTED', {}]
 
780
 
 
781
    def _setState(self, state):
 
782
        self._state[0] = state
 
783
 
 
784
    def _getState(self):
 
785
        return self._state[0]
 
786
 
 
787
    def _getStateData(self, key):
 
788
        return self._state[1][key]
 
789
 
 
790
    def _setStateData(self, key, value):
 
791
        self._state[1][key] = value
 
792
 
 
793
    def _remStateData(self, *args):
 
794
        for key in args:
 
795
            del self._state[1][key]
 
796
 
 
797
    def connectionMade(self):
 
798
        MSNEventBase.connectionMade(self)
 
799
        self._setState('CONNECTED')
 
800
        self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
 
801
 
 
802
    def connectionLost(self, reason):
 
803
        self._setState('DISCONNECTED')
 
804
        self._state[1] = {}
 
805
        MSNEventBase.connectionLost(self, reason)
 
806
 
 
807
    def checkMessage(self, message):
 
808
        """ hook used for detecting specific notification messages """
 
809
        cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
 
810
        if 'text/x-msmsgsprofile' in cTypes:
 
811
            self.gotProfile(message)
 
812
            return 0
 
813
        return 1
 
814
 
 
815
    ### protocol command handlers - no need to override these
 
816
 
 
817
    def handle_VER(self, params):
 
818
        versions = params[1:]
 
819
        if versions is None or ' '.join(versions) != MSN_PROTOCOL_VERSION:
 
820
            self.transport.loseConnection()
 
821
            raise MSNProtocolError, "Invalid version response"
 
822
        self.sendLine("CVR %s %s %s" % (self._nextTransactionID(), MSN_CVR_STR, self.factory.userHandle))
 
823
 
 
824
    def handle_CVR(self, params):
 
825
        self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
 
826
 
 
827
    def handle_USR(self, params):
 
828
        if len(params) != 4 and len(params) != 6:
 
829
            raise MSNProtocolError, "Invalid Number of Parameters for USR"
 
830
 
 
831
        mechanism = params[1]
 
832
        if mechanism == "OK":
 
833
            self.loggedIn(params[2], unquote(params[3]), int(params[4]))
 
834
        elif params[2].upper() == "S":
 
835
            # we need to obtain auth from a passport server
 
836
            f = self.factory
 
837
            d = _login(f.userHandle, f.password, f.passportServer, authData=params[3])
 
838
            d.addCallback(self._passportLogin)
 
839
            d.addErrback(self._passportError)
 
840
 
 
841
    def _passportLogin(self, result):
 
842
        if result[0] == LOGIN_REDIRECT:
 
843
            d = _login(self.factory.userHandle, self.factory.password,
 
844
                       result[1], cached=1, authData=result[2])
 
845
            d.addCallback(self._passportLogin)
 
846
            d.addErrback(self._passportError)
 
847
        elif result[0] == LOGIN_SUCCESS:
 
848
            self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
 
849
        elif result[0] == LOGIN_FAILURE:
 
850
            self.loginFailure(result[1])
 
851
 
 
852
    def _passportError(self, failure):
 
853
        self.loginFailure("Exception while authenticating: %s" % failure)
 
854
 
 
855
    def handle_CHG(self, params):
 
856
        checkParamLen(len(params), 3, 'CHG')
 
857
        id = int(params[0])
 
858
        if not self._fireCallback(id, params[1]):
 
859
            self.statusChanged(params[1])
 
860
 
 
861
    def handle_ILN(self, params):
 
862
        checkParamLen(len(params), 5, 'ILN')
 
863
        self.gotContactStatus(params[1], params[2], unquote(params[3]))
 
864
 
 
865
    def handle_CHL(self, params):
 
866
        checkParamLen(len(params), 2, 'CHL')
 
867
        self.sendLine("QRY %s msmsgs@msnmsgr.com 32" % self._nextTransactionID())
 
868
        self.transport.write(md5.md5(params[1] + MSN_CHALLENGE_STR).hexdigest())
 
869
 
 
870
    def handle_QRY(self, params):
 
871
        pass
 
872
 
 
873
    def handle_NLN(self, params):
 
874
        checkParamLen(len(params), 4, 'NLN')
 
875
        self.contactStatusChanged(params[0], params[1], unquote(params[2]))
 
876
 
 
877
    def handle_FLN(self, params):
 
878
        checkParamLen(len(params), 1, 'FLN')
 
879
        self.contactOffline(params[0])
 
880
 
 
881
    def handle_LST(self, params):
 
882
        # support no longer exists for manually
 
883
        # requesting lists - why do I feel cleaner now?
 
884
        if self._getState() != 'SYNC':
 
885
            return
 
886
        contact = MSNContact(userHandle=params[0], screenName=unquote(params[1]),
 
887
                             lists=int(params[2]))
 
888
        if contact.lists & FORWARD_LIST:
 
889
            contact.groups.extend(map(int, params[3].split(',')))
 
890
        self._getStateData('list').addContact(contact)
 
891
        self._setStateData('last_contact', contact)
 
892
        sofar = self._getStateData('lst_sofar') + 1
 
893
        if sofar == self._getStateData('lst_reply'):
 
894
            # this is the best place to determine that
 
895
            # a syn realy has finished - msn _may_ send
 
896
            # BPR information for the last contact
 
897
            # which is unfortunate because it means
 
898
            # that the real end of a syn is non-deterministic.
 
899
            # to handle this we'll keep 'last_contact' hanging
 
900
            # around in the state data and update it if we need
 
901
            # to later.
 
902
            self._setState('SESSION')
 
903
            contacts = self._getStateData('list')
 
904
            phone = self._getStateData('phone')
 
905
            id = self._getStateData('synid')
 
906
            self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
 
907
            self._fireCallback(id, contacts, phone)
 
908
        else:
 
909
            self._setStateData('lst_sofar',sofar)
 
910
 
 
911
    def handle_BLP(self, params):
 
912
        # check to see if this is in response to a SYN
 
913
        if self._getState() == 'SYNC':
 
914
            self._getStateData('list').privacy = listCodeToID[params[0].lower()]
 
915
        else:
 
916
            id = int(params[0])
 
917
            self._fireCallback(id, int(params[1]), listCodeToID[params[2].lower()])
 
918
 
 
919
    def handle_GTC(self, params):
 
920
        # check to see if this is in response to a SYN
 
921
        if self._getState() == 'SYNC':
 
922
            if params[0].lower() == "a":
 
923
                self._getStateData('list').autoAdd = 0
 
924
            elif params[0].lower() == "n":
 
925
                self._getStateData('list').autoAdd = 1
 
926
            else:
 
927
                raise MSNProtocolError, "Invalid Paramater for GTC" # debug
 
928
        else:
 
929
            id = int(params[0])
 
930
            if params[1].lower() == "a":
 
931
                self._fireCallback(id, 0)
 
932
            elif params[1].lower() == "n":
 
933
                self._fireCallback(id, 1)
 
934
            else:
 
935
                raise MSNProtocolError, "Invalid Paramater for GTC" # debug
 
936
 
 
937
    def handle_SYN(self, params):
 
938
        id = int(params[0])
 
939
        if len(params) == 2:
 
940
            self._setState('SESSION')
 
941
            self._fireCallback(id, None, None)
 
942
        else:
 
943
            contacts = MSNContactList()
 
944
            contacts.version = int(params[1])
 
945
            self._setStateData('list', contacts)
 
946
            self._setStateData('lst_reply', int(params[2]))
 
947
            self._setStateData('lsg_reply', int(params[3]))
 
948
            self._setStateData('lst_sofar', 0)
 
949
            self._setStateData('phone', [])
 
950
 
 
951
    def handle_LSG(self, params):
 
952
        if self._getState() == 'SYNC':
 
953
            self._getStateData('list').groups[int(params[0])] = unquote(params[1])
 
954
 
 
955
        # Please see the comment above the requestListGroups / requestList methods
 
956
        # regarding support for this
 
957
        #
 
958
        #else:
 
959
        #    self._getStateData('groups').append((int(params[4]), unquote(params[5])))
 
960
        #    if params[3] == params[4]: # this was the last group
 
961
        #        self._fireCallback(int(params[0]), self._getStateData('groups'), int(params[1]))
 
962
        #        self._remStateData('groups')
 
963
 
 
964
    def handle_PRP(self, params):
 
965
        if self._getState() == 'SYNC':
 
966
            self._getStateData('phone').append((params[0], unquote(params[1])))
 
967
        else:
 
968
            self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
 
969
 
 
970
    def handle_BPR(self, params):
 
971
        numParams = len(params)
 
972
        if numParams == 2: # part of a syn
 
973
            self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
 
974
        elif numParams == 4:
 
975
            self.gotPhoneNumber(int(params[0]), params[1], params[2], unquote(params[3]))
 
976
 
 
977
    def handle_ADG(self, params):
 
978
        checkParamLen(len(params), 5, 'ADG')
 
979
        id = int(params[0])
 
980
        if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
 
981
            raise MSNProtocolError, "ADG response does not match up to a request" # debug
 
982
 
 
983
    def handle_RMG(self, params):
 
984
        checkParamLen(len(params), 3, 'RMG')
 
985
        id = int(params[0])
 
986
        if not self._fireCallback(id, int(params[1]), int(params[2])):
 
987
            raise MSNProtocolError, "RMG response does not match up to a request" # debug
 
988
 
 
989
    def handle_REG(self, params):
 
990
        checkParamLen(len(params), 5, 'REG')
 
991
        id = int(params[0])
 
992
        if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
 
993
            raise MSNProtocolError, "REG response does not match up to a request" # debug
 
994
 
 
995
    def handle_ADD(self, params):
 
996
        numParams = len(params)
 
997
        if numParams < 5 or params[1].upper() not in ('AL','BL','RL','FL'):
 
998
            raise MSNProtocolError, "Invalid Paramaters for ADD" # debug
 
999
        id = int(params[0])
 
1000
        listType = params[1].lower()
 
1001
        listVer = int(params[2])
 
1002
        userHandle = params[3]
 
1003
        groupID = None
 
1004
        if numParams == 6: # they sent a group id
 
1005
            if params[1].upper() != "FL":
 
1006
                raise MSNProtocolError, "Only forward list can contain groups" # debug
 
1007
            groupID = int(params[5])
 
1008
        if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
 
1009
            self.userAddedMe(userHandle, unquote(params[4]), listVer)
 
1010
 
 
1011
    def handle_REM(self, params):
 
1012
        numParams = len(params)
 
1013
        if numParams < 4 or params[1].upper() not in ('AL','BL','FL','RL'):
 
1014
            raise MSNProtocolError, "Invalid Paramaters for REM" # debug
 
1015
        id = int(params[0])
 
1016
        listType = params[1].lower()
 
1017
        listVer = int(params[2])
 
1018
        userHandle = params[3]
 
1019
        groupID = None
 
1020
        if numParams == 5:
 
1021
            if params[1] != "FL":
 
1022
                raise MSNProtocolError, "Only forward list can contain groups" # debug
 
1023
            groupID = int(params[4])
 
1024
        if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
 
1025
            if listType.upper() == "RL":
 
1026
                self.userRemovedMe(userHandle, listVer)
 
1027
 
 
1028
    def handle_REA(self, params):
 
1029
        checkParamLen(len(params), 4, 'REA')
 
1030
        id = int(params[0])
 
1031
        self._fireCallback(id, int(params[1]), unquote(params[3]))
 
1032
 
 
1033
    def handle_XFR(self, params):
 
1034
        checkParamLen(len(params), 5, 'XFR')
 
1035
        id = int(params[0])
 
1036
        # check to see if they sent a host/port pair
 
1037
        try:
 
1038
            host, port = params[2].split(':')
 
1039
        except ValueError:
 
1040
            host = params[2]
 
1041
            port = MSN_PORT
 
1042
 
 
1043
        if not self._fireCallback(id, host, int(port), params[4]):
 
1044
            raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
 
1045
 
 
1046
    def handle_RNG(self, params):
 
1047
        checkParamLen(len(params), 6, 'RNG')
 
1048
        # check for host:port pair
 
1049
        try:
 
1050
            host, port = params[1].split(":")
 
1051
            port = int(port)
 
1052
        except ValueError:
 
1053
            host = params[1]
 
1054
            port = MSN_PORT
 
1055
        self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
 
1056
                                      unquote(params[5]))
 
1057
 
 
1058
    def handle_OUT(self, params):
 
1059
        checkParamLen(len(params), 1, 'OUT')
 
1060
        if params[0] == "OTH":
 
1061
            self.multipleLogin()
 
1062
        elif params[0] == "SSD":
 
1063
            self.serverGoingDown()
 
1064
        else:
 
1065
            raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
 
1066
 
 
1067
    # callbacks
 
1068
 
 
1069
    def loggedIn(self, userHandle, screenName, verified):
 
1070
        """
 
1071
        Called when the client has logged in.
 
1072
        The default behaviour of this method is to
 
1073
        update the factory with our screenName and
 
1074
        to sync the contact list (factory.contacts).
 
1075
        When this is complete self.listSynchronized
 
1076
        will be called.
 
1077
 
 
1078
        @param userHandle: our userHandle
 
1079
        @param screenName: our screenName
 
1080
        @param verified: 1 if our passport has been (verified), 0 if not.
 
1081
                         (i'm not sure of the significace of this)
 
1082
        @type verified: int
 
1083
        """
 
1084
        self.factory.screenName = screenName
 
1085
        if not self.factory.contacts:
 
1086
            listVersion = 0
 
1087
        else:
 
1088
            listVersion = self.factory.contacts.version
 
1089
        self.syncList(listVersion).addCallback(self.listSynchronized)
 
1090
 
 
1091
    def loginFailure(self, message):
 
1092
        """
 
1093
        Called when the client fails to login.
 
1094
 
 
1095
        @param message: a message indicating the problem that was encountered
 
1096
        """
 
1097
        pass
 
1098
 
 
1099
    def gotProfile(self, message):
 
1100
        """
 
1101
        Called after logging in when the server sends an initial
 
1102
        message with MSN/passport specific profile information
 
1103
        such as country, number of kids, etc.
 
1104
        Check the message headers for the specific values.
 
1105
 
 
1106
        @param message: The profile message
 
1107
        """
 
1108
        pass
 
1109
 
 
1110
    def listSynchronized(self, *args):
 
1111
        """
 
1112
        Lists are now synchronized by default upon logging in, this
 
1113
        method is called after the synchronization has finished
 
1114
        and the factory now has the up-to-date contacts.
 
1115
        """
 
1116
        pass
 
1117
 
 
1118
    def statusChanged(self, statusCode):
 
1119
        """
 
1120
        Called when our status changes and it isn't in response to
 
1121
        a client command. By default we will update the status
 
1122
        attribute of the factory.
 
1123
 
 
1124
        @param statusCode: 3-letter status code
 
1125
        """
 
1126
        self.factory.status = statusCode
 
1127
 
 
1128
    def gotContactStatus(self, statusCode, userHandle, screenName):
 
1129
        """
 
1130
        Called after loggin in when the server sends status of online contacts.
 
1131
        By default we will update the status attribute of the contact stored
 
1132
        on the factory.
 
1133
 
 
1134
        @param statusCode: 3-letter status code
 
1135
        @param userHandle: the contact's user handle (passport)
 
1136
        @param screenName: the contact's screen name
 
1137
        """
 
1138
        self.factory.contacts.getContact(userHandle).status = statusCode
 
1139
 
 
1140
    def contactStatusChanged(self, statusCode, userHandle, screenName):
 
1141
        """
 
1142
        Called when we're notified that a contact's status has changed.
 
1143
        By default we will update the status attribute of the contact
 
1144
        stored on the factory.
 
1145
 
 
1146
        @param statusCode: 3-letter status code
 
1147
        @param userHandle: the contact's user handle (passport)
 
1148
        @param screenName: the contact's screen name
 
1149
        """
 
1150
        self.factory.contacts.getContact(userHandle).status = statusCode
 
1151
 
 
1152
    def contactOffline(self, userHandle):
 
1153
        """
 
1154
        Called when a contact goes offline. By default this method
 
1155
        will update the status attribute of the contact stored
 
1156
        on the factory.
 
1157
 
 
1158
        @param userHandle: the contact's user handle
 
1159
        """
 
1160
        self.factory.contacts.getContact(userHandle).status = STATUS_OFFLINE
 
1161
 
 
1162
    def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
 
1163
        """
 
1164
        Called when the server sends us phone details about
 
1165
        a specific user (for example after a user is added
 
1166
        the server will send their status, phone details etc.
 
1167
        By default we will update the list version for the
 
1168
        factory's contact list and update the phone details
 
1169
        for the specific user.
 
1170
 
 
1171
        @param listVersion: the new list version
 
1172
        @param userHandle: the contact's user handle (passport)
 
1173
        @param phoneType: the specific phoneType
 
1174
                          (*_PHONE constants or HAS_PAGER)
 
1175
        @param number: the value/phone number.
 
1176
        """
 
1177
        self.factory.contacts.version = listVersion
 
1178
        self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
 
1179
 
 
1180
    def userAddedMe(self, userHandle, screenName, listVersion):
 
1181
        """
 
1182
        Called when a user adds me to their list. (ie. they have been added to
 
1183
        the reverse list. By default this method will update the version of
 
1184
        the factory's contact list -- that is, if the contact already exists
 
1185
        it will update the associated lists attribute, otherwise it will create
 
1186
        a new MSNContact object and store it.
 
1187
 
 
1188
        @param userHandle: the userHandle of the user
 
1189
        @param screenName: the screen name of the user
 
1190
        @param listVersion: the new list version
 
1191
        @type listVersion: int
 
1192
        """
 
1193
        self.factory.contacts.version = listVersion
 
1194
        c = self.factory.contacts.getContact(userHandle)
 
1195
        if not c:
 
1196
            c = MSNContact(userHandle=userHandle, screenName=screenName)
 
1197
            self.factory.contacts.addContact(c)
 
1198
        c.addToList(REVERSE_LIST)
 
1199
 
 
1200
    def userRemovedMe(self, userHandle, listVersion):
 
1201
        """
 
1202
        Called when a user removes us from their contact list
 
1203
        (they are no longer on our reverseContacts list.
 
1204
        By default this method will update the version of
 
1205
        the factory's contact list -- that is, the user will
 
1206
        be removed from the reverse list and if they are no longer
 
1207
        part of any lists they will be removed from the contact
 
1208
        list entirely.
 
1209
 
 
1210
        @param userHandle: the contact's user handle (passport)
 
1211
        @param listVersion: the new list version
 
1212
        """
 
1213
        self.factory.contacts.version = listVersion
 
1214
        c = self.factory.contacts.getContact(userHandle)
 
1215
        c.removeFromList(REVERSE_LIST)
 
1216
        if c.lists == 0:
 
1217
            self.factory.contacts.remContact(c.userHandle)
 
1218
 
 
1219
    def gotSwitchboardInvitation(self, sessionID, host, port,
 
1220
                                 key, userHandle, screenName):
 
1221
        """
 
1222
        Called when we get an invitation to a switchboard server.
 
1223
        This happens when a user requests a chat session with us.
 
1224
 
 
1225
        @param sessionID: session ID number, must be remembered for logging in
 
1226
        @param host: the hostname of the switchboard server
 
1227
        @param port: the port to connect to
 
1228
        @param key: used for authorization when connecting
 
1229
        @param userHandle: the user handle of the person who invited us
 
1230
        @param screenName: the screen name of the person who invited us
 
1231
        """
 
1232
        pass
 
1233
 
 
1234
    def multipleLogin(self):
 
1235
        """
 
1236
        Called when the server says there has been another login
 
1237
        under our account, the server should disconnect us right away.
 
1238
        """
 
1239
        pass
 
1240
 
 
1241
    def serverGoingDown(self):
 
1242
        """
 
1243
        Called when the server has notified us that it is going down for
 
1244
        maintenance.
 
1245
        """
 
1246
        pass
 
1247
 
 
1248
    # api calls
 
1249
 
 
1250
    def changeStatus(self, status):
 
1251
        """
 
1252
        Change my current status. This method will add
 
1253
        a default callback to the returned Deferred
 
1254
        which will update the status attribute of the
 
1255
        factory.
 
1256
 
 
1257
        @param status: 3-letter status code (as defined by
 
1258
                       the STATUS_* constants)
 
1259
        @return: A Deferred, the callback of which will be
 
1260
                 fired when the server confirms the change
 
1261
                 of status.  The callback argument will be
 
1262
                 a tuple with the new status code as the
 
1263
                 only element.
 
1264
        """
 
1265
        
 
1266
        id, d = self._createIDMapping()
 
1267
        self.sendLine("CHG %s %s" % (id, status))
 
1268
        def _cb(r):
 
1269
            self.factory.status = r[0]
 
1270
            return r
 
1271
        return d.addCallback(_cb)
 
1272
 
 
1273
    # I am no longer supporting the process of manually requesting
 
1274
    # lists or list groups -- as far as I can see this has no use
 
1275
    # if lists are synchronized and updated correctly, which they
 
1276
    # should be. If someone has a specific justified need for this
 
1277
    # then please contact me and i'll re-enable/fix support for it.
 
1278
 
 
1279
    #def requestList(self, listType):
 
1280
    #    """
 
1281
    #    request the desired list type
 
1282
    #
 
1283
    #    @param listType: (as defined by the *_LIST constants)
 
1284
    #    @return: A Deferred, the callback of which will be
 
1285
    #             fired when the list has been retrieved.
 
1286
    #             The callback argument will be a tuple with
 
1287
    #             the only element being a list of MSNContact
 
1288
    #             objects.
 
1289
    #    """
 
1290
    #    # this doesn't need to ever be used if syncing of the lists takes place
 
1291
    #    # i.e. please don't use it!
 
1292
    #    warnings.warn("Please do not use this method - use the list syncing process instead")
 
1293
    #    id, d = self._createIDMapping()
 
1294
    #    self.sendLine("LST %s %s" % (id, listIDToCode[listType].upper()))
 
1295
    #    self._setStateData('list',[])
 
1296
    #    return d
 
1297
 
 
1298
    def setPrivacyMode(self, privLevel):
 
1299
        """
 
1300
        Set my privacy mode on the server.
 
1301
 
 
1302
        B{Note}:
 
1303
        This only keeps the current privacy setting on
 
1304
        the server for later retrieval, it does not
 
1305
        effect the way the server works at all.
 
1306
 
 
1307
        @param privLevel: This parameter can be true, in which
 
1308
                          case the server will keep the state as
 
1309
                          'al' which the official client interprets
 
1310
                          as -> allow messages from only users on
 
1311
                          the allow list.  Alternatively it can be
 
1312
                          false, in which case the server will keep
 
1313
                          the state as 'bl' which the official client
 
1314
                          interprets as -> allow messages from all
 
1315
                          users except those on the block list.
 
1316
                          
 
1317
        @return: A Deferred, the callback of which will be fired when
 
1318
                 the server replies with the new privacy setting.
 
1319
                 The callback argument will be a tuple, the 2 elements
 
1320
                 of which being the list version and either 'al'
 
1321
                 or 'bl' (the new privacy setting).
 
1322
        """
 
1323
 
 
1324
        id, d = self._createIDMapping()
 
1325
        if privLevel:
 
1326
            self.sendLine("BLP %s AL" % id)
 
1327
        else:
 
1328
            self.sendLine("BLP %s BL" % id)
 
1329
        return d
 
1330
 
 
1331
    def syncList(self, version):
 
1332
        """
 
1333
        Used for keeping an up-to-date contact list.
 
1334
        A callback is added to the returned Deferred
 
1335
        that updates the contact list on the factory
 
1336
        and also sets my state to STATUS_ONLINE.
 
1337
 
 
1338
        B{Note}:
 
1339
        This is called automatically upon signing
 
1340
        in using the version attribute of
 
1341
        factory.contacts, so you may want to persist
 
1342
        this object accordingly. Because of this there
 
1343
        is no real need to ever call this method
 
1344
        directly.
 
1345
 
 
1346
        @param version: The current known list version
 
1347
 
 
1348
        @return: A Deferred, the callback of which will be
 
1349
                 fired when the server sends an adequate reply.
 
1350
                 The callback argument will be a tuple with two
 
1351
                 elements, the new list (MSNContactList) and
 
1352
                 your current state (a dictionary).  If the version
 
1353
                 you sent _was_ the latest list version, both elements
 
1354
                 will be None. To just request the list send a version of 0.
 
1355
        """
 
1356
 
 
1357
        self._setState('SYNC')
 
1358
        id, d = self._createIDMapping(data=str(version))
 
1359
        self._setStateData('synid',id)
 
1360
        self.sendLine("SYN %s %s" % (id, version))
 
1361
        def _cb(r):
 
1362
            self.changeStatus(STATUS_ONLINE)
 
1363
            if r[0] is not None:
 
1364
                self.factory.contacts = r[0]
 
1365
            return r
 
1366
        return d.addCallback(_cb)
 
1367
 
 
1368
 
 
1369
    # I am no longer supporting the process of manually requesting
 
1370
    # lists or list groups -- as far as I can see this has no use
 
1371
    # if lists are synchronized and updated correctly, which they
 
1372
    # should be. If someone has a specific justified need for this
 
1373
    # then please contact me and i'll re-enable/fix support for it.
 
1374
                    
 
1375
    #def requestListGroups(self):
 
1376
    #    """
 
1377
    #    Request (forward) list groups.
 
1378
    #
 
1379
    #    @return: A Deferred, the callback for which will be called
 
1380
    #             when the server responds with the list groups.
 
1381
    #             The callback argument will be a tuple with two elements,
 
1382
    #             a dictionary mapping group IDs to group names and the
 
1383
    #             current list version.
 
1384
    #    """
 
1385
    #    
 
1386
    #    # this doesn't need to be used if syncing of the lists takes place (which it SHOULD!)
 
1387
    #    # i.e. please don't use it!
 
1388
    #    warnings.warn("Please do not use this method - use the list syncing process instead")
 
1389
    #    id, d = self._createIDMapping()
 
1390
    #    self.sendLine("LSG %s" % id)
 
1391
    #    self._setStateData('groups',{})
 
1392
    #    return d
 
1393
 
 
1394
    def setPhoneDetails(self, phoneType, value):
 
1395
        """
 
1396
        Set/change my phone numbers stored on the server.
 
1397
 
 
1398
        @param phoneType: phoneType can be one of the following
 
1399
                          constants - HOME_PHONE, WORK_PHONE,
 
1400
                          MOBILE_PHONE, HAS_PAGER.
 
1401
                          These are pretty self-explanatory, except
 
1402
                          maybe HAS_PAGER which refers to whether or
 
1403
                          not you have a pager.
 
1404
        @param value: for all of the *_PHONE constants the value is a
 
1405
                      phone number (str), for HAS_PAGER accepted values
 
1406
                      are 'Y' (for yes) and 'N' (for no).
 
1407
 
 
1408
        @return: A Deferred, the callback for which will be fired when
 
1409
                 the server confirms the change has been made. The
 
1410
                 callback argument will be a tuple with 2 elements, the
 
1411
                 first being the new list version (int) and the second
 
1412
                 being the new phone number value (str).
 
1413
        """
 
1414
        # XXX: Add a default callback which updates
 
1415
        # factory.contacts.version and the relevant phone
 
1416
        # number
 
1417
        id, d = self._createIDMapping()
 
1418
        self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
 
1419
        return d
 
1420
 
 
1421
    def addListGroup(self, name):
 
1422
        """
 
1423
        Used to create a new list group.
 
1424
        A default callback is added to the
 
1425
        returned Deferred which updates the
 
1426
        contacts attribute of the factory.
 
1427
 
 
1428
        @param name: The desired name of the new group.
 
1429
 
 
1430
        @return: A Deferred, the callbacck for which will be called
 
1431
                 when the server clarifies that the new group has been
 
1432
                 created.  The callback argument will be a tuple with 3
 
1433
                 elements: the new list version (int), the new group name
 
1434
                 (str) and the new group ID (int).
 
1435
        """
 
1436
 
 
1437
        id, d = self._createIDMapping()
 
1438
        self.sendLine("ADG %s %s 0" % (id, quote(name)))
 
1439
        def _cb(r):
 
1440
            self.factory.contacts.version = r[0]
 
1441
            self.factory.contacts.setGroup(r[1], r[2])
 
1442
            return r
 
1443
        return d.addCallback(_cb)
 
1444
 
 
1445
    def remListGroup(self, groupID):
 
1446
        """
 
1447
        Used to remove a list group.
 
1448
        A default callback is added to the
 
1449
        returned Deferred which updates the
 
1450
        contacts attribute of the factory.
 
1451
 
 
1452
        @param groupID: the ID of the desired group to be removed.
 
1453
 
 
1454
        @return: A Deferred, the callback for which will be called when
 
1455
                 the server clarifies the deletion of the group.
 
1456
                 The callback argument will be a tuple with 2 elements:
 
1457
                 the new list version (int) and the group ID (int) of
 
1458
                 the removed group.
 
1459
        """
 
1460
 
 
1461
        id, d = self._createIDMapping()
 
1462
        self.sendLine("RMG %s %s" % (id, groupID))
 
1463
        def _cb(r):
 
1464
            self.factory.contacts.version = r[0]
 
1465
            self.factory.contacts.remGroup(r[1])
 
1466
            return r
 
1467
        return d.addCallback(_cb)
 
1468
 
 
1469
    def renameListGroup(self, groupID, newName):
 
1470
        """
 
1471
        Used to rename an existing list group.
 
1472
        A default callback is added to the returned
 
1473
        Deferred which updates the contacts attribute
 
1474
        of the factory.
 
1475
 
 
1476
        @param groupID: the ID of the desired group to rename.
 
1477
        @param newName: the desired new name for the group.
 
1478
 
 
1479
        @return: A Deferred, the callback for which will be called
 
1480
                 when the server clarifies the renaming.
 
1481
                 The callback argument will be a tuple of 3 elements,
 
1482
                 the new list version (int), the group id (int) and
 
1483
                 the new group name (str).
 
1484
        """
 
1485
        
 
1486
        id, d = self._createIDMapping()
 
1487
        self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
 
1488
        def _cb(r):
 
1489
            self.factory.contacts.version = r[0]
 
1490
            self.factory.contacts.setGroup(r[1], r[2])
 
1491
            return r
 
1492
        return d.addCallback(_cb)
 
1493
 
 
1494
    def addContact(self, listType, userHandle, groupID=0):
 
1495
        """
 
1496
        Used to add a contact to the desired list.
 
1497
        A default callback is added to the returned
 
1498
        Deferred which updates the contacts attribute of
 
1499
        the factory with the new contact information.
 
1500
        If you are adding a contact to the forward list
 
1501
        and you want to associate this contact with multiple
 
1502
        groups then you will need to call this method for each
 
1503
        group you would like to add them to, changing the groupID
 
1504
        parameter. The default callback will take care of updating
 
1505
        the group information on the factory's contact list.
 
1506
 
 
1507
        @param listType: (as defined by the *_LIST constants)
 
1508
        @param userHandle: the user handle (passport) of the contact
 
1509
                           that is being added
 
1510
        @param groupID: the group ID for which to associate this contact
 
1511
                        with. (default 0 - default group). Groups are only
 
1512
                        valid for FORWARD_LIST.
 
1513
 
 
1514
        @return: A Deferred, the callback for which will be called when
 
1515
                 the server has clarified that the user has been added.
 
1516
                 The callback argument will be a tuple with 4 elements:
 
1517
                 the list type, the contact's user handle, the new list
 
1518
                 version, and the group id (if relevant, otherwise it
 
1519
                 will be None)
 
1520
        """
 
1521
        
 
1522
        id, d = self._createIDMapping()
 
1523
        listType = listIDToCode[listType].upper()
 
1524
        if listType == "FL":
 
1525
            self.sendLine("ADD %s FL %s %s %s" % (id, userHandle, userHandle, groupID))
 
1526
        else:
 
1527
            self.sendLine("ADD %s %s %s %s" % (id, listType, userHandle, userHandle))
 
1528
 
 
1529
        def _cb(r):
 
1530
            self.factory.contacts.version = r[2]
 
1531
            c = self.factory.contacts.getContact(r[1])
 
1532
            if not c:
 
1533
                c = MSNContact(userHandle=r[1])
 
1534
            if r[3]:
 
1535
                c.groups.append(r[3])
 
1536
            c.addToList(r[0])
 
1537
            return r
 
1538
        return d.addCallback(_cb)
 
1539
 
 
1540
    def remContact(self, listType, userHandle, groupID=0):
 
1541
        """
 
1542
        Used to remove a contact from the desired list.
 
1543
        A default callback is added to the returned deferred
 
1544
        which updates the contacts attribute of the factory
 
1545
        to reflect the new contact information. If you are
 
1546
        removing from the forward list then you will need to
 
1547
        supply a groupID, if the contact is in more than one
 
1548
        group then they will only be removed from this group
 
1549
        and not the entire forward list, but if this is their
 
1550
        only group they will be removed from the whole list.
 
1551
 
 
1552
        @param listType: (as defined by the *_LIST constants)
 
1553
        @param userHandle: the user handle (passport) of the
 
1554
                           contact being removed
 
1555
        @param groupID: the ID of the group to which this contact
 
1556
                        belongs (only relevant for FORWARD_LIST,
 
1557
                        default is 0)
 
1558
 
 
1559
        @return: A Deferred, the callback for which will be called when
 
1560
                 the server has clarified that the user has been removed.
 
1561
                 The callback argument will be a tuple of 4 elements:
 
1562
                 the list type, the contact's user handle, the new list
 
1563
                 version, and the group id (if relevant, otherwise it will
 
1564
                 be None)
 
1565
        """
 
1566
        
 
1567
        id, d = self._createIDMapping()
 
1568
        listType = listIDToCode[listType].upper()
 
1569
        if listType == "FL":
 
1570
            self.sendLine("REM %s FL %s %s" % (id, userHandle, groupID))
 
1571
        else:
 
1572
            self.sendLine("REM %s %s %s" % (id, listType, userHandle))
 
1573
 
 
1574
        def _cb(r):
 
1575
            l = self.factory.contacts
 
1576
            l.version = r[2]
 
1577
            c = l.getContact(r[1])
 
1578
            group = r[3]
 
1579
            shouldRemove = 1
 
1580
            if group: # they may not have been removed from the list
 
1581
                c.groups.remove(group)
 
1582
                if c.groups:
 
1583
                    shouldRemove = 0
 
1584
            if shouldRemove:
 
1585
                c.removeFromList(r[0])
 
1586
                if c.lists == 0:
 
1587
                    l.remContact(c.userHandle)
 
1588
            return r
 
1589
        return d.addCallback(_cb)
 
1590
 
 
1591
    def changeScreenName(self, newName):
 
1592
        """
 
1593
        Used to change your current screen name.
 
1594
        A default callback is added to the returned
 
1595
        Deferred which updates the screenName attribute
 
1596
        of the factory and also updates the contact list
 
1597
        version.
 
1598
 
 
1599
        @param newName: the new screen name
 
1600
 
 
1601
        @return: A Deferred, the callback for which will be called
 
1602
                 when the server sends an adequate reply.
 
1603
                 The callback argument will be a tuple of 2 elements:
 
1604
                 the new list version and the new screen name.
 
1605
        """
 
1606
 
 
1607
        id, d = self._createIDMapping()
 
1608
        self.sendLine("REA %s %s %s" % (id, self.factory.userHandle, quote(newName)))
 
1609
        def _cb(r):
 
1610
            self.factory.contacts.version = r[0]
 
1611
            self.factory.screenName = r[1]
 
1612
            return r
 
1613
        return d.addCallback(_cb)
 
1614
 
 
1615
    def requestSwitchboardServer(self):
 
1616
        """
 
1617
        Used to request a switchboard server to use for conversations.
 
1618
 
 
1619
        @return: A Deferred, the callback for which will be called when
 
1620
                 the server responds with the switchboard information.
 
1621
                 The callback argument will be a tuple with 3 elements:
 
1622
                 the host of the switchboard server, the port and a key
 
1623
                 used for logging in.
 
1624
        """
 
1625
 
 
1626
        id, d = self._createIDMapping()
 
1627
        self.sendLine("XFR %s SB" % id)
 
1628
        return d
 
1629
 
 
1630
    def logOut(self):
 
1631
        """
 
1632
        Used to log out of the notification server.
 
1633
        After running the method the server is expected
 
1634
        to close the connection.
 
1635
        """
 
1636
        
 
1637
        self.sendLine("OUT")
 
1638
 
 
1639
class NotificationFactory(ClientFactory):
 
1640
    """
 
1641
    Factory for the NotificationClient protocol.
 
1642
    This is basically responsible for keeping
 
1643
    the state of the client and thus should be used
 
1644
    in a 1:1 situation with clients.
 
1645
 
 
1646
    @ivar contacts: An MSNContactList instance reflecting
 
1647
                    the current contact list -- this is
 
1648
                    generally kept up to date by the default
 
1649
                    command handlers.
 
1650
    @ivar userHandle: The client's userHandle, this is expected
 
1651
                      to be set by the client and is used by the
 
1652
                      protocol (for logging in etc).
 
1653
    @ivar screenName: The client's current screen-name -- this is
 
1654
                      generally kept up to date by the default
 
1655
                      command handlers.
 
1656
    @ivar password: The client's password -- this is (obviously)
 
1657
                    expected to be set by the client.
 
1658
    @ivar passportServer: This must point to an msn passport server
 
1659
                          (the whole URL is required)
 
1660
    @ivar status: The status of the client -- this is generally kept
 
1661
                  up to date by the default command handlers
 
1662
    """
 
1663
 
 
1664
    contacts = None
 
1665
    userHandle = ''
 
1666
    screenName = ''
 
1667
    password = ''
 
1668
    passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
 
1669
    status = 'FLN'
 
1670
    protocol = NotificationClient
 
1671
 
 
1672
 
 
1673
# XXX: A lot of the state currently kept in
 
1674
# instances of SwitchboardClient is likely to
 
1675
# be moved into a factory at some stage in the
 
1676
# future
 
1677
 
 
1678
class SwitchboardClient(MSNEventBase):
 
1679
    """
 
1680
    This class provides support for clients connecting to a switchboard server.
 
1681
 
 
1682
    Switchboard servers are used for conversations with other people
 
1683
    on the MSN network. This means that the number of conversations at
 
1684
    any given time will be directly proportional to the number of
 
1685
    connections to varioius switchboard servers.
 
1686
 
 
1687
    MSN makes no distinction between single and group conversations,
 
1688
    so any number of users may be invited to join a specific conversation
 
1689
    taking place on a switchboard server.
 
1690
 
 
1691
    @ivar key: authorization key, obtained when receiving
 
1692
               invitation / requesting switchboard server.
 
1693
    @ivar userHandle: your user handle (passport)
 
1694
    @ivar sessionID: unique session ID, used if you are replying
 
1695
                     to a switchboard invitation
 
1696
    @ivar reply: set this to 1 in connectionMade or before to signifiy
 
1697
                 that you are replying to a switchboard invitation.
 
1698
    """
 
1699
 
 
1700
    key = 0
 
1701
    userHandle = ""
 
1702
    sessionID = ""
 
1703
    reply = 0
 
1704
 
 
1705
    _iCookie = 0
 
1706
 
 
1707
    def __init__(self):
 
1708
        MSNEventBase.__init__(self)
 
1709
        self.pendingUsers = {}
 
1710
        self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
 
1711
 
 
1712
    def connectionMade(self):
 
1713
        MSNEventBase.connectionMade(self)
 
1714
        print 'sending initial stuff'
 
1715
        self._sendInit()
 
1716
 
 
1717
    def connectionLost(self, reason):
 
1718
        self.cookies['iCookies'] = {}
 
1719
        self.cookies['external'] = {}
 
1720
        MSNEventBase.connectionLost(self, reason)
 
1721
 
 
1722
    def _sendInit(self):
 
1723
        """
 
1724
        send initial data based on whether we are replying to an invitation
 
1725
        or starting one.
 
1726
        """
 
1727
        id = self._nextTransactionID()
 
1728
        if not self.reply:
 
1729
            self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
 
1730
        else:
 
1731
            self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
 
1732
 
 
1733
    def _newInvitationCookie(self):
 
1734
        self._iCookie += 1
 
1735
        if self._iCookie > 1000:
 
1736
            self._iCookie = 1
 
1737
        return self._iCookie
 
1738
 
 
1739
    def _checkTyping(self, message, cTypes):
 
1740
        """ helper method for checkMessage """
 
1741
        if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
 
1742
            self.userTyping(message)
 
1743
            return 1
 
1744
 
 
1745
    def _checkFileInvitation(self, message, info):
 
1746
        """ helper method for checkMessage """
 
1747
        guid = info.get('Application-GUID', '').lower()
 
1748
        name = info.get('Application-Name', '').lower()
 
1749
 
 
1750
        # Both fields are required, but we'll let some lazy clients get away
 
1751
        # with only sending a name, if it is easy for us to recognize the
 
1752
        # name (the name is localized, so this check might fail for lazy,
 
1753
        # non-english clients, but I'm not about to include "file transfer"
 
1754
        # in 80 different languages here).
 
1755
 
 
1756
        if name != "file transfer" and guid != classNameToGUID["file transfer"]:
 
1757
            return 0
 
1758
        try:
 
1759
            cookie = int(info['Invitation-Cookie'])
 
1760
            fileName = info['Application-File']
 
1761
            fileSize = int(info['Application-FileSize'])
 
1762
        except KeyError:
 
1763
            log.msg('Received munged file transfer request ... ignoring.')
 
1764
            return 0
 
1765
        self.gotSendRequest(fileName, fileSize, cookie, message)
 
1766
        return 1
 
1767
 
 
1768
    def _checkFileResponse(self, message, info):
 
1769
        """ helper method for checkMessage """
 
1770
        try:
 
1771
            cmd = info['Invitation-Command'].upper()
 
1772
            cookie = int(info['Invitation-Cookie'])
 
1773
        except KeyError:
 
1774
            return 0
 
1775
        accept = (cmd == 'ACCEPT') and 1 or 0
 
1776
        requested = self.cookies['iCookies'].get(cookie)
 
1777
        if not requested:
 
1778
            return 1
 
1779
        requested[0].callback((accept, cookie, info))
 
1780
        del self.cookies['iCookies'][cookie]
 
1781
        return 1
 
1782
 
 
1783
    def _checkFileInfo(self, message, info):
 
1784
        """ helper method for checkMessage """
 
1785
        try:
 
1786
            ip = info['IP-Address']
 
1787
            iCookie = int(info['Invitation-Cookie'])
 
1788
            aCookie = int(info['AuthCookie'])
 
1789
            cmd = info['Invitation-Command'].upper()
 
1790
            port = int(info['Port'])
 
1791
        except KeyError:
 
1792
            return 0
 
1793
        accept = (cmd == 'ACCEPT') and 1 or 0
 
1794
        requested = self.cookies['external'].get(iCookie)
 
1795
        if not requested:
 
1796
            return 1 # we didn't ask for this
 
1797
        requested[0].callback((accept, ip, port, aCookie, info))
 
1798
        del self.cookies['external'][iCookie]
 
1799
        return 1
 
1800
 
 
1801
    def checkMessage(self, message):
 
1802
        """
 
1803
        hook for detecting any notification type messages
 
1804
        (e.g. file transfer)
 
1805
        """
 
1806
        cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
 
1807
        if self._checkTyping(message, cTypes):
 
1808
            return 0
 
1809
        if 'text/x-msmsgsinvite' in cTypes:
 
1810
            # header like info is sent as part of the message body.
 
1811
            info = {}
 
1812
            for line in message.message.split('\r\n'):
 
1813
                try:
 
1814
                    key, val = line.split(':')
 
1815
                    info[key] = val.lstrip()
 
1816
                except ValueError:
 
1817
                    continue
 
1818
            if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info):
 
1819
                return 0
 
1820
        elif 'text/x-clientcaps' in cTypes:
 
1821
            # do something with capabilities
 
1822
            return 0
 
1823
        return 1
 
1824
 
 
1825
    # negotiation
 
1826
    def handle_USR(self, params):
 
1827
        checkParamLen(len(params), 4, 'USR')
 
1828
        if params[1] == "OK":
 
1829
            self.loggedIn()
 
1830
 
 
1831
    # invite a user
 
1832
    def handle_CAL(self, params):
 
1833
        checkParamLen(len(params), 3, 'CAL')
 
1834
        id = int(params[0])
 
1835
        if params[1].upper() == "RINGING":
 
1836
            self._fireCallback(id, int(params[2])) # session ID as parameter
 
1837
 
 
1838
    # user joined
 
1839
    def handle_JOI(self, params):
 
1840
        checkParamLen(len(params), 2, 'JOI')
 
1841
        self.userJoined(params[0], unquote(params[1]))
 
1842
 
 
1843
    # users participating in the current chat
 
1844
    def handle_IRO(self, params):
 
1845
        checkParamLen(len(params), 5, 'IRO')
 
1846
        self.pendingUsers[params[3]] = unquote(params[4])
 
1847
        if params[1] == params[2]:
 
1848
            self.gotChattingUsers(self.pendingUsers)
 
1849
            self.pendingUsers = {}
 
1850
 
 
1851
    # finished listing users
 
1852
    def handle_ANS(self, params):
 
1853
        checkParamLen(len(params), 2, 'ANS')
 
1854
        if params[1] == "OK":
 
1855
            self.loggedIn()
 
1856
 
 
1857
    def handle_ACK(self, params):
 
1858
        checkParamLen(len(params), 1, 'ACK')
 
1859
        self._fireCallback(int(params[0]), None)
 
1860
 
 
1861
    def handle_NAK(self, params):
 
1862
        checkParamLen(len(params), 1, 'NAK')
 
1863
        self._fireCallback(int(params[0]), None)
 
1864
 
 
1865
    def handle_BYE(self, params):
 
1866
        #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
 
1867
        self.userLeft(params[0])
 
1868
 
 
1869
    # callbacks
 
1870
 
 
1871
    def loggedIn(self):
 
1872
        """
 
1873
        called when all login details have been negotiated.
 
1874
        Messages can now be sent, or new users invited.
 
1875
        """
 
1876
        pass
 
1877
 
 
1878
    def gotChattingUsers(self, users):
 
1879
        """
 
1880
        called after connecting to an existing chat session.
 
1881
 
 
1882
        @param users: A dict mapping user handles to screen names
 
1883
                      (current users taking part in the conversation)
 
1884
        """
 
1885
        pass
 
1886
 
 
1887
    def userJoined(self, userHandle, screenName):
 
1888
        """
 
1889
        called when a user has joined the conversation.
 
1890
 
 
1891
        @param userHandle: the user handle (passport) of the user
 
1892
        @param screenName: the screen name of the user
 
1893
        """
 
1894
        pass
 
1895
 
 
1896
    def userLeft(self, userHandle):
 
1897
        """
 
1898
        called when a user has left the conversation.
 
1899
 
 
1900
        @param userHandle: the user handle (passport) of the user.
 
1901
        """
 
1902
        pass
 
1903
 
 
1904
    def gotMessage(self, message):
 
1905
        """
 
1906
        called when we receive a message.
 
1907
 
 
1908
        @param message: the associated MSNMessage object
 
1909
        """
 
1910
        pass
 
1911
 
 
1912
    def userTyping(self, message):
 
1913
        """
 
1914
        called when we receive the special type of message notifying
 
1915
        us that a user is typing a message.
 
1916
 
 
1917
        @param message: the associated MSNMessage object
 
1918
        """
 
1919
        pass
 
1920
 
 
1921
    def gotSendRequest(self, fileName, fileSize, iCookie, message):
 
1922
        """
 
1923
        called when a contact is trying to send us a file.
 
1924
        To accept or reject this transfer see the
 
1925
        fileInvitationReply method.
 
1926
 
 
1927
        @param fileName: the name of the file
 
1928
        @param fileSize: the size of the file
 
1929
        @param iCookie: the invitation cookie, used so the client can
 
1930
                        match up your reply with this request.
 
1931
        @param message: the MSNMessage object which brought about this
 
1932
                        invitation (it may contain more information)
 
1933
        """
 
1934
        pass
 
1935
 
 
1936
    # api calls
 
1937
 
 
1938
    def inviteUser(self, userHandle):
 
1939
        """
 
1940
        used to invite a user to the current switchboard server.
 
1941
 
 
1942
        @param userHandle: the user handle (passport) of the desired user.
 
1943
 
 
1944
        @return: A Deferred, the callback for which will be called
 
1945
                 when the server notifies us that the user has indeed
 
1946
                 been invited.  The callback argument will be a tuple
 
1947
                 with 1 element, the sessionID given to the invited user.
 
1948
                 I'm not sure if this is useful or not.
 
1949
        """
 
1950
 
 
1951
        id, d = self._createIDMapping()
 
1952
        self.sendLine("CAL %s %s" % (id, userHandle))
 
1953
        return d
 
1954
 
 
1955
    def sendMessage(self, message):
 
1956
        """
 
1957
        used to send a message.
 
1958
 
 
1959
        @param message: the corresponding MSNMessage object.
 
1960
 
 
1961
        @return: Depending on the value of message.ack.
 
1962
                 If set to MSNMessage.MESSAGE_ACK or
 
1963
                 MSNMessage.MESSAGE_NACK a Deferred will be returned,
 
1964
                 the callback for which will be fired when an ACK or
 
1965
                 NACK is received - the callback argument will be
 
1966
                 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
 
1967
                 the return value is None.
 
1968
        """
 
1969
 
 
1970
        if message.ack not in ('A','N'):
 
1971
            id, d = self._nextTransactionID(), None
 
1972
        else:
 
1973
            id, d = self._createIDMapping()
 
1974
        if message.length == 0:
 
1975
            message.length = message._calcMessageLen()
 
1976
        self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
 
1977
        # apparently order matters with at least MIME-Version and Content-Type
 
1978
        self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
 
1979
        self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
 
1980
        # send the rest of the headers
 
1981
        for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
 
1982
            self.sendLine("%s: %s" % (header[0], header[1]))
 
1983
        self.transport.write(CR+LF)
 
1984
        self.transport.write(message.message)
 
1985
        return d
 
1986
 
 
1987
    def sendTypingNotification(self):
 
1988
        """
 
1989
        used to send a typing notification. Upon receiving this
 
1990
        message the official client will display a 'user is typing'
 
1991
        message to all other users in the chat session for 10 seconds.
 
1992
        The official client sends one of these every 5 seconds (I think)
 
1993
        as long as you continue to type.
 
1994
        """
 
1995
        m = MSNMessage()
 
1996
        m.ack = m.MESSAGE_ACK_NONE
 
1997
        m.setHeader('Content-Type', 'text/x-msmsgscontrol')
 
1998
        m.setHeader('TypingUser', self.userHandle)
 
1999
        m.message = "\r\n"
 
2000
        self.sendMessage(m)
 
2001
 
 
2002
    def sendFileInvitation(self, fileName, fileSize):
 
2003
        """
 
2004
        send an notification that we want to send a file.
 
2005
 
 
2006
        @param fileName: the file name
 
2007
        @param fileSize: the file size
 
2008
 
 
2009
        @return: A Deferred, the callback of which will be fired
 
2010
                 when the user responds to this invitation with an
 
2011
                 appropriate message. The callback argument will be
 
2012
                 a tuple with 3 elements, the first being 1 or 0
 
2013
                 depending on whether they accepted the transfer
 
2014
                 (1=yes, 0=no), the second being an invitation cookie
 
2015
                 to identify your follow-up responses and the third being
 
2016
                 the message 'info' which is a dict of information they
 
2017
                 sent in their reply (this doesn't really need to be used).
 
2018
                 If you wish to proceed with the transfer see the
 
2019
                 sendTransferInfo method.
 
2020
        """
 
2021
        cookie = self._newInvitationCookie()
 
2022
        d = Deferred()
 
2023
        m = MSNMessage()
 
2024
        m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
 
2025
        m.message += 'Application-Name: File Transfer\r\n'
 
2026
        m.message += 'Application-GUID: %s\r\n' % (classNameToGUID["file transfer"],)
 
2027
        m.message += 'Invitation-Command: INVITE\r\n'
 
2028
        m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
 
2029
        m.message += 'Application-File: %s\r\n' % fileName
 
2030
        m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
 
2031
        m.ack = m.MESSAGE_ACK_NONE
 
2032
        self.sendMessage(m)
 
2033
        self.cookies['iCookies'][cookie] = (d, m)
 
2034
        return d
 
2035
 
 
2036
    def fileInvitationReply(self, iCookie, accept=1):
 
2037
        """
 
2038
        used to reply to a file transfer invitation.
 
2039
 
 
2040
        @param iCookie: the invitation cookie of the initial invitation
 
2041
        @param accept: whether or not you accept this transfer,
 
2042
                       1 = yes, 0 = no, default = 1.
 
2043
 
 
2044
        @return: A Deferred, the callback for which will be fired when
 
2045
                 the user responds with the transfer information.
 
2046
                 The callback argument will be a tuple with 5 elements,
 
2047
                 whether or not they wish to proceed with the transfer
 
2048
                 (1=yes, 0=no), their ip, the port, the authentication
 
2049
                 cookie (see FileReceive/FileSend) and the message
 
2050
                 info (dict) (in case they send extra header-like info
 
2051
                 like Internal-IP, this doesn't necessarily need to be
 
2052
                 used). If you wish to proceed with the transfer see
 
2053
                 FileReceive.
 
2054
        """
 
2055
        d = Deferred()
 
2056
        m = MSNMessage()
 
2057
        m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
 
2058
        m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
 
2059
        m.message += 'Invitation-Cookie: %s\r\n' % str(iCookie)
 
2060
        if not accept:
 
2061
            m.message += 'Cancel-Code: REJECT\r\n'
 
2062
        m.message += 'Launch-Application: FALSE\r\n'
 
2063
        m.message += 'Request-Data: IP-Address:\r\n'
 
2064
        m.message += '\r\n'
 
2065
        m.ack = m.MESSAGE_ACK_NONE
 
2066
        self.sendMessage(m)
 
2067
        self.cookies['external'][iCookie] = (d, m)
 
2068
        return d
 
2069
 
 
2070
    def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
 
2071
        """
 
2072
        send information relating to a file transfer session.
 
2073
 
 
2074
        @param accept: whether or not to go ahead with the transfer
 
2075
                       (1=yes, 0=no)
 
2076
        @param iCookie: the invitation cookie of previous replies
 
2077
                        relating to this transfer
 
2078
        @param authCookie: the authentication cookie obtained from
 
2079
                           an FileSend instance
 
2080
        @param ip: your ip
 
2081
        @param port: the port on which an FileSend protocol is listening.
 
2082
        """
 
2083
        m = MSNMessage()
 
2084
        m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
 
2085
        m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
 
2086
        m.message += 'Invitation-Cookie: %s\r\n' % iCookie
 
2087
        m.message += 'IP-Address: %s\r\n' % ip
 
2088
        m.message += 'Port: %s\r\n' % port
 
2089
        m.message += 'AuthCookie: %s\r\n' % authCookie
 
2090
        m.message += '\r\n'
 
2091
        m.ack = m.MESSAGE_NACK
 
2092
        self.sendMessage(m)
 
2093
 
 
2094
class FileReceive(LineReceiver):
 
2095
    """
 
2096
    This class provides support for receiving files from contacts.
 
2097
 
 
2098
    @ivar fileSize: the size of the receiving file. (you will have to set this)
 
2099
    @ivar connected: true if a connection has been established.
 
2100
    @ivar completed: true if the transfer is complete.
 
2101
    @ivar bytesReceived: number of bytes (of the file) received.
 
2102
                         This does not include header data.
 
2103
    """
 
2104
 
 
2105
    def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
 
2106
        """
 
2107
        @param auth: auth string received in the file invitation.
 
2108
        @param myUserHandle: your userhandle.
 
2109
        @param file: A string or file object represnting the file
 
2110
                     to save data to.
 
2111
        @param directory: optional parameter specifiying the directory.
 
2112
                          Defaults to the current directory.
 
2113
        @param overwrite: if true and a file of the same name exists on
 
2114
                          your system, it will be overwritten. (0 by default)
 
2115
        """
 
2116
        self.auth = auth
 
2117
        self.myUserHandle = myUserHandle
 
2118
        self.fileSize = 0
 
2119
        self.connected = 0
 
2120
        self.completed = 0
 
2121
        self.directory = directory
 
2122
        self.bytesReceived = 0
 
2123
        self.overwrite = overwrite
 
2124
 
 
2125
        # used for handling current received state
 
2126
        self.state = 'CONNECTING'
 
2127
        self.segmentLength = 0
 
2128
        self.buffer = ''
 
2129
        
 
2130
        if isinstance(file, types.StringType):
 
2131
            path = os.path.join(directory, file)
 
2132
            if os.path.exists(path) and not self.overwrite:
 
2133
                log.msg('File already exists...')
 
2134
                raise IOError, "File Exists" # is this all we should do here?
 
2135
            self.file = open(os.path.join(directory, file), 'wb')
 
2136
        else:
 
2137
            self.file = file
 
2138
 
 
2139
    def connectionMade(self):
 
2140
        self.connected = 1
 
2141
        self.state = 'INHEADER'
 
2142
        self.sendLine('VER MSNFTP')
 
2143
 
 
2144
    def connectionLost(self, reason):
 
2145
        self.connected = 0
 
2146
        self.file.close()
 
2147
 
 
2148
    def parseHeader(self, header):
 
2149
        """ parse the header of each 'message' to obtain the segment length """
 
2150
 
 
2151
        if ord(header[0]) != 0: # they requested that we close the connection
 
2152
            self.transport.loseConnection()
 
2153
            return
 
2154
        try:
 
2155
            extra, factor = header[1:]
 
2156
        except ValueError:
 
2157
            # munged header, ending transfer
 
2158
            self.transport.loseConnection()
 
2159
            raise
 
2160
        extra  = ord(extra)
 
2161
        factor = ord(factor)
 
2162
        return factor * 256 + extra
 
2163
 
 
2164
    def lineReceived(self, line):
 
2165
        temp = line.split()
 
2166
        if len(temp) == 1:
 
2167
            params = []
 
2168
        else:
 
2169
            params = temp[1:]
 
2170
        cmd = temp[0]
 
2171
        handler = getattr(self, "handle_%s" % cmd.upper(), None)
 
2172
        if handler:
 
2173
            handler(params) # try/except
 
2174
        else:
 
2175
            self.handle_UNKNOWN(cmd, params)
 
2176
 
 
2177
    def rawDataReceived(self, data):
 
2178
        bufferLen = len(self.buffer)
 
2179
        if self.state == 'INHEADER':
 
2180
            delim = 3-bufferLen
 
2181
            self.buffer += data[:delim]
 
2182
            if len(self.buffer) == 3:
 
2183
                self.segmentLength = self.parseHeader(self.buffer)
 
2184
                if not self.segmentLength:
 
2185
                    return # hrm
 
2186
                self.buffer = ""
 
2187
                self.state = 'INSEGMENT'
 
2188
            extra = data[delim:]
 
2189
            if len(extra) > 0:
 
2190
                self.rawDataReceived(extra)
 
2191
            return
 
2192
 
 
2193
        elif self.state == 'INSEGMENT':
 
2194
            dataSeg = data[:(self.segmentLength-bufferLen)]
 
2195
            self.buffer += dataSeg
 
2196
            self.bytesReceived += len(dataSeg)
 
2197
            if len(self.buffer) == self.segmentLength:
 
2198
                self.gotSegment(self.buffer)
 
2199
                self.buffer = ""
 
2200
                if self.bytesReceived == self.fileSize:
 
2201
                    self.completed = 1
 
2202
                    self.buffer = ""
 
2203
                    self.file.close()
 
2204
                    self.sendLine("BYE 16777989")
 
2205
                    return
 
2206
                self.state = 'INHEADER'
 
2207
                extra = data[(self.segmentLength-bufferLen):]
 
2208
                if len(extra) > 0:
 
2209
                    self.rawDataReceived(extra)
 
2210
                return
 
2211
 
 
2212
    def handle_VER(self, params):
 
2213
        checkParamLen(len(params), 1, 'VER')
 
2214
        if params[0].upper() == "MSNFTP":
 
2215
            self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
 
2216
        else:
 
2217
            log.msg('they sent the wrong version, time to quit this transfer')
 
2218
            self.transport.loseConnection()
 
2219
 
 
2220
    def handle_FIL(self, params):
 
2221
        checkParamLen(len(params), 1, 'FIL')
 
2222
        try:
 
2223
            self.fileSize = int(params[0])
 
2224
        except ValueError: # they sent the wrong file size - probably want to log this
 
2225
            self.transport.loseConnection()
 
2226
            return
 
2227
        self.setRawMode()
 
2228
        self.sendLine("TFR")
 
2229
 
 
2230
    def handle_UNKNOWN(self, cmd, params):
 
2231
        log.msg('received unknown command (%s), params: %s' % (cmd, params))
 
2232
 
 
2233
    def gotSegment(self, data):
 
2234
        """ called when a segment (block) of data arrives. """
 
2235
        self.file.write(data)
 
2236
 
 
2237
class FileSend(LineReceiver):
 
2238
    """
 
2239
    This class provides support for sending files to other contacts.
 
2240
 
 
2241
    @ivar bytesSent: the number of bytes that have currently been sent.
 
2242
    @ivar completed: true if the send has completed.
 
2243
    @ivar connected: true if a connection has been established.
 
2244
    @ivar targetUser: the target user (contact).
 
2245
    @ivar segmentSize: the segment (block) size.
 
2246
    @ivar auth: the auth cookie (number) to use when sending the
 
2247
                transfer invitation
 
2248
    """
 
2249
    
 
2250
    def __init__(self, file):
 
2251
        """
 
2252
        @param file: A string or file object represnting the file to send.
 
2253
        """
 
2254
 
 
2255
        if isinstance(file, types.StringType):
 
2256
            self.file = open(file, 'rb')
 
2257
        else:
 
2258
            self.file = file
 
2259
 
 
2260
        self.fileSize = 0
 
2261
        self.bytesSent = 0
 
2262
        self.completed = 0
 
2263
        self.connected = 0
 
2264
        self.targetUser = None
 
2265
        self.segmentSize = 2045
 
2266
        self.auth = randint(0, 2**30)
 
2267
        self._pendingSend = None # :(
 
2268
 
 
2269
    def connectionMade(self):
 
2270
        self.connected = 1
 
2271
 
 
2272
    def connectionLost(self, reason):
 
2273
        if self._pendingSend.active():
 
2274
            self._pendingSend.cancel()
 
2275
            self._pendingSend = None
 
2276
        if self.bytesSent == self.fileSize:
 
2277
            self.completed = 1
 
2278
        self.connected = 0
 
2279
        self.file.close()
 
2280
 
 
2281
    def lineReceived(self, line):
 
2282
        temp = line.split()
 
2283
        if len(temp) == 1:
 
2284
            params = []
 
2285
        else:
 
2286
            params = temp[1:]
 
2287
        cmd = temp[0]
 
2288
        handler = getattr(self, "handle_%s" % cmd.upper(), None)
 
2289
        if handler:
 
2290
            handler(params)
 
2291
        else:
 
2292
            self.handle_UNKNOWN(cmd, params)
 
2293
 
 
2294
    def handle_VER(self, params):
 
2295
        checkParamLen(len(params), 1, 'VER')
 
2296
        if params[0].upper() == "MSNFTP":
 
2297
            self.sendLine("VER MSNFTP")
 
2298
        else: # they sent some weird version during negotiation, i'm quitting.
 
2299
            self.transport.loseConnection()
 
2300
 
 
2301
    def handle_USR(self, params):
 
2302
        checkParamLen(len(params), 2, 'USR')
 
2303
        self.targetUser = params[0]
 
2304
        if self.auth == int(params[1]):
 
2305
            self.sendLine("FIL %s" % (self.fileSize))
 
2306
        else: # they failed the auth test, disconnecting.
 
2307
            self.transport.loseConnection()
 
2308
 
 
2309
    def handle_TFR(self, params):
 
2310
        checkParamLen(len(params), 0, 'TFR')
 
2311
        # they are ready for me to start sending
 
2312
        self.sendPart()
 
2313
 
 
2314
    def handle_BYE(self, params):
 
2315
        self.completed = (self.bytesSent == self.fileSize)
 
2316
        self.transport.loseConnection()
 
2317
 
 
2318
    def handle_CCL(self, params):
 
2319
        self.completed = (self.bytesSent == self.fileSize)
 
2320
        self.transport.loseConnection()
 
2321
 
 
2322
    def handle_UNKNOWN(self, cmd, params):
 
2323
        log.msg('received unknown command (%s), params: %s' % (cmd, params))
 
2324
 
 
2325
    def makeHeader(self, size):
 
2326
        """ make the appropriate header given a specific segment size. """
 
2327
        quotient, remainder = divmod(size, 256)
 
2328
        return chr(0) + chr(remainder) + chr(quotient)
 
2329
 
 
2330
    def sendPart(self):
 
2331
        """ send a segment of data """
 
2332
        if not self.connected:
 
2333
            self._pendingSend = None
 
2334
            return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
 
2335
        data = self.file.read(self.segmentSize)
 
2336
        if data:
 
2337
            dataSize = len(data)
 
2338
            header = self.makeHeader(dataSize)
 
2339
            self.bytesSent += dataSize
 
2340
            self.transport.write(header + data)
 
2341
            self._pendingSend = reactor.callLater(0, self.sendPart)
 
2342
        else:
 
2343
            self._pendingSend = None
 
2344
            self.completed = 1
 
2345
 
 
2346
# mapping of error codes to error messages
 
2347
errorCodes = {
 
2348
 
 
2349
    200 : "Syntax error",
 
2350
    201 : "Invalid parameter",
 
2351
    205 : "Invalid user",
 
2352
    206 : "Domain name missing",
 
2353
    207 : "Already logged in",
 
2354
    208 : "Invalid username",
 
2355
    209 : "Invalid screen name",
 
2356
    210 : "User list full",
 
2357
    215 : "User already there",
 
2358
    216 : "User already on list",
 
2359
    217 : "User not online",
 
2360
    218 : "Already in mode",
 
2361
    219 : "User is in the opposite list",
 
2362
    223 : "Too many groups",
 
2363
    224 : "Invalid group",
 
2364
    225 : "User not in group",
 
2365
    229 : "Group name too long",
 
2366
    230 : "Cannot remove group 0",
 
2367
    231 : "Invalid group",
 
2368
    280 : "Switchboard failed",
 
2369
    281 : "Transfer to switchboard failed",
 
2370
 
 
2371
    300 : "Required field missing",
 
2372
    301 : "Too many FND responses",
 
2373
    302 : "Not logged in",
 
2374
 
 
2375
    500 : "Internal server error",
 
2376
    501 : "Database server error",
 
2377
    502 : "Command disabled",
 
2378
    510 : "File operation failed",
 
2379
    520 : "Memory allocation failed",
 
2380
    540 : "Wrong CHL value sent to server",
 
2381
 
 
2382
    600 : "Server is busy",
 
2383
    601 : "Server is unavaliable",
 
2384
    602 : "Peer nameserver is down",
 
2385
    603 : "Database connection failed",
 
2386
    604 : "Server is going down",
 
2387
    605 : "Server unavailable",
 
2388
 
 
2389
    707 : "Could not create connection",
 
2390
    710 : "Invalid CVR parameters",
 
2391
    711 : "Write is blocking",
 
2392
    712 : "Session is overloaded",
 
2393
    713 : "Too many active users",
 
2394
    714 : "Too many sessions",
 
2395
    715 : "Not expected",
 
2396
    717 : "Bad friend file",
 
2397
    731 : "Not expected",
 
2398
 
 
2399
    800 : "Requests too rapid",
 
2400
 
 
2401
    910 : "Server too busy",
 
2402
    911 : "Authentication failed",
 
2403
    912 : "Server too busy",
 
2404
    913 : "Not allowed when offline",
 
2405
    914 : "Server too busy",
 
2406
    915 : "Server too busy",
 
2407
    916 : "Server too busy",
 
2408
    917 : "Server too busy",
 
2409
    918 : "Server too busy",
 
2410
    919 : "Server too busy",
 
2411
    920 : "Not accepting new users",
 
2412
    921 : "Server too busy",
 
2413
    922 : "Server too busy",
 
2414
    923 : "No parent consent",
 
2415
    924 : "Passport account not yet verified"
 
2416
 
 
2417
}
 
2418
 
 
2419
# mapping of status codes to readable status format
 
2420
statusCodes = {
 
2421
 
 
2422
    STATUS_ONLINE  : "Online",
 
2423
    STATUS_OFFLINE : "Offline",
 
2424
    STATUS_HIDDEN  : "Appear Offline",
 
2425
    STATUS_IDLE    : "Idle",
 
2426
    STATUS_AWAY    : "Away",
 
2427
    STATUS_BUSY    : "Busy",
 
2428
    STATUS_BRB     : "Be Right Back",
 
2429
    STATUS_PHONE   : "On the Phone",
 
2430
    STATUS_LUNCH   : "Out to Lunch"
 
2431
 
 
2432
}
 
2433
 
 
2434
# mapping of list ids to list codes
 
2435
listIDToCode = {
 
2436
 
 
2437
    FORWARD_LIST : 'fl',
 
2438
    BLOCK_LIST   : 'bl',
 
2439
    ALLOW_LIST   : 'al',
 
2440
    REVERSE_LIST : 'rl'
 
2441
 
 
2442
}
 
2443
 
 
2444
# mapping of list codes to list ids
 
2445
listCodeToID = {}
 
2446
for id,code in listIDToCode.items():
 
2447
    listCodeToID[code] = id
 
2448
 
 
2449
del id, code
 
2450
 
 
2451
# Mapping of class GUIDs to simple english names
 
2452
guidToClassName = {
 
2453
    "{5D3E02AB-6190-11d3-BBBB-00C04F795683}": "file transfer",
 
2454
    }
 
2455
 
 
2456
# Reverse of the above
 
2457
classNameToGUID = {}
 
2458
for guid, name in guidToClassName.iteritems():
 
2459
    classNameToGUID[name] = guid