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

« back to all changes in this revision

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

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

Show diffs side-by-side

added added

removed removed

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