Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / words / protocols / msn.py
1 # -*- test-case-name: twisted.words.test -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 MSNP8 Protocol (client only) - semi-experimental
7
8 This module provides support for clients using the MSN Protocol (MSNP8).
9 There are basically 3 servers involved in any MSN session:
10
11 I{Dispatch server}
12
13 The DispatchClient class handles connections to the
14 dispatch server, which basically delegates users to a
15 suitable notification server.
16
17 You will want to subclass this and handle the gotNotificationReferral
18 method appropriately.
19
20 I{Notification Server}
21
22 The NotificationClient class handles connections to the
23 notification server, which acts as a session server
24 (state updates, message negotiation etc...)
25
26 I{Switcboard Server}
27
28 The SwitchboardClient handles connections to switchboard
29 servers which are used to conduct conversations with other users.
30
31 There are also two classes (FileSend and FileReceive) used
32 for file transfers.
33
34 Clients handle events in two ways.
35
36   - each client request requiring a response will return a Deferred,
37     the callback for same will be fired when the server sends the
38     required response
39   - Events which are not in response to any client request have
40     respective methods which should be overridden and handled in
41     an adequate manner
42
43 Most client request callbacks require more than one argument,
44 and since Deferreds can only pass the callback one result,
45 most of the time the callback argument will be a tuple of
46 values (documented in the respective request method).
47 To make reading/writing code easier, callbacks can be defined in
48 a number of ways to handle this 'cleanly'. One way would be to
49 define methods like: def callBack(self, (arg1, arg2, arg)): ...
50 another way would be to do something like:
51 d.addCallback(lambda result: myCallback(*result)).
52
53 If the server sends an error response to a client request,
54 the errback of the corresponding Deferred will be called,
55 the argument being the corresponding error code.
56
57 B{NOTE}:
58 Due to the lack of an official spec for MSNP8, extra checking
59 than may be deemed necessary often takes place considering the
60 server is never 'wrong'. Thus, if gotBadLine (in any of the 3
61 main clients) is called, or an MSNProtocolError is raised, it's
62 probably a good idea to submit a bug report. ;)
63 Use of this module requires that PyOpenSSL is installed.
64
65 TODO
66 ====
67 - check message hooks with invalid x-msgsinvite messages.
68 - font handling
69 - switchboard factory
70
71 @author: Sam Jordan
72 """
73
74 import types, operator, os
75 from random import randint
76 from urllib import quote, unquote
77
78 from twisted.python import failure, log
79 from twisted.python.hashlib import md5
80 from twisted.internet import reactor
81 from twisted.internet.defer import Deferred, execute
82 from twisted.internet.protocol import ClientFactory
83 try:
84     from twisted.internet.ssl import ClientContextFactory
85 except ImportError:
86     ClientContextFactory = None
87 from twisted.protocols.basic import LineReceiver
88 from twisted.web.http import HTTPClient
89
90
91 MSN_PROTOCOL_VERSION = "MSNP8 CVR0"       # protocol version
92 MSN_PORT             = 1863               # default dispatch server port
93 MSN_MAX_MESSAGE      = 1664               # max message length
94 MSN_CHALLENGE_STR    = "Q1P7W2E4J9R8U3S5" # used for server challenges
95 MSN_CVR_STR          = "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(
96
97 # auth constants
98 LOGIN_SUCCESS  = 1
99 LOGIN_FAILURE  = 2
100 LOGIN_REDIRECT = 3
101
102 # list constants
103 FORWARD_LIST = 1
104 ALLOW_LIST   = 2
105 BLOCK_LIST   = 4
106 REVERSE_LIST = 8
107
108 # phone constants
109 HOME_PHONE   = "PHH"
110 WORK_PHONE   = "PHW"
111 MOBILE_PHONE = "PHM"
112 HAS_PAGER    = "MOB"
113
114 # status constants
115 STATUS_ONLINE  = 'NLN'
116 STATUS_OFFLINE = 'FLN'
117 STATUS_HIDDEN  = 'HDN'
118 STATUS_IDLE    = 'IDL'
119 STATUS_AWAY    = 'AWY'
120 STATUS_BUSY    = 'BSY'
121 STATUS_BRB     = 'BRB'
122 STATUS_PHONE   = 'PHN'
123 STATUS_LUNCH   = 'LUN'
124
125 CR = "\r"
126 LF = "\n"
127
128
129 class 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
139 def 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
145 def _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
168 def _parsePrimitiveHost(host):
169     # Ho Ho Ho
170     h,p = host.replace('https://','').split('/',1)
171     p = '/' + p
172     return h,p
173
174
175 def _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
205 class 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 not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
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
242 class 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 self.headers.has_key(_interHeader):
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
306 class 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
318 class 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
331 class 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 self.headers.has_key(header)
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
399 class 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
470 class 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
579 class 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 self.ids.has_key(id):
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
732 class 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
780 class 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
1659 class 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
1698 class 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
2114 class 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
2257 class 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
2367 errorCodes = {
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
2440 statusCodes = {
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
2455 listIDToCode = {
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
2465 listCodeToID = {}
2466 for id,code in listIDToCode.items():
2467     listCodeToID[code] = id
2468
2469 del id, code
2470
2471 # Mapping of class GUIDs to simple english names
2472 guidToClassName = {
2473     "{5D3E02AB-6190-11d3-BBBB-00C04F795683}": "file transfer",
2474     }
2475
2476 # Reverse of the above
2477 classNameToGUID = {}
2478 for guid, name in guidToClassName.iteritems():
2479     classNameToGUID[name] = guid