root / trunk / twisted / words / protocols / msn.py

Revision 25457, 86.3 kB (checked in by exarkun, 8 months ago)

Merge hashlib-2763-3

Author: wsanchez, exarkun
Reviewer: exarkun, mwhudson
Fixes: #2763

Replace uses of md5 and sha modules in Twisted with use of a new twisted.python.hashlib
module which transparently uses the new hashlib standard library module if it is available
or falls back to md5 and sha if not.

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