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