1 # -*- test-case-name: twisted.words.test -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 MSNP8 Protocol (client only) - semi-experimental
8 This module provides support for clients using the MSN Protocol (MSNP8).
9 There are basically 3 servers involved in any MSN session:
13 The DispatchClient class handles connections to the
14 dispatch server, which basically delegates users to a
15 suitable notification server.
17 You will want to subclass this and handle the gotNotificationReferral
20 I{Notification Server}
22 The NotificationClient class handles connections to the
23 notification server, which acts as a session server
24 (state updates, message negotiation etc...)
28 The SwitchboardClient handles connections to switchboard
29 servers which are used to conduct conversations with other users.
31 There are also two classes (FileSend and FileReceive) used
34 Clients handle events in two ways.
36 - each client request requiring a response will return a Deferred,
37 the callback for same will be fired when the server sends the
39 - Events which are not in response to any client request have
40 respective methods which should be overridden and handled in
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)).
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.
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.
67 - check message hooks with invalid x-msgsinvite messages.
74 import types, operator, os
75 from random import randint
76 from urllib import quote, unquote
78 from twisted.python import failure, log
79 from twisted.python.hashlib import md5
80 from twisted.internet import reactor
81 from twisted.internet.defer import Deferred, execute
82 from twisted.internet.protocol import ClientFactory
84 from twisted.internet.ssl import ClientContextFactory
86 ClientContextFactory = None
87 from twisted.protocols.basic import LineReceiver
88 from twisted.web.http import HTTPClient
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" # :(
115 STATUS_ONLINE = 'NLN'
116 STATUS_OFFLINE = 'FLN'
117 STATUS_HIDDEN = 'HDN'
129 class SSLRequired(Exception):
131 This exception is raised when it is necessary to talk to a passport server
132 using SSL, but the necessary SSL dependencies are unavailable.
139 def checkParamLen(num, expected, cmd, error=None):
141 error = "Invalid Number of Parameters for %s" % cmd
143 raise MSNProtocolError, error
145 def _parseHeader(h, v):
147 Split a certin number of known
148 header values with the format:
149 field1=val,field2=val,field3=val into
150 a dict mapping fields to values.
151 @param h: the header's key
152 @param v: the header's value as a string
155 if h in ('passporturls','authentication-info','www-authenticate'):
156 v = v.replace('Passport1.4','').lstrip()
158 for fieldPair in v.split(','):
160 field,value = fieldPair.split('=',1)
161 fields[field.lower()] = value
163 fields[field.lower()] = ''
168 def _parsePrimitiveHost(host):
170 h,p = host.replace('https://','').split('/',1)
175 def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
177 This function is used internally and should not ever be called
180 @raise SSLRequired: If there is no SSL support available.
182 if ClientContextFactory is None:
184 'Connecting to the Passport server requires SSL, but SSL is '
188 def _cb(server, auth):
189 loginFac = ClientFactory()
190 loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
191 reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
194 _cb(nexusServer, authData)
196 fac = ClientFactory()
198 d.addCallbacks(_cb, callbackArgs=(authData,))
199 d.addErrback(lambda f: cb.errback(f))
200 fac.protocol = lambda : PassportNexus(d, nexusServer)
201 reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
205 class PassportNexus(HTTPClient):
208 Used to obtain the URL of a valid passport
211 This class is used internally and should
212 not be instantiated directly -- that is,
213 The passport logging in process is handled
214 transparantly by NotificationClient.
217 def __init__(self, deferred, host):
218 self.deferred = deferred
219 self.host, self.path = _parsePrimitiveHost(host)
221 def connectionMade(self):
222 HTTPClient.connectionMade(self)
223 self.sendCommand('GET', self.path)
224 self.sendHeader('Host', self.host)
228 def handleHeader(self, header, value):
230 self.headers[h] = _parseHeader(h, value)
232 def handleEndHeaders(self):
234 self.transport.loseConnection()
235 if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
236 self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
237 self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
239 def handleResponse(self, r):
242 class PassportLogin(HTTPClient):
244 This class is used internally to obtain
245 a login ticket from a passport HTTPS
246 server -- it should not be used directly.
251 def __init__(self, deferred, userHandle, passwd, host, authData):
252 self.deferred = deferred
253 self.userHandle = userHandle
255 self.authData = authData
256 self.host, self.path = _parsePrimitiveHost(host)
258 def connectionMade(self):
259 self.sendCommand('GET', self.path)
260 self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
261 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
262 self.sendHeader('Host', self.host)
266 def handleHeader(self, header, value):
268 self.headers[h] = _parseHeader(h, value)
270 def handleEndHeaders(self):
273 self._finished = 1 # I think we need this because of HTTPClient
275 self.transport.loseConnection()
276 authHeader = 'authentication-info'
277 _interHeader = 'www-authenticate'
278 if self.headers.has_key(_interHeader):
279 authHeader = _interHeader
281 info = self.headers[authHeader]
282 status = info['da-status']
283 handler = getattr(self, 'login_%s' % (status,), None)
289 self.deferred.errback(failure.Failure(e))
291 def handleResponse(self, r):
294 def login_success(self, info):
295 ticket = info['from-pp']
296 ticket = ticket[1:len(ticket)-1]
297 self.deferred.callback((LOGIN_SUCCESS, ticket))
299 def login_failed(self, info):
300 self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
302 def login_redir(self, info):
303 self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
306 class MSNProtocolError(Exception):
308 This Exception is basically used for debugging
309 purposes, as the official MSN server should never
310 send anything _wrong_ and nobody in their right
311 mind would run their B{own} MSN server.
312 If it is raised by default command handlers
313 (handle_BLAH) the error will be logged.
318 class MSNCommandFailed(Exception):
320 The server said that the command failed.
323 def __init__(self, errorCode):
324 self.errorCode = errorCode
327 return ("Command failed: %s (error code %d)"
328 % (errorCodes[self.errorCode], self.errorCode))
333 I am the class used to represent an 'instant' message.
335 @ivar userHandle: The user handle (passport) of the sender
336 (this is only used when receiving a message)
337 @ivar screenName: The screen name of the sender (this is only used
338 when receiving a message)
339 @ivar message: The message
340 @ivar headers: The message headers
342 @ivar length: The message length (including headers and line endings)
343 @ivar ack: This variable is used to tell the server how to respond
344 once the message has been sent. If set to MESSAGE_ACK
345 (default) the server will respond with an ACK upon receiving
346 the message, if set to MESSAGE_NACK the server will respond
347 with a NACK upon failure to receive the message.
348 If set to MESSAGE_ACK_NONE the server will do nothing.
349 This is relevant for the return value of
350 SwitchboardClient.sendMessage (which will return
351 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
352 and will fire when the respective ACK or NACK is received).
353 If set to MESSAGE_ACK_NONE sendMessage will return None.
357 MESSAGE_ACK_NONE = 'U'
361 def __init__(self, length=0, userHandle="", screenName="", message=""):
362 self.userHandle = userHandle
363 self.screenName = screenName
364 self.message = message
365 self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
369 def _calcMessageLen(self):
371 used to calculte the number to send
372 as the message length when sending a message.
374 return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
376 def setHeader(self, header, value):
377 """ set the desired header """
378 self.headers[header] = value
380 def getHeader(self, header):
382 get the desired header value
383 @raise KeyError: if no such header exists.
385 return self.headers[header]
387 def hasHeader(self, header):
388 """ check to see if the desired header exists """
389 return self.headers.has_key(header)
391 def getMessage(self):
392 """ return the message - not including headers """
395 def setMessage(self, message):
396 """ set the message text """
397 self.message = message
402 This class represents a contact (user).
404 @ivar userHandle: The contact's user handle (passport).
405 @ivar screenName: The contact's screen name.
406 @ivar groups: A list of all the group IDs which this
408 @ivar lists: An integer representing the sum of all lists
409 that this contact belongs to.
410 @ivar status: The contact's status code.
411 @type status: str if contact's status is known, None otherwise.
413 @ivar homePhone: The contact's home phone number.
414 @type homePhone: str if known, otherwise None.
415 @ivar workPhone: The contact's work phone number.
416 @type workPhone: str if known, otherwise None.
417 @ivar mobilePhone: The contact's mobile phone number.
418 @type mobilePhone: str if known, otherwise None.
419 @ivar hasPager: Whether or not this user has a mobile pager
423 def __init__(self, userHandle="", screenName="", lists=0, groups=[], status=None):
424 self.userHandle = userHandle
425 self.screenName = screenName
427 self.groups = [] # if applicable
428 self.status = status # current status
431 self.homePhone = None
432 self.workPhone = None
433 self.mobilePhone = None
436 def setPhone(self, phoneType, value):
438 set phone numbers/values for this specific user.
439 for phoneType check the *_PHONE constants and HAS_PAGER
442 t = phoneType.upper()
444 self.homePhone = value
445 elif t == WORK_PHONE:
446 self.workPhone = value
447 elif t == MOBILE_PHONE:
448 self.mobilePhone = value
450 self.hasPager = value
452 raise ValueError, "Invalid Phone Type"
454 def addToList(self, listType):
456 Update the lists attribute to
457 reflect being part of the
460 self.lists |= listType
462 def removeFromList(self, listType):
464 Update the lists attribute to
465 reflect being removed from the
468 self.lists ^= listType
470 class MSNContactList:
472 This class represents a basic MSN contact list.
474 @ivar contacts: All contacts on my various lists
475 @type contacts: dict (mapping user handles to MSNContact objects)
476 @ivar version: The current contact list version (used for list syncing)
477 @ivar groups: a mapping of group ids to group names
478 (groups can only exist on the forward list)
482 This is used only for storage and doesn't effect the
483 server's contact list.
493 def _getContactsFromList(self, listType):
495 Obtain all contacts which belong
496 to the given list type.
498 return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
500 def addContact(self, contact):
504 self.contacts[contact.userHandle] = contact
506 def remContact(self, userHandle):
511 del self.contacts[userHandle]
515 def getContact(self, userHandle):
517 Obtain the MSNContact object
518 associated with the given
520 @return: the MSNContact object if
521 the user exists, or None.
524 return self.contacts[userHandle]
528 def getBlockedContacts(self):
530 Obtain all the contacts on my block list
532 return self._getContactsFromList(BLOCK_LIST)
534 def getAuthorizedContacts(self):
536 Obtain all the contacts on my auth list.
537 (These are contacts which I have verified
538 can view my state changes).
540 return self._getContactsFromList(ALLOW_LIST)
542 def getReverseContacts(self):
544 Get all contacts on my reverse list.
545 (These are contacts which have added me
546 to their forward list).
548 return self._getContactsFromList(REVERSE_LIST)
550 def getContacts(self):
552 Get all contacts on my forward list.
553 (These are the contacts which I have added
556 return self._getContactsFromList(FORWARD_LIST)
558 def setGroup(self, id, name):
560 Keep a mapping from the given id
563 self.groups[id] = name
565 def remGroup(self, id):
567 Removed the stored group
568 mapping for the given id.
574 for c in self.contacts:
579 class MSNEventBase(LineReceiver):
581 This class provides support for handling / dispatching events and is the
582 base class of the three main client protocols (DispatchClient,
583 NotificationClient, SwitchboardClient)
587 self.ids = {} # mapping of ids to Deferreds
591 self.currentMessage = None
593 def connectionLost(self, reason):
597 def connectionMade(self):
600 def _fireCallback(self, id, *args):
602 Fire the callback for the given id
603 if one exists and return 1, else return false
605 if self.ids.has_key(id):
606 self.ids[id][0].callback(args)
611 def _nextTransactionID(self):
612 """ return a usable transaction ID """
614 if self.currentID > 1000:
616 return self.currentID
618 def _createIDMapping(self, data=None):
620 return a unique transaction ID that is mapped internally to a
621 deferred .. also store arbitrary data if it is needed
623 id = self._nextTransactionID()
625 self.ids[id] = (d, data)
628 def checkMessage(self, message):
630 process received messages to check for file invitations and
631 typing notifications and other control type messages
633 raise NotImplementedError
635 def lineReceived(self, line):
636 if self.currentMessage:
637 self.currentMessage.readPos += len(line+CR+LF)
640 if self.currentMessage.readPos == self.currentMessage.length:
641 self.rawDataReceived("") # :(
644 header, value = line.split(':')
646 raise MSNProtocolError, "Invalid Message Header"
647 self.currentMessage.setHeader(header, unquote(value).lstrip())
650 cmd, params = line.split(' ', 1)
652 raise MSNProtocolError, "Invalid Message, %s" % repr(line)
655 raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
658 id = int(params.split()[0])
660 self.ids[id][0].errback(MSNCommandFailed(errorCode))
663 else: # we received an error which doesn't map to a sent command
664 self.gotError(errorCode)
667 handler = getattr(self, "handle_%s" % cmd.upper(), None)
670 handler(params.split())
671 except MSNProtocolError, why:
672 self.gotBadLine(line, why)
674 self.handle_UNKNOWN(cmd, params.split())
676 def rawDataReceived(self, data):
678 self.currentMessage.readPos += len(data)
679 diff = self.currentMessage.readPos - self.currentMessage.length
681 self.currentMessage.message += data[:-diff]
684 self.currentMessage.message += data
686 self.currentMessage += data
688 del self.currentMessage.readPos
689 m = self.currentMessage
690 self.currentMessage = None
691 self.setLineMode(extra)
692 if not self.checkMessage(m):
696 ### protocol command handlers - no need to override these.
698 def handle_MSG(self, params):
699 checkParamLen(len(params), 3, 'MSG')
701 messageLen = int(params[2])
703 raise MSNProtocolError, "Invalid Parameter for MSG length argument"
704 self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName=unquote(params[1]))
706 def handle_UNKNOWN(self, cmd, params):
707 """ implement me in subclasses if you want to handle unknown events """
708 log.msg("Received unknown command (%s), params: %s" % (cmd, params))
712 def gotMessage(self, message):
714 called when we receive a message - override in notification
715 and switchboard clients
717 raise NotImplementedError
719 def gotBadLine(self, line, why):
720 """ called when a handler notifies me that this line is broken """
721 log.msg('Error in line: %s (%s)' % (line, why))
723 def gotError(self, errorCode):
725 called when the server sends an error which is not in
726 response to a sent command (ie. it has no matching transaction ID)
728 log.msg('Error %s' % (errorCodes[errorCode]))
732 class DispatchClient(MSNEventBase):
734 This class provides support for clients connecting to the dispatch server
735 @ivar userHandle: your user handle (passport) needed before connecting.
738 # eventually this may become an attribute of the
742 def connectionMade(self):
743 MSNEventBase.connectionMade(self)
744 self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
746 ### protocol command handlers ( there is no need to override these )
748 def handle_VER(self, params):
749 id = self._nextTransactionID()
750 self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
752 def handle_CVR(self, params):
753 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
755 def handle_XFR(self, params):
757 raise MSNProtocolError, "Invalid number of parameters for XFR"
758 id, refType, addr = params[:3]
759 # was addr a host:port pair?
761 host, port = addr.split(':')
766 self.gotNotificationReferral(host, int(port))
770 def gotNotificationReferral(self, host, port):
772 called when we get a referral to the notification server.
774 @param host: the notification server's hostname
775 @param port: the port to connect to
780 class NotificationClient(MSNEventBase):
782 This class provides support for clients connecting
783 to the notification server.
786 factory = None # sssh pychecker
788 def __init__(self, currentID=0):
789 MSNEventBase.__init__(self)
790 self.currentID = currentID
791 self._state = ['DISCONNECTED', {}]
793 def _setState(self, state):
794 self._state[0] = state
797 return self._state[0]
799 def _getStateData(self, key):
800 return self._state[1][key]
802 def _setStateData(self, key, value):
803 self._state[1][key] = value
805 def _remStateData(self, *args):
807 del self._state[1][key]
809 def connectionMade(self):
810 MSNEventBase.connectionMade(self)
811 self._setState('CONNECTED')
812 self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
814 def connectionLost(self, reason):
815 self._setState('DISCONNECTED')
817 MSNEventBase.connectionLost(self, reason)
819 def checkMessage(self, message):
820 """ hook used for detecting specific notification messages """
821 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
822 if 'text/x-msmsgsprofile' in cTypes:
823 self.gotProfile(message)
827 ### protocol command handlers - no need to override these
829 def handle_VER(self, params):
830 id = self._nextTransactionID()
831 self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.factory.userHandle))
833 def handle_CVR(self, params):
834 self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
836 def handle_USR(self, params):
837 if len(params) != 4 and len(params) != 6:
838 raise MSNProtocolError, "Invalid Number of Parameters for USR"
840 mechanism = params[1]
841 if mechanism == "OK":
842 self.loggedIn(params[2], unquote(params[3]), int(params[4]))
843 elif params[2].upper() == "S":
844 # we need to obtain auth from a passport server
847 _login, f.userHandle, f.password, f.passportServer,
849 d.addCallback(self._passportLogin)
850 d.addErrback(self._passportError)
852 def _passportLogin(self, result):
853 if result[0] == LOGIN_REDIRECT:
854 d = _login(self.factory.userHandle, self.factory.password,
855 result[1], cached=1, authData=result[2])
856 d.addCallback(self._passportLogin)
857 d.addErrback(self._passportError)
858 elif result[0] == LOGIN_SUCCESS:
859 self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
860 elif result[0] == LOGIN_FAILURE:
861 self.loginFailure(result[1])
864 def _passportError(self, failure):
866 Handle a problem logging in via the Passport server, passing on the
867 error as a string message to the C{loginFailure} callback.
869 if failure.check(SSLRequired):
870 failure = failure.getErrorMessage()
871 self.loginFailure("Exception while authenticating: %s" % failure)
874 def handle_CHG(self, params):
875 checkParamLen(len(params), 3, 'CHG')
877 if not self._fireCallback(id, params[1]):
878 self.statusChanged(params[1])
880 def handle_ILN(self, params):
881 checkParamLen(len(params), 5, 'ILN')
882 self.gotContactStatus(params[1], params[2], unquote(params[3]))
884 def handle_CHL(self, params):
885 checkParamLen(len(params), 2, 'CHL')
886 self.sendLine("QRY %s msmsgs@msnmsgr.com 32" % self._nextTransactionID())
887 self.transport.write(md5(params[1] + MSN_CHALLENGE_STR).hexdigest())
889 def handle_QRY(self, params):
892 def handle_NLN(self, params):
893 checkParamLen(len(params), 4, 'NLN')
894 self.contactStatusChanged(params[0], params[1], unquote(params[2]))
896 def handle_FLN(self, params):
897 checkParamLen(len(params), 1, 'FLN')
898 self.contactOffline(params[0])
900 def handle_LST(self, params):
901 # support no longer exists for manually
902 # requesting lists - why do I feel cleaner now?
903 if self._getState() != 'SYNC':
905 contact = MSNContact(userHandle=params[0], screenName=unquote(params[1]),
906 lists=int(params[2]))
907 if contact.lists & FORWARD_LIST:
908 contact.groups.extend(map(int, params[3].split(',')))
909 self._getStateData('list').addContact(contact)
910 self._setStateData('last_contact', contact)
911 sofar = self._getStateData('lst_sofar') + 1
912 if sofar == self._getStateData('lst_reply'):
913 # this is the best place to determine that
914 # a syn realy has finished - msn _may_ send
915 # BPR information for the last contact
916 # which is unfortunate because it means
917 # that the real end of a syn is non-deterministic.
918 # to handle this we'll keep 'last_contact' hanging
919 # around in the state data and update it if we need
921 self._setState('SESSION')
922 contacts = self._getStateData('list')
923 phone = self._getStateData('phone')
924 id = self._getStateData('synid')
925 self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
926 self._fireCallback(id, contacts, phone)
928 self._setStateData('lst_sofar',sofar)
930 def handle_BLP(self, params):
931 # check to see if this is in response to a SYN
932 if self._getState() == 'SYNC':
933 self._getStateData('list').privacy = listCodeToID[params[0].lower()]
936 self._fireCallback(id, int(params[1]), listCodeToID[params[2].lower()])
938 def handle_GTC(self, params):
939 # check to see if this is in response to a SYN
940 if self._getState() == 'SYNC':
941 if params[0].lower() == "a":
942 self._getStateData('list').autoAdd = 0
943 elif params[0].lower() == "n":
944 self._getStateData('list').autoAdd = 1
946 raise MSNProtocolError, "Invalid Paramater for GTC" # debug
949 if params[1].lower() == "a":
950 self._fireCallback(id, 0)
951 elif params[1].lower() == "n":
952 self._fireCallback(id, 1)
954 raise MSNProtocolError, "Invalid Paramater for GTC" # debug
956 def handle_SYN(self, params):
959 self._setState('SESSION')
960 self._fireCallback(id, None, None)
962 contacts = MSNContactList()
963 contacts.version = int(params[1])
964 self._setStateData('list', contacts)
965 self._setStateData('lst_reply', int(params[2]))
966 self._setStateData('lsg_reply', int(params[3]))
967 self._setStateData('lst_sofar', 0)
968 self._setStateData('phone', [])
970 def handle_LSG(self, params):
971 if self._getState() == 'SYNC':
972 self._getStateData('list').groups[int(params[0])] = unquote(params[1])
974 # Please see the comment above the requestListGroups / requestList methods
975 # regarding support for this
978 # self._getStateData('groups').append((int(params[4]), unquote(params[5])))
979 # if params[3] == params[4]: # this was the last group
980 # self._fireCallback(int(params[0]), self._getStateData('groups'), int(params[1]))
981 # self._remStateData('groups')
983 def handle_PRP(self, params):
984 if self._getState() == 'SYNC':
985 self._getStateData('phone').append((params[0], unquote(params[1])))
987 self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
989 def handle_BPR(self, params):
990 numParams = len(params)
991 if numParams == 2: # part of a syn
992 self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
994 self.gotPhoneNumber(int(params[0]), params[1], params[2], unquote(params[3]))
996 def handle_ADG(self, params):
997 checkParamLen(len(params), 5, 'ADG')
999 if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
1000 raise MSNProtocolError, "ADG response does not match up to a request" # debug
1002 def handle_RMG(self, params):
1003 checkParamLen(len(params), 3, 'RMG')
1005 if not self._fireCallback(id, int(params[1]), int(params[2])):
1006 raise MSNProtocolError, "RMG response does not match up to a request" # debug
1008 def handle_REG(self, params):
1009 checkParamLen(len(params), 5, 'REG')
1011 if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
1012 raise MSNProtocolError, "REG response does not match up to a request" # debug
1014 def handle_ADD(self, params):
1015 numParams = len(params)
1016 if numParams < 5 or params[1].upper() not in ('AL','BL','RL','FL'):
1017 raise MSNProtocolError, "Invalid Paramaters for ADD" # debug
1019 listType = params[1].lower()
1020 listVer = int(params[2])
1021 userHandle = params[3]
1023 if numParams == 6: # they sent a group id
1024 if params[1].upper() != "FL":
1025 raise MSNProtocolError, "Only forward list can contain groups" # debug
1026 groupID = int(params[5])
1027 if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
1028 self.userAddedMe(userHandle, unquote(params[4]), listVer)
1030 def handle_REM(self, params):
1031 numParams = len(params)
1032 if numParams < 4 or params[1].upper() not in ('AL','BL','FL','RL'):
1033 raise MSNProtocolError, "Invalid Paramaters for REM" # debug
1035 listType = params[1].lower()
1036 listVer = int(params[2])
1037 userHandle = params[3]
1040 if params[1] != "FL":
1041 raise MSNProtocolError, "Only forward list can contain groups" # debug
1042 groupID = int(params[4])
1043 if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
1044 if listType.upper() == "RL":
1045 self.userRemovedMe(userHandle, listVer)
1047 def handle_REA(self, params):
1048 checkParamLen(len(params), 4, 'REA')
1050 self._fireCallback(id, int(params[1]), unquote(params[3]))
1052 def handle_XFR(self, params):
1053 checkParamLen(len(params), 5, 'XFR')
1055 # check to see if they sent a host/port pair
1057 host, port = params[2].split(':')
1062 if not self._fireCallback(id, host, int(port), params[4]):
1063 raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1065 def handle_RNG(self, params):
1066 checkParamLen(len(params), 6, 'RNG')
1067 # check for host:port pair
1069 host, port = params[1].split(":")
1074 self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
1077 def handle_OUT(self, params):
1078 checkParamLen(len(params), 1, 'OUT')
1079 if params[0] == "OTH":
1080 self.multipleLogin()
1081 elif params[0] == "SSD":
1082 self.serverGoingDown()
1084 raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
1088 def loggedIn(self, userHandle, screenName, verified):
1090 Called when the client has logged in.
1091 The default behaviour of this method is to
1092 update the factory with our screenName and
1093 to sync the contact list (factory.contacts).
1094 When this is complete self.listSynchronized
1097 @param userHandle: our userHandle
1098 @param screenName: our screenName
1099 @param verified: 1 if our passport has been (verified), 0 if not.
1100 (i'm not sure of the significace of this)
1103 self.factory.screenName = screenName
1104 if not self.factory.contacts:
1107 listVersion = self.factory.contacts.version
1108 self.syncList(listVersion).addCallback(self.listSynchronized)
1111 def loginFailure(self, message):
1113 Called when the client fails to login.
1115 @param message: a message indicating the problem that was encountered
1119 def gotProfile(self, message):
1121 Called after logging in when the server sends an initial
1122 message with MSN/passport specific profile information
1123 such as country, number of kids, etc.
1124 Check the message headers for the specific values.
1126 @param message: The profile message
1130 def listSynchronized(self, *args):
1132 Lists are now synchronized by default upon logging in, this
1133 method is called after the synchronization has finished
1134 and the factory now has the up-to-date contacts.
1138 def statusChanged(self, statusCode):
1140 Called when our status changes and it isn't in response to
1141 a client command. By default we will update the status
1142 attribute of the factory.
1144 @param statusCode: 3-letter status code
1146 self.factory.status = statusCode
1148 def gotContactStatus(self, statusCode, userHandle, screenName):
1150 Called after loggin in when the server sends status of online contacts.
1151 By default we will update the status attribute of the contact stored
1154 @param statusCode: 3-letter status code
1155 @param userHandle: the contact's user handle (passport)
1156 @param screenName: the contact's screen name
1158 self.factory.contacts.getContact(userHandle).status = statusCode
1160 def contactStatusChanged(self, statusCode, userHandle, screenName):
1162 Called when we're notified that a contact's status has changed.
1163 By default we will update the status attribute of the contact
1164 stored on the factory.
1166 @param statusCode: 3-letter status code
1167 @param userHandle: the contact's user handle (passport)
1168 @param screenName: the contact's screen name
1170 self.factory.contacts.getContact(userHandle).status = statusCode
1172 def contactOffline(self, userHandle):
1174 Called when a contact goes offline. By default this method
1175 will update the status attribute of the contact stored
1178 @param userHandle: the contact's user handle
1180 self.factory.contacts.getContact(userHandle).status = STATUS_OFFLINE
1182 def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
1184 Called when the server sends us phone details about
1185 a specific user (for example after a user is added
1186 the server will send their status, phone details etc.
1187 By default we will update the list version for the
1188 factory's contact list and update the phone details
1189 for the specific user.
1191 @param listVersion: the new list version
1192 @param userHandle: the contact's user handle (passport)
1193 @param phoneType: the specific phoneType
1194 (*_PHONE constants or HAS_PAGER)
1195 @param number: the value/phone number.
1197 self.factory.contacts.version = listVersion
1198 self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
1200 def userAddedMe(self, userHandle, screenName, listVersion):
1202 Called when a user adds me to their list. (ie. they have been added to
1203 the reverse list. By default this method will update the version of
1204 the factory's contact list -- that is, if the contact already exists
1205 it will update the associated lists attribute, otherwise it will create
1206 a new MSNContact object and store it.
1208 @param userHandle: the userHandle of the user
1209 @param screenName: the screen name of the user
1210 @param listVersion: the new list version
1211 @type listVersion: int
1213 self.factory.contacts.version = listVersion
1214 c = self.factory.contacts.getContact(userHandle)
1216 c = MSNContact(userHandle=userHandle, screenName=screenName)
1217 self.factory.contacts.addContact(c)
1218 c.addToList(REVERSE_LIST)
1220 def userRemovedMe(self, userHandle, listVersion):
1222 Called when a user removes us from their contact list
1223 (they are no longer on our reverseContacts list.
1224 By default this method will update the version of
1225 the factory's contact list -- that is, the user will
1226 be removed from the reverse list and if they are no longer
1227 part of any lists they will be removed from the contact
1230 @param userHandle: the contact's user handle (passport)
1231 @param listVersion: the new list version
1233 self.factory.contacts.version = listVersion
1234 c = self.factory.contacts.getContact(userHandle)
1235 c.removeFromList(REVERSE_LIST)
1237 self.factory.contacts.remContact(c.userHandle)
1239 def gotSwitchboardInvitation(self, sessionID, host, port,
1240 key, userHandle, screenName):
1242 Called when we get an invitation to a switchboard server.
1243 This happens when a user requests a chat session with us.
1245 @param sessionID: session ID number, must be remembered for logging in
1246 @param host: the hostname of the switchboard server
1247 @param port: the port to connect to
1248 @param key: used for authorization when connecting
1249 @param userHandle: the user handle of the person who invited us
1250 @param screenName: the screen name of the person who invited us
1254 def multipleLogin(self):
1256 Called when the server says there has been another login
1257 under our account, the server should disconnect us right away.
1261 def serverGoingDown(self):
1263 Called when the server has notified us that it is going down for
1270 def changeStatus(self, status):
1272 Change my current status. This method will add
1273 a default callback to the returned Deferred
1274 which will update the status attribute of the
1277 @param status: 3-letter status code (as defined by
1278 the STATUS_* constants)
1279 @return: A Deferred, the callback of which will be
1280 fired when the server confirms the change
1281 of status. The callback argument will be
1282 a tuple with the new status code as the
1286 id, d = self._createIDMapping()
1287 self.sendLine("CHG %s %s" % (id, status))
1289 self.factory.status = r[0]
1291 return d.addCallback(_cb)
1293 # I am no longer supporting the process of manually requesting
1294 # lists or list groups -- as far as I can see this has no use
1295 # if lists are synchronized and updated correctly, which they
1296 # should be. If someone has a specific justified need for this
1297 # then please contact me and i'll re-enable/fix support for it.
1299 #def requestList(self, listType):
1301 # request the desired list type
1303 # @param listType: (as defined by the *_LIST constants)
1304 # @return: A Deferred, the callback of which will be
1305 # fired when the list has been retrieved.
1306 # The callback argument will be a tuple with
1307 # the only element being a list of MSNContact
1310 # # this doesn't need to ever be used if syncing of the lists takes place
1311 # # i.e. please don't use it!
1312 # warnings.warn("Please do not use this method - use the list syncing process instead")
1313 # id, d = self._createIDMapping()
1314 # self.sendLine("LST %s %s" % (id, listIDToCode[listType].upper()))
1315 # self._setStateData('list',[])
1318 def setPrivacyMode(self, privLevel):
1320 Set my privacy mode on the server.
1323 This only keeps the current privacy setting on
1324 the server for later retrieval, it does not
1325 effect the way the server works at all.
1327 @param privLevel: This parameter can be true, in which
1328 case the server will keep the state as
1329 'al' which the official client interprets
1330 as -> allow messages from only users on
1331 the allow list. Alternatively it can be
1332 false, in which case the server will keep
1333 the state as 'bl' which the official client
1334 interprets as -> allow messages from all
1335 users except those on the block list.
1337 @return: A Deferred, the callback of which will be fired when
1338 the server replies with the new privacy setting.
1339 The callback argument will be a tuple, the 2 elements
1340 of which being the list version and either 'al'
1341 or 'bl' (the new privacy setting).
1344 id, d = self._createIDMapping()
1346 self.sendLine("BLP %s AL" % id)
1348 self.sendLine("BLP %s BL" % id)
1351 def syncList(self, version):
1353 Used for keeping an up-to-date contact list.
1354 A callback is added to the returned Deferred
1355 that updates the contact list on the factory
1356 and also sets my state to STATUS_ONLINE.
1359 This is called automatically upon signing
1360 in using the version attribute of
1361 factory.contacts, so you may want to persist
1362 this object accordingly. Because of this there
1363 is no real need to ever call this method
1366 @param version: The current known list version
1368 @return: A Deferred, the callback of which will be
1369 fired when the server sends an adequate reply.
1370 The callback argument will be a tuple with two
1371 elements, the new list (MSNContactList) and
1372 your current state (a dictionary). If the version
1373 you sent _was_ the latest list version, both elements
1374 will be None. To just request the list send a version of 0.
1377 self._setState('SYNC')
1378 id, d = self._createIDMapping(data=str(version))
1379 self._setStateData('synid',id)
1380 self.sendLine("SYN %s %s" % (id, version))
1382 self.changeStatus(STATUS_ONLINE)
1383 if r[0] is not None:
1384 self.factory.contacts = r[0]
1386 return d.addCallback(_cb)
1389 # I am no longer supporting the process of manually requesting
1390 # lists or list groups -- as far as I can see this has no use
1391 # if lists are synchronized and updated correctly, which they
1392 # should be. If someone has a specific justified need for this
1393 # then please contact me and i'll re-enable/fix support for it.
1395 #def requestListGroups(self):
1397 # Request (forward) list groups.
1399 # @return: A Deferred, the callback for which will be called
1400 # when the server responds with the list groups.
1401 # The callback argument will be a tuple with two elements,
1402 # a dictionary mapping group IDs to group names and the
1403 # current list version.
1406 # # this doesn't need to be used if syncing of the lists takes place (which it SHOULD!)
1407 # # i.e. please don't use it!
1408 # warnings.warn("Please do not use this method - use the list syncing process instead")
1409 # id, d = self._createIDMapping()
1410 # self.sendLine("LSG %s" % id)
1411 # self._setStateData('groups',{})
1414 def setPhoneDetails(self, phoneType, value):
1416 Set/change my phone numbers stored on the server.
1418 @param phoneType: phoneType can be one of the following
1419 constants - HOME_PHONE, WORK_PHONE,
1420 MOBILE_PHONE, HAS_PAGER.
1421 These are pretty self-explanatory, except
1422 maybe HAS_PAGER which refers to whether or
1423 not you have a pager.
1424 @param value: for all of the *_PHONE constants the value is a
1425 phone number (str), for HAS_PAGER accepted values
1426 are 'Y' (for yes) and 'N' (for no).
1428 @return: A Deferred, the callback for which will be fired when
1429 the server confirms the change has been made. The
1430 callback argument will be a tuple with 2 elements, the
1431 first being the new list version (int) and the second
1432 being the new phone number value (str).
1434 # XXX: Add a default callback which updates
1435 # factory.contacts.version and the relevant phone
1437 id, d = self._createIDMapping()
1438 self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
1441 def addListGroup(self, name):
1443 Used to create a new list group.
1444 A default callback is added to the
1445 returned Deferred which updates the
1446 contacts attribute of the factory.
1448 @param name: The desired name of the new group.
1450 @return: A Deferred, the callbacck for which will be called
1451 when the server clarifies that the new group has been
1452 created. The callback argument will be a tuple with 3
1453 elements: the new list version (int), the new group name
1454 (str) and the new group ID (int).
1457 id, d = self._createIDMapping()
1458 self.sendLine("ADG %s %s 0" % (id, quote(name)))
1460 self.factory.contacts.version = r[0]
1461 self.factory.contacts.setGroup(r[1], r[2])
1463 return d.addCallback(_cb)
1465 def remListGroup(self, groupID):
1467 Used to remove a list group.
1468 A default callback is added to the
1469 returned Deferred which updates the
1470 contacts attribute of the factory.
1472 @param groupID: the ID of the desired group to be removed.
1474 @return: A Deferred, the callback for which will be called when
1475 the server clarifies the deletion of the group.
1476 The callback argument will be a tuple with 2 elements:
1477 the new list version (int) and the group ID (int) of
1481 id, d = self._createIDMapping()
1482 self.sendLine("RMG %s %s" % (id, groupID))
1484 self.factory.contacts.version = r[0]
1485 self.factory.contacts.remGroup(r[1])
1487 return d.addCallback(_cb)
1489 def renameListGroup(self, groupID, newName):
1491 Used to rename an existing list group.
1492 A default callback is added to the returned
1493 Deferred which updates the contacts attribute
1496 @param groupID: the ID of the desired group to rename.
1497 @param newName: the desired new name for the group.
1499 @return: A Deferred, the callback for which will be called
1500 when the server clarifies the renaming.
1501 The callback argument will be a tuple of 3 elements,
1502 the new list version (int), the group id (int) and
1503 the new group name (str).
1506 id, d = self._createIDMapping()
1507 self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
1509 self.factory.contacts.version = r[0]
1510 self.factory.contacts.setGroup(r[1], r[2])
1512 return d.addCallback(_cb)
1514 def addContact(self, listType, userHandle, groupID=0):
1516 Used to add a contact to the desired list.
1517 A default callback is added to the returned
1518 Deferred which updates the contacts attribute of
1519 the factory with the new contact information.
1520 If you are adding a contact to the forward list
1521 and you want to associate this contact with multiple
1522 groups then you will need to call this method for each
1523 group you would like to add them to, changing the groupID
1524 parameter. The default callback will take care of updating
1525 the group information on the factory's contact list.
1527 @param listType: (as defined by the *_LIST constants)
1528 @param userHandle: the user handle (passport) of the contact
1530 @param groupID: the group ID for which to associate this contact
1531 with. (default 0 - default group). Groups are only
1532 valid for FORWARD_LIST.
1534 @return: A Deferred, the callback for which will be called when
1535 the server has clarified that the user has been added.
1536 The callback argument will be a tuple with 4 elements:
1537 the list type, the contact's user handle, the new list
1538 version, and the group id (if relevant, otherwise it
1542 id, d = self._createIDMapping()
1543 listType = listIDToCode[listType].upper()
1544 if listType == "FL":
1545 self.sendLine("ADD %s FL %s %s %s" % (id, userHandle, userHandle, groupID))
1547 self.sendLine("ADD %s %s %s %s" % (id, listType, userHandle, userHandle))
1550 self.factory.contacts.version = r[2]
1551 c = self.factory.contacts.getContact(r[1])
1553 c = MSNContact(userHandle=r[1])
1555 c.groups.append(r[3])
1558 return d.addCallback(_cb)
1560 def remContact(self, listType, userHandle, groupID=0):
1562 Used to remove a contact from the desired list.
1563 A default callback is added to the returned deferred
1564 which updates the contacts attribute of the factory
1565 to reflect the new contact information. If you are
1566 removing from the forward list then you will need to
1567 supply a groupID, if the contact is in more than one
1568 group then they will only be removed from this group
1569 and not the entire forward list, but if this is their
1570 only group they will be removed from the whole list.
1572 @param listType: (as defined by the *_LIST constants)
1573 @param userHandle: the user handle (passport) of the
1574 contact being removed
1575 @param groupID: the ID of the group to which this contact
1576 belongs (only relevant for FORWARD_LIST,
1579 @return: A Deferred, the callback for which will be called when
1580 the server has clarified that the user has been removed.
1581 The callback argument will be a tuple of 4 elements:
1582 the list type, the contact's user handle, the new list
1583 version, and the group id (if relevant, otherwise it will
1587 id, d = self._createIDMapping()
1588 listType = listIDToCode[listType].upper()
1589 if listType == "FL":
1590 self.sendLine("REM %s FL %s %s" % (id, userHandle, groupID))
1592 self.sendLine("REM %s %s %s" % (id, listType, userHandle))
1595 l = self.factory.contacts
1597 c = l.getContact(r[1])
1600 if group: # they may not have been removed from the list
1601 c.groups.remove(group)
1605 c.removeFromList(r[0])
1607 l.remContact(c.userHandle)
1609 return d.addCallback(_cb)
1611 def changeScreenName(self, newName):
1613 Used to change your current screen name.
1614 A default callback is added to the returned
1615 Deferred which updates the screenName attribute
1616 of the factory and also updates the contact list
1619 @param newName: the new screen name
1621 @return: A Deferred, the callback for which will be called
1622 when the server sends an adequate reply.
1623 The callback argument will be a tuple of 2 elements:
1624 the new list version and the new screen name.
1627 id, d = self._createIDMapping()
1628 self.sendLine("REA %s %s %s" % (id, self.factory.userHandle, quote(newName)))
1630 self.factory.contacts.version = r[0]
1631 self.factory.screenName = r[1]
1633 return d.addCallback(_cb)
1635 def requestSwitchboardServer(self):
1637 Used to request a switchboard server to use for conversations.
1639 @return: A Deferred, the callback for which will be called when
1640 the server responds with the switchboard information.
1641 The callback argument will be a tuple with 3 elements:
1642 the host of the switchboard server, the port and a key
1643 used for logging in.
1646 id, d = self._createIDMapping()
1647 self.sendLine("XFR %s SB" % id)
1652 Used to log out of the notification server.
1653 After running the method the server is expected
1654 to close the connection.
1657 self.sendLine("OUT")
1659 class NotificationFactory(ClientFactory):
1661 Factory for the NotificationClient protocol.
1662 This is basically responsible for keeping
1663 the state of the client and thus should be used
1664 in a 1:1 situation with clients.
1666 @ivar contacts: An MSNContactList instance reflecting
1667 the current contact list -- this is
1668 generally kept up to date by the default
1670 @ivar userHandle: The client's userHandle, this is expected
1671 to be set by the client and is used by the
1672 protocol (for logging in etc).
1673 @ivar screenName: The client's current screen-name -- this is
1674 generally kept up to date by the default
1676 @ivar password: The client's password -- this is (obviously)
1677 expected to be set by the client.
1678 @ivar passportServer: This must point to an msn passport server
1679 (the whole URL is required)
1680 @ivar status: The status of the client -- this is generally kept
1681 up to date by the default command handlers
1688 passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
1690 protocol = NotificationClient
1693 # XXX: A lot of the state currently kept in
1694 # instances of SwitchboardClient is likely to
1695 # be moved into a factory at some stage in the
1698 class SwitchboardClient(MSNEventBase):
1700 This class provides support for clients connecting to a switchboard server.
1702 Switchboard servers are used for conversations with other people
1703 on the MSN network. This means that the number of conversations at
1704 any given time will be directly proportional to the number of
1705 connections to varioius switchboard servers.
1707 MSN makes no distinction between single and group conversations,
1708 so any number of users may be invited to join a specific conversation
1709 taking place on a switchboard server.
1711 @ivar key: authorization key, obtained when receiving
1712 invitation / requesting switchboard server.
1713 @ivar userHandle: your user handle (passport)
1714 @ivar sessionID: unique session ID, used if you are replying
1715 to a switchboard invitation
1716 @ivar reply: set this to 1 in connectionMade or before to signifiy
1717 that you are replying to a switchboard invitation.
1728 MSNEventBase.__init__(self)
1729 self.pendingUsers = {}
1730 self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
1732 def connectionMade(self):
1733 MSNEventBase.connectionMade(self)
1734 print 'sending initial stuff'
1737 def connectionLost(self, reason):
1738 self.cookies['iCookies'] = {}
1739 self.cookies['external'] = {}
1740 MSNEventBase.connectionLost(self, reason)
1742 def _sendInit(self):
1744 send initial data based on whether we are replying to an invitation
1747 id = self._nextTransactionID()
1749 self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
1751 self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
1753 def _newInvitationCookie(self):
1755 if self._iCookie > 1000:
1757 return self._iCookie
1759 def _checkTyping(self, message, cTypes):
1760 """ helper method for checkMessage """
1761 if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
1762 self.userTyping(message)
1765 def _checkFileInvitation(self, message, info):
1766 """ helper method for checkMessage """
1767 guid = info.get('Application-GUID', '').lower()
1768 name = info.get('Application-Name', '').lower()
1770 # Both fields are required, but we'll let some lazy clients get away
1771 # with only sending a name, if it is easy for us to recognize the
1772 # name (the name is localized, so this check might fail for lazy,
1773 # non-english clients, but I'm not about to include "file transfer"
1774 # in 80 different languages here).
1776 if name != "file transfer" and guid != classNameToGUID["file transfer"]:
1779 cookie = int(info['Invitation-Cookie'])
1780 fileName = info['Application-File']
1781 fileSize = int(info['Application-FileSize'])
1783 log.msg('Received munged file transfer request ... ignoring.')
1785 self.gotSendRequest(fileName, fileSize, cookie, message)
1788 def _checkFileResponse(self, message, info):
1789 """ helper method for checkMessage """
1791 cmd = info['Invitation-Command'].upper()
1792 cookie = int(info['Invitation-Cookie'])
1795 accept = (cmd == 'ACCEPT') and 1 or 0
1796 requested = self.cookies['iCookies'].get(cookie)
1799 requested[0].callback((accept, cookie, info))
1800 del self.cookies['iCookies'][cookie]
1803 def _checkFileInfo(self, message, info):
1804 """ helper method for checkMessage """
1806 ip = info['IP-Address']
1807 iCookie = int(info['Invitation-Cookie'])
1808 aCookie = int(info['AuthCookie'])
1809 cmd = info['Invitation-Command'].upper()
1810 port = int(info['Port'])
1813 accept = (cmd == 'ACCEPT') and 1 or 0
1814 requested = self.cookies['external'].get(iCookie)
1816 return 1 # we didn't ask for this
1817 requested[0].callback((accept, ip, port, aCookie, info))
1818 del self.cookies['external'][iCookie]
1821 def checkMessage(self, message):
1823 hook for detecting any notification type messages
1824 (e.g. file transfer)
1826 cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
1827 if self._checkTyping(message, cTypes):
1829 if 'text/x-msmsgsinvite' in cTypes:
1830 # header like info is sent as part of the message body.
1832 for line in message.message.split('\r\n'):
1834 key, val = line.split(':')
1835 info[key] = val.lstrip()
1838 if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info):
1840 elif 'text/x-clientcaps' in cTypes:
1841 # do something with capabilities
1846 def handle_USR(self, params):
1847 checkParamLen(len(params), 4, 'USR')
1848 if params[1] == "OK":
1852 def handle_CAL(self, params):
1853 checkParamLen(len(params), 3, 'CAL')
1855 if params[1].upper() == "RINGING":
1856 self._fireCallback(id, int(params[2])) # session ID as parameter
1859 def handle_JOI(self, params):
1860 checkParamLen(len(params), 2, 'JOI')
1861 self.userJoined(params[0], unquote(params[1]))
1863 # users participating in the current chat
1864 def handle_IRO(self, params):
1865 checkParamLen(len(params), 5, 'IRO')
1866 self.pendingUsers[params[3]] = unquote(params[4])
1867 if params[1] == params[2]:
1868 self.gotChattingUsers(self.pendingUsers)
1869 self.pendingUsers = {}
1871 # finished listing users
1872 def handle_ANS(self, params):
1873 checkParamLen(len(params), 2, 'ANS')
1874 if params[1] == "OK":
1877 def handle_ACK(self, params):
1878 checkParamLen(len(params), 1, 'ACK')
1879 self._fireCallback(int(params[0]), None)
1881 def handle_NAK(self, params):
1882 checkParamLen(len(params), 1, 'NAK')
1883 self._fireCallback(int(params[0]), None)
1885 def handle_BYE(self, params):
1886 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
1887 self.userLeft(params[0])
1893 called when all login details have been negotiated.
1894 Messages can now be sent, or new users invited.
1898 def gotChattingUsers(self, users):
1900 called after connecting to an existing chat session.
1902 @param users: A dict mapping user handles to screen names
1903 (current users taking part in the conversation)
1907 def userJoined(self, userHandle, screenName):
1909 called when a user has joined the conversation.
1911 @param userHandle: the user handle (passport) of the user
1912 @param screenName: the screen name of the user
1916 def userLeft(self, userHandle):
1918 called when a user has left the conversation.
1920 @param userHandle: the user handle (passport) of the user.
1924 def gotMessage(self, message):
1926 called when we receive a message.
1928 @param message: the associated MSNMessage object
1932 def userTyping(self, message):
1934 called when we receive the special type of message notifying
1935 us that a user is typing a message.
1937 @param message: the associated MSNMessage object
1941 def gotSendRequest(self, fileName, fileSize, iCookie, message):
1943 called when a contact is trying to send us a file.
1944 To accept or reject this transfer see the
1945 fileInvitationReply method.
1947 @param fileName: the name of the file
1948 @param fileSize: the size of the file
1949 @param iCookie: the invitation cookie, used so the client can
1950 match up your reply with this request.
1951 @param message: the MSNMessage object which brought about this
1952 invitation (it may contain more information)
1958 def inviteUser(self, userHandle):
1960 used to invite a user to the current switchboard server.
1962 @param userHandle: the user handle (passport) of the desired user.
1964 @return: A Deferred, the callback for which will be called
1965 when the server notifies us that the user has indeed
1966 been invited. The callback argument will be a tuple
1967 with 1 element, the sessionID given to the invited user.
1968 I'm not sure if this is useful or not.
1971 id, d = self._createIDMapping()
1972 self.sendLine("CAL %s %s" % (id, userHandle))
1975 def sendMessage(self, message):
1977 used to send a message.
1979 @param message: the corresponding MSNMessage object.
1981 @return: Depending on the value of message.ack.
1982 If set to MSNMessage.MESSAGE_ACK or
1983 MSNMessage.MESSAGE_NACK a Deferred will be returned,
1984 the callback for which will be fired when an ACK or
1985 NACK is received - the callback argument will be
1986 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
1987 the return value is None.
1990 if message.ack not in ('A','N'):
1991 id, d = self._nextTransactionID(), None
1993 id, d = self._createIDMapping()
1994 if message.length == 0:
1995 message.length = message._calcMessageLen()
1996 self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
1997 # apparently order matters with at least MIME-Version and Content-Type
1998 self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
1999 self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
2000 # send the rest of the headers
2001 for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
2002 self.sendLine("%s: %s" % (header[0], header[1]))
2003 self.transport.write(CR+LF)
2004 self.transport.write(message.message)
2007 def sendTypingNotification(self):
2009 used to send a typing notification. Upon receiving this
2010 message the official client will display a 'user is typing'
2011 message to all other users in the chat session for 10 seconds.
2012 The official client sends one of these every 5 seconds (I think)
2013 as long as you continue to type.
2016 m.ack = m.MESSAGE_ACK_NONE
2017 m.setHeader('Content-Type', 'text/x-msmsgscontrol')
2018 m.setHeader('TypingUser', self.userHandle)
2022 def sendFileInvitation(self, fileName, fileSize):
2024 send an notification that we want to send a file.
2026 @param fileName: the file name
2027 @param fileSize: the file size
2029 @return: A Deferred, the callback of which will be fired
2030 when the user responds to this invitation with an
2031 appropriate message. The callback argument will be
2032 a tuple with 3 elements, the first being 1 or 0
2033 depending on whether they accepted the transfer
2034 (1=yes, 0=no), the second being an invitation cookie
2035 to identify your follow-up responses and the third being
2036 the message 'info' which is a dict of information they
2037 sent in their reply (this doesn't really need to be used).
2038 If you wish to proceed with the transfer see the
2039 sendTransferInfo method.
2041 cookie = self._newInvitationCookie()
2044 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2045 m.message += 'Application-Name: File Transfer\r\n'
2046 m.message += 'Application-GUID: %s\r\n' % (classNameToGUID["file transfer"],)
2047 m.message += 'Invitation-Command: INVITE\r\n'
2048 m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
2049 m.message += 'Application-File: %s\r\n' % fileName
2050 m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
2051 m.ack = m.MESSAGE_ACK_NONE
2053 self.cookies['iCookies'][cookie] = (d, m)
2056 def fileInvitationReply(self, iCookie, accept=1):
2058 used to reply to a file transfer invitation.
2060 @param iCookie: the invitation cookie of the initial invitation
2061 @param accept: whether or not you accept this transfer,
2062 1 = yes, 0 = no, default = 1.
2064 @return: A Deferred, the callback for which will be fired when
2065 the user responds with the transfer information.
2066 The callback argument will be a tuple with 5 elements,
2067 whether or not they wish to proceed with the transfer
2068 (1=yes, 0=no), their ip, the port, the authentication
2069 cookie (see FileReceive/FileSend) and the message
2070 info (dict) (in case they send extra header-like info
2071 like Internal-IP, this doesn't necessarily need to be
2072 used). If you wish to proceed with the transfer see
2077 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2078 m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2079 m.message += 'Invitation-Cookie: %s\r\n' % str(iCookie)
2081 m.message += 'Cancel-Code: REJECT\r\n'
2082 m.message += 'Launch-Application: FALSE\r\n'
2083 m.message += 'Request-Data: IP-Address:\r\n'
2085 m.ack = m.MESSAGE_ACK_NONE
2087 self.cookies['external'][iCookie] = (d, m)
2090 def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
2092 send information relating to a file transfer session.
2094 @param accept: whether or not to go ahead with the transfer
2096 @param iCookie: the invitation cookie of previous replies
2097 relating to this transfer
2098 @param authCookie: the authentication cookie obtained from
2099 an FileSend instance
2101 @param port: the port on which an FileSend protocol is listening.
2104 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2105 m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2106 m.message += 'Invitation-Cookie: %s\r\n' % iCookie
2107 m.message += 'IP-Address: %s\r\n' % ip
2108 m.message += 'Port: %s\r\n' % port
2109 m.message += 'AuthCookie: %s\r\n' % authCookie
2111 m.ack = m.MESSAGE_NACK
2114 class FileReceive(LineReceiver):
2116 This class provides support for receiving files from contacts.
2118 @ivar fileSize: the size of the receiving file. (you will have to set this)
2119 @ivar connected: true if a connection has been established.
2120 @ivar completed: true if the transfer is complete.
2121 @ivar bytesReceived: number of bytes (of the file) received.
2122 This does not include header data.
2125 def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
2127 @param auth: auth string received in the file invitation.
2128 @param myUserHandle: your userhandle.
2129 @param file: A string or file object represnting the file
2131 @param directory: optional parameter specifiying the directory.
2132 Defaults to the current directory.
2133 @param overwrite: if true and a file of the same name exists on
2134 your system, it will be overwritten. (0 by default)
2137 self.myUserHandle = myUserHandle
2141 self.directory = directory
2142 self.bytesReceived = 0
2143 self.overwrite = overwrite
2145 # used for handling current received state
2146 self.state = 'CONNECTING'
2147 self.segmentLength = 0
2150 if isinstance(file, types.StringType):
2151 path = os.path.join(directory, file)
2152 if os.path.exists(path) and not self.overwrite:
2153 log.msg('File already exists...')
2154 raise IOError, "File Exists" # is this all we should do here?
2155 self.file = open(os.path.join(directory, file), 'wb')
2159 def connectionMade(self):
2161 self.state = 'INHEADER'
2162 self.sendLine('VER MSNFTP')
2164 def connectionLost(self, reason):
2168 def parseHeader(self, header):
2169 """ parse the header of each 'message' to obtain the segment length """
2171 if ord(header[0]) != 0: # they requested that we close the connection
2172 self.transport.loseConnection()
2175 extra, factor = header[1:]
2177 # munged header, ending transfer
2178 self.transport.loseConnection()
2181 factor = ord(factor)
2182 return factor * 256 + extra
2184 def lineReceived(self, line):
2191 handler = getattr(self, "handle_%s" % cmd.upper(), None)
2193 handler(params) # try/except
2195 self.handle_UNKNOWN(cmd, params)
2197 def rawDataReceived(self, data):
2198 bufferLen = len(self.buffer)
2199 if self.state == 'INHEADER':
2201 self.buffer += data[:delim]
2202 if len(self.buffer) == 3:
2203 self.segmentLength = self.parseHeader(self.buffer)
2204 if not self.segmentLength:
2207 self.state = 'INSEGMENT'
2208 extra = data[delim:]
2210 self.rawDataReceived(extra)
2213 elif self.state == 'INSEGMENT':
2214 dataSeg = data[:(self.segmentLength-bufferLen)]
2215 self.buffer += dataSeg
2216 self.bytesReceived += len(dataSeg)
2217 if len(self.buffer) == self.segmentLength:
2218 self.gotSegment(self.buffer)
2220 if self.bytesReceived == self.fileSize:
2224 self.sendLine("BYE 16777989")
2226 self.state = 'INHEADER'
2227 extra = data[(self.segmentLength-bufferLen):]
2229 self.rawDataReceived(extra)
2232 def handle_VER(self, params):
2233 checkParamLen(len(params), 1, 'VER')
2234 if params[0].upper() == "MSNFTP":
2235 self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
2237 log.msg('they sent the wrong version, time to quit this transfer')
2238 self.transport.loseConnection()
2240 def handle_FIL(self, params):
2241 checkParamLen(len(params), 1, 'FIL')
2243 self.fileSize = int(params[0])
2244 except ValueError: # they sent the wrong file size - probably want to log this
2245 self.transport.loseConnection()
2248 self.sendLine("TFR")
2250 def handle_UNKNOWN(self, cmd, params):
2251 log.msg('received unknown command (%s), params: %s' % (cmd, params))
2253 def gotSegment(self, data):
2254 """ called when a segment (block) of data arrives. """
2255 self.file.write(data)
2257 class FileSend(LineReceiver):
2259 This class provides support for sending files to other contacts.
2261 @ivar bytesSent: the number of bytes that have currently been sent.
2262 @ivar completed: true if the send has completed.
2263 @ivar connected: true if a connection has been established.
2264 @ivar targetUser: the target user (contact).
2265 @ivar segmentSize: the segment (block) size.
2266 @ivar auth: the auth cookie (number) to use when sending the
2270 def __init__(self, file):
2272 @param file: A string or file object represnting the file to send.
2275 if isinstance(file, types.StringType):
2276 self.file = open(file, 'rb')
2284 self.targetUser = None
2285 self.segmentSize = 2045
2286 self.auth = randint(0, 2**30)
2287 self._pendingSend = None # :(
2289 def connectionMade(self):
2292 def connectionLost(self, reason):
2293 if self._pendingSend.active():
2294 self._pendingSend.cancel()
2295 self._pendingSend = None
2296 if self.bytesSent == self.fileSize:
2301 def lineReceived(self, line):
2308 handler = getattr(self, "handle_%s" % cmd.upper(), None)
2312 self.handle_UNKNOWN(cmd, params)
2314 def handle_VER(self, params):
2315 checkParamLen(len(params), 1, 'VER')
2316 if params[0].upper() == "MSNFTP":
2317 self.sendLine("VER MSNFTP")
2318 else: # they sent some weird version during negotiation, i'm quitting.
2319 self.transport.loseConnection()
2321 def handle_USR(self, params):
2322 checkParamLen(len(params), 2, 'USR')
2323 self.targetUser = params[0]
2324 if self.auth == int(params[1]):
2325 self.sendLine("FIL %s" % (self.fileSize))
2326 else: # they failed the auth test, disconnecting.
2327 self.transport.loseConnection()
2329 def handle_TFR(self, params):
2330 checkParamLen(len(params), 0, 'TFR')
2331 # they are ready for me to start sending
2334 def handle_BYE(self, params):
2335 self.completed = (self.bytesSent == self.fileSize)
2336 self.transport.loseConnection()
2338 def handle_CCL(self, params):
2339 self.completed = (self.bytesSent == self.fileSize)
2340 self.transport.loseConnection()
2342 def handle_UNKNOWN(self, cmd, params):
2343 log.msg('received unknown command (%s), params: %s' % (cmd, params))
2345 def makeHeader(self, size):
2346 """ make the appropriate header given a specific segment size. """
2347 quotient, remainder = divmod(size, 256)
2348 return chr(0) + chr(remainder) + chr(quotient)
2351 """ send a segment of data """
2352 if not self.connected:
2353 self._pendingSend = None
2354 return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
2355 data = self.file.read(self.segmentSize)
2357 dataSize = len(data)
2358 header = self.makeHeader(dataSize)
2359 self.bytesSent += dataSize
2360 self.transport.write(header + data)
2361 self._pendingSend = reactor.callLater(0, self.sendPart)
2363 self._pendingSend = None
2366 # mapping of error codes to error messages
2369 200 : "Syntax error",
2370 201 : "Invalid parameter",
2371 205 : "Invalid user",
2372 206 : "Domain name missing",
2373 207 : "Already logged in",
2374 208 : "Invalid username",
2375 209 : "Invalid screen name",
2376 210 : "User list full",
2377 215 : "User already there",
2378 216 : "User already on list",
2379 217 : "User not online",
2380 218 : "Already in mode",
2381 219 : "User is in the opposite list",
2382 223 : "Too many groups",
2383 224 : "Invalid group",
2384 225 : "User not in group",
2385 229 : "Group name too long",
2386 230 : "Cannot remove group 0",
2387 231 : "Invalid group",
2388 280 : "Switchboard failed",
2389 281 : "Transfer to switchboard failed",
2391 300 : "Required field missing",
2392 301 : "Too many FND responses",
2393 302 : "Not logged in",
2395 500 : "Internal server error",
2396 501 : "Database server error",
2397 502 : "Command disabled",
2398 510 : "File operation failed",
2399 520 : "Memory allocation failed",
2400 540 : "Wrong CHL value sent to server",
2402 600 : "Server is busy",
2403 601 : "Server is unavaliable",
2404 602 : "Peer nameserver is down",
2405 603 : "Database connection failed",
2406 604 : "Server is going down",
2407 605 : "Server unavailable",
2409 707 : "Could not create connection",
2410 710 : "Invalid CVR parameters",
2411 711 : "Write is blocking",
2412 712 : "Session is overloaded",
2413 713 : "Too many active users",
2414 714 : "Too many sessions",
2415 715 : "Not expected",
2416 717 : "Bad friend file",
2417 731 : "Not expected",
2419 800 : "Requests too rapid",
2421 910 : "Server too busy",
2422 911 : "Authentication failed",
2423 912 : "Server too busy",
2424 913 : "Not allowed when offline",
2425 914 : "Server too busy",
2426 915 : "Server too busy",
2427 916 : "Server too busy",
2428 917 : "Server too busy",
2429 918 : "Server too busy",
2430 919 : "Server too busy",
2431 920 : "Not accepting new users",
2432 921 : "Server too busy",
2433 922 : "Server too busy",
2434 923 : "No parent consent",
2435 924 : "Passport account not yet verified"
2439 # mapping of status codes to readable status format
2442 STATUS_ONLINE : "Online",
2443 STATUS_OFFLINE : "Offline",
2444 STATUS_HIDDEN : "Appear Offline",
2445 STATUS_IDLE : "Idle",
2446 STATUS_AWAY : "Away",
2447 STATUS_BUSY : "Busy",
2448 STATUS_BRB : "Be Right Back",
2449 STATUS_PHONE : "On the Phone",
2450 STATUS_LUNCH : "Out to Lunch"
2454 # mapping of list ids to list codes
2457 FORWARD_LIST : 'fl',
2464 # mapping of list codes to list ids
2466 for id,code in listIDToCode.items():
2467 listCodeToID[code] = id
2471 # Mapping of class GUIDs to simple english names
2473 "{5D3E02AB-6190-11d3-BBBB-00C04F795683}": "file transfer",
2476 # Reverse of the above
2477 classNameToGUID = {}
2478 for guid, name in guidToClassName.iteritems():
2479 classNameToGUID[name] = guid