1 # Twisted, the Framework of Your Internet
2 # Copyright (C) 2001-2002 Matthew W. Lefkowitz
3 # Copyright (C) 2004-2005 James C. Bunton
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of version 2.1 of the GNU Lesser General Public
7 # License as published by the Free Software Foundation.
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 MSNP11 Protocol (client only) - semi-experimental
24 This module provides support for clients using the MSN Protocol (MSNP11).
25 There are basically 3 servers involved in any MSN session:
29 The DispatchClient class handles connections to the
30 dispatch server, which basically delegates users to a
31 suitable notification server.
33 You will want to subclass this and handle the gotNotificationReferral
36 I{Notification Server}
38 The NotificationClient class handles connections to the
39 notification server, which acts as a session server
40 (state updates, message negotiation etc...)
44 The SwitchboardClient handles connections to switchboard
45 servers which are used to conduct conversations with other users.
47 There are also two classes (FileSend and FileReceive) used
50 Clients handle events in two ways.
52 - each client request requiring a response will return a Deferred,
53 the callback for same will be fired when the server sends the
55 - Events which are not in response to any client request have
56 respective methods which should be overridden and handled in
59 Most client request callbacks require more than one argument,
60 and since Deferreds can only pass the callback one result,
61 most of the time the callback argument will be a tuple of
62 values (documented in the respective request method).
63 To make reading/writing code easier, callbacks can be defined in
64 a number of ways to handle this 'cleanly'. One way would be to
65 define methods like: def callBack(self, (arg1, arg2, arg)): ...
66 another way would be to do something like:
67 d.addCallback(lambda result: myCallback(*result)).
69 If the server sends an error response to a client request,
70 the errback of the corresponding Deferred will be called,
71 the argument being the corresponding error code.
74 Due to the lack of an official spec for MSNP11, extra checking
75 than may be deemed necessary often takes place considering the
76 server is never 'wrong'. Thus, if gotBadLine (in any of the 3
77 main clients) is called, or an MSNProtocolError is raised, it's
78 probably a good idea to submit a bug report. ;)
79 Use of this module requires that PyOpenSSL is installed.
81 @author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
82 @author: U{James Bunton<mailto:james@delx.cjb.net>}
85 from __future__
import nested_scopes
88 from twisted
.protocols
.basic
import LineReceiver
90 from twisted
.web
.http
import HTTPClient
93 from twisted
.protocols
.http
import HTTPClient
95 print "Couldn't find a HTTPClient. If you're using Twisted 2.0 make sure you've installed twisted.web"
100 from twisted
.internet
import reactor
, task
101 from twisted
.internet
.defer
import Deferred
102 from twisted
.internet
.protocol
import ReconnectingClientFactory
, ClientFactory
104 from twisted
.internet
.ssl
import ClientContextFactory
106 print "You must install pycrypto and pyopenssl."
108 from twisted
.python
import failure
, log
111 from tlib
import xmlw
114 import types
, operator
, os
, sys
, base64
, random
, struct
, random
, sha
, base64
, StringIO
, array
, codecs
, binascii
115 from urllib
import quote
, unquote
118 MSN_PROTOCOL_VERSION
= "MSNP11 CVR0" # protocol version
119 MSN_PORT
= 1863 # default dispatch server port
120 MSN_MAX_MESSAGE
= 1664 # max message length
121 MSN_CVR_STR
= "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
122 MSN_AVATAR_GUID
= "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
123 MSN_MSNFTP_GUID
= "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
145 STATUS_ONLINE
= 'NLN'
146 STATUS_OFFLINE
= 'FLN'
147 STATUS_HIDDEN
= 'HDN'
168 P2PSEQ
= [-3, -2, 0, -1, 1, 2, 3, 4, 5, 6, 7, 8]
177 return inp
.split('=')[1]
189 userHandle
= getVal(p
)
191 screenName
= unquote(getVal(p
))
196 else: # Must be the groups
198 groups
= p
.split(',')
200 raise MSNProtocolError
, "Unknown LST/ADC response" + str(params
) # debug
202 return userHandle
, screenName
, userGuid
, lists
, groups
205 """ Needed for Python 2.3 compatibility """
206 return s
+ (n
-len(s
))*c
208 if sys
.byteorder
== "little":
210 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
211 a
= array
.array("h", s
.encode("utf-16")[2:])
216 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
217 return s
.encode("utf-16")[2:]
220 return base64
.encodestring(s
).replace("\n", "")
223 for pad
in ["", "=", "==", "A", "A=", "A=="]: # Stupid MSN client!
225 return base64
.decodestring(s
+ pad
)
228 raise ValueError("Got some very bad base64!")
231 format
= "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
234 data
.append(random
.random() * 0xAAFF + 0x1111)
239 def checkParamLen(num
, expected
, cmd
, error
=None):
240 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
241 if num
!= expected
: raise MSNProtocolError
, error
243 def _parseHeader(h
, v
):
245 Split a certin number of known
246 header values with the format:
247 field1=val,field2=val,field3=val into
248 a dict mapping fields to values.
249 @param h: the header's key
250 @param v: the header's value as a string
253 if h
in ('passporturls','authentication-info','www-authenticate'):
254 v
= v
.replace('Passport1.4','').lstrip()
256 for fieldPair
in v
.split(','):
258 field
,value
= fieldPair
.split('=',1)
259 fields
[field
.lower()] = value
261 fields
[field
.lower()] = ''
265 def _parsePrimitiveHost(host
):
267 h
,p
= host
.replace('https://','').split('/',1)
271 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
=''):
273 This function is used internally and should not ever be called
277 def _cb(server
, auth
):
278 loginFac
= ClientFactory()
279 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
280 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
283 _cb(nexusServer
, authData
)
285 fac
= ClientFactory()
287 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
288 d
.addErrback(lambda f
: cb
.errback(f
))
289 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
290 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
294 class PassportNexus(HTTPClient
):
297 Used to obtain the URL of a valid passport
300 This class is used internally and should
301 not be instantiated directly -- that is,
302 The passport logging in process is handled
303 transparantly by NotificationClient.
306 def __init__(self
, deferred
, host
):
307 self
.deferred
= deferred
308 self
.host
, self
.path
= _parsePrimitiveHost(host
)
310 def connectionMade(self
):
311 HTTPClient
.connectionMade(self
)
312 self
.sendCommand('GET', self
.path
)
313 self
.sendHeader('Host', self
.host
)
317 def handleHeader(self
, header
, value
):
319 self
.headers
[h
] = _parseHeader(h
, value
)
321 def handleEndHeaders(self
):
322 if self
.connected
: self
.transport
.loseConnection()
323 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
324 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
326 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
328 def handleResponse(self
, r
): pass
330 class PassportLogin(HTTPClient
):
332 This class is used internally to obtain
333 a login ticket from a passport HTTPS
334 server -- it should not be used directly.
339 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
340 self
.deferred
= deferred
341 self
.userHandle
= userHandle
343 self
.authData
= authData
344 self
.host
, self
.path
= _parsePrimitiveHost(host
)
346 def connectionMade(self
):
347 self
.sendCommand('GET', self
.path
)
348 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
349 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), self
.passwd
,self
.authData
))
350 self
.sendHeader('Host', self
.host
)
354 def handleHeader(self
, header
, value
):
356 self
.headers
[h
] = _parseHeader(h
, value
)
358 def handleEndHeaders(self
):
359 if self
._finished
: return
360 self
._finished
= 1 # I think we need this because of HTTPClient
361 if self
.connected
: self
.transport
.loseConnection()
362 authHeader
= 'authentication-info'
363 _interHeader
= 'www-authenticate'
364 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
366 info
= self
.headers
[authHeader
]
367 status
= info
['da-status']
368 handler
= getattr(self
, 'login_%s' % (status
,), None)
371 else: raise Exception()
373 self
.deferred
.errback(failure
.Failure(e
))
375 def handleResponse(self
, r
): pass
377 def login_success(self
, info
):
378 ticket
= info
['from-pp']
379 ticket
= ticket
[1:len(ticket
)-1]
380 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
382 def login_failed(self
, info
):
383 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
385 def login_redir(self
, info
):
386 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
388 class MSNProtocolError(Exception):
390 This Exception is basically used for debugging
391 purposes, as the official MSN server should never
392 send anything _wrong_ and nobody in their right
393 mind would run their B{own} MSN server.
394 If it is raised by default command handlers
395 (handle_BLAH) the error will be logged.
402 I am the class used to represent an 'instant' message.
404 @ivar userHandle: The user handle (passport) of the sender
405 (this is only used when receiving a message)
406 @ivar screenName: The screen name of the sender (this is only used
407 when receiving a message)
408 @ivar message: The message
409 @ivar headers: The message headers
411 @ivar length: The message length (including headers and line endings)
412 @ivar ack: This variable is used to tell the server how to respond
413 once the message has been sent. If set to MESSAGE_ACK
414 (default) the server will respond with an ACK upon receiving
415 the message, if set to MESSAGE_NACK the server will respond
416 with a NACK upon failure to receive the message.
417 If set to MESSAGE_ACK_NONE the server will do nothing.
418 This is relevant for the return value of
419 SwitchboardClient.sendMessage (which will return
420 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
421 and will fire when the respective ACK or NACK is received).
422 If set to MESSAGE_ACK_NONE sendMessage will return None.
425 MESSAGE_ACK_FAT
= 'D'
427 MESSAGE_ACK_NONE
= 'U'
431 def __init__(self
, length
=0, userHandle
="", screenName
="", message
="", specialMessage
=False):
432 self
.userHandle
= userHandle
433 self
.screenName
= screenName
434 self
.specialMessage
= specialMessage
435 self
.message
= message
436 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain; charset=UTF-8'}
440 def _calcMessageLen(self
):
442 used to calculte the number to send
443 as the message length when sending a message.
445 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
447 def delHeader(self
, header
):
448 """ delete the desired header """
449 if self
.headers
.has_key(header
):
450 del self
.headers
[header
]
452 def setHeader(self
, header
, value
):
453 """ set the desired header """
454 self
.headers
[header
] = value
456 def getHeader(self
, header
):
458 get the desired header value
459 @raise KeyError: if no such header exists.
461 return self
.headers
[header
]
463 def hasHeader(self
, header
):
464 """ check to see if the desired header exists """
465 return self
.headers
.has_key(header
)
467 def getMessage(self
):
468 """ return the message - not including headers """
471 def setMessage(self
, message
):
472 """ set the message text """
473 self
.message
= message
478 Used to represent a MSNObject. This can be currently only be an avatar.
480 @ivar creator: The userHandle of the creator of this picture.
481 @ivar imageData: The PNG image data (only for our own avatar)
482 @ivar type: Always set to 3, for avatar.
483 @ivar size: The size of the image.
484 @ivar location: The filename of the image.
485 @ivar friendly: Unknown.
486 @ivar text: The textual representation of this MSNObject.
488 def __init__(self
, s
=""):
489 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
495 def setData(self
, creator
, imageData
):
496 """ Set the creator and imageData for this object """
497 self
.creator
= creator
498 self
.imageData
= imageData
499 self
.size
= len(imageData
)
501 self
.location
= "TMP" + str(random
.randint(1000,9999))
502 self
.friendly
= "AAA="
503 self
.sha1d
= b64enc(sha
.sha(imageData
).digest())
517 """ Makes a textual representation of this MSNObject. Stores it in self.text """
520 h
.append(self
.creator
)
522 h
.append(str(self
.size
))
524 h
.append(str(self
.type))
526 h
.append(self
.location
)
528 h
.append(self
.friendly
)
531 sha1c
= b64enc(sha
.sha("".join(h
)).digest())
532 self
.text
= '<msnobj Creator="%s" Size="%s" Type="%s" Location="%s" Friendly="%s" SHA1D="%s" SHA1C="%s"/>' % (self
.creator
, str(self
.size
), str(self
.type), self
.location
, self
.friendly
, self
.sha1d
, sha1c
)
535 e
= xmlw
.parseText(s
, True)
536 self
.creator
= e
.getAttribute("Creator")
537 self
.size
= int(e
.getAttribute("Size"))
538 self
.type = int(e
.getAttribute("Type"))
539 self
.location
= e
.getAttribute("Location")
540 self
.friendly
= e
.getAttribute("Friendly")
541 self
.sha1d
= e
.getAttribute("SHA1D")
548 This class represents a contact (user).
550 @ivar userGuid: The contact's user guid (unique string)
551 @ivar userHandle: The contact's user handle (passport).
552 @ivar screenName: The contact's screen name.
553 @ivar groups: A list of all the group IDs which this
555 @ivar lists: An integer representing the sum of all lists
556 that this contact belongs to.
557 @ivar caps: int, The capabilities of this client
558 @ivar msnobj: The MSNObject representing the contact's avatar
559 @ivar status: The contact's status code.
560 @type status: str if contact's status is known, None otherwise.
561 @ivar personal: The contact's personal message .
562 @type personal: str if contact's personal message is known, None otherwise.
564 @ivar homePhone: The contact's home phone number.
565 @type homePhone: str if known, otherwise None.
566 @ivar workPhone: The contact's work phone number.
567 @type workPhone: str if known, otherwise None.
568 @ivar mobilePhone: The contact's mobile phone number.
569 @type mobilePhone: str if known, otherwise None.
570 @ivar hasPager: Whether or not this user has a mobile pager
571 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
579 def __init__(self
, userGuid
="", userHandle
="", screenName
="", lists
=0, caps
=0, msnobj
=None, groups
={}, status
=None, personal
=""):
580 self
.userGuid
= userGuid
581 self
.userHandle
= userHandle
582 self
.screenName
= screenName
586 self
.msnobjGot
= True
587 self
.groups
= [] # if applicable
588 self
.status
= status
# current status
589 self
.personal
= personal
592 self
.homePhone
= None
593 self
.workPhone
= None
594 self
.mobilePhone
= None
598 def setPhone(self
, phoneType
, value
):
600 set phone numbers/values for this specific user.
601 for phoneType check the *_PHONE constants and HAS_PAGER
604 t
= phoneType
.upper()
605 if t
== HOME_PHONE
: self
.homePhone
= value
606 elif t
== WORK_PHONE
: self
.workPhone
= value
607 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
608 elif t
== HAS_PAGER
: self
.hasPager
= value
609 elif t
== HAS_BLOG
: self
.hasBlog
= value
610 #else: raise ValueError, "Invalid Phone Type: " + t
612 def addToList(self
, listType
):
614 Update the lists attribute to
615 reflect being part of the
618 self
.lists |
= listType
620 def removeFromList(self
, listType
):
622 Update the lists attribute to
623 reflect being removed from the
626 self
.lists ^
= listType
628 class MSNContactList
:
630 This class represents a basic MSN contact list.
632 @ivar contacts: All contacts on my various lists
633 @type contacts: dict (mapping user handles to MSNContact objects)
634 @ivar groups: a mapping of group ids to group names
635 (groups can only exist on the forward list)
639 This is used only for storage and doesn't effect the
640 server's contact list.
650 def _getContactsFromList(self
, listType
):
652 Obtain all contacts which belong
653 to the given list type.
655 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
657 def addContact(self
, contact
):
661 self
.contacts
[contact
.userHandle
] = contact
663 def remContact(self
, userHandle
):
668 del self
.contacts
[userHandle
]
669 except KeyError: pass
671 def getContact(self
, userHandle
):
673 Obtain the MSNContact object
674 associated with the given
676 @return: the MSNContact object if
677 the user exists, or None.
680 return self
.contacts
[userHandle
]
684 def getBlockedContacts(self
):
686 Obtain all the contacts on my block list
688 return self
._getContactsFromList
(BLOCK_LIST
)
690 def getAuthorizedContacts(self
):
692 Obtain all the contacts on my auth list.
693 (These are contacts which I have verified
694 can view my state changes).
696 return self
._getContactsFromList
(ALLOW_LIST
)
698 def getReverseContacts(self
):
700 Get all contacts on my reverse list.
701 (These are contacts which have added me
702 to their forward list).
704 return self
._getContactsFromList
(REVERSE_LIST
)
706 def getContacts(self
):
708 Get all contacts on my forward list.
709 (These are the contacts which I have added
712 return self
._getContactsFromList
(FORWARD_LIST
)
714 def setGroup(self
, id, name
):
716 Keep a mapping from the given id
719 self
.groups
[id] = name
721 def remGroup(self
, id):
723 Removed the stored group
724 mapping for the given id.
728 except KeyError: pass
729 for c
in self
.contacts
:
730 if id in c
.groups
: c
.groups
.remove(id)
733 class MSNEventBase(LineReceiver
):
735 This class provides support for handling / dispatching events and is the
736 base class of the three main client protocols (DispatchClient,
737 NotificationClient, SwitchboardClient)
741 self
.ids
= {} # mapping of ids to Deferreds
745 self
.currentMessage
= None
747 def connectionLost(self
, reason
):
751 def connectionMade(self
):
754 def _fireCallback(self
, id, *args
):
756 Fire the callback for the given id
757 if one exists and return 1, else return false
759 if self
.ids
.has_key(id):
760 self
.ids
[id][0].callback(args
)
765 def _nextTransactionID(self
):
766 """ return a usable transaction ID """
768 if self
.currentID
> 1000: self
.currentID
= 1
769 return self
.currentID
771 def _createIDMapping(self
, data
=None):
773 return a unique transaction ID that is mapped internally to a
774 deferred .. also store arbitrary data if it is needed
776 id = self
._nextTransactionID
()
778 self
.ids
[id] = (d
, data
)
781 def checkMessage(self
, message
):
783 process received messages to check for file invitations and
784 typing notifications and other control type messages
786 raise NotImplementedError
788 def sendLine(self
, line
):
789 if LINEDEBUG
: log
.msg("<< " + line
)
790 LineReceiver
.sendLine(self
, line
)
792 def lineReceived(self
, line
):
793 if LINEDEBUG
: log
.msg(">> " + line
)
794 if not self
.connected
: return
795 if self
.currentMessage
:
796 self
.currentMessage
.readPos
+= len(line
+"\r\n")
798 header
, value
= line
.split(':')
799 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
802 #raise MSNProtocolError, "Invalid Message Header"
804 if line
== "" or self
.currentMessage
.specialMessage
:
806 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
809 cmd
, params
= line
.split(' ', 1)
811 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
813 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
815 if self
.ids
.has_key(params
.split(' ')[0]):
816 self
.ids
[id].errback(int(cmd
))
819 else: # we received an error which doesn't map to a sent command
820 self
.gotError(int(cmd
))
823 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
825 try: handler(params
.split(' '))
826 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
828 self
.handle_UNKNOWN(cmd
, params
.split(' '))
830 def rawDataReceived(self
, data
):
831 if not self
.connected
: return
833 self
.currentMessage
.readPos
+= len(data
)
834 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
836 self
.currentMessage
.message
+= data
[:-diff
]
839 self
.currentMessage
.message
+= data
841 self
.currentMessage
.message
+= data
843 del self
.currentMessage
.readPos
844 m
= self
.currentMessage
845 self
.currentMessage
= None
846 if MESSAGEDEBUG
: log
.msg(m
.message
)
848 if not self
.checkMessage(m
):
849 self
.setLineMode(extra
)
852 self
.setLineMode(extra
)
855 self
.setLineMode(extra
)
857 ### protocol command handlers - no need to override these.
859 def handle_MSG(self
, params
):
860 checkParamLen(len(params
), 3, 'MSG')
862 messageLen
= int(params
[2])
863 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
864 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
866 def handle_UNKNOWN(self
, cmd
, params
):
867 """ implement me in subclasses if you want to handle unknown events """
868 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
872 def gotBadLine(self
, line
, why
):
873 """ called when a handler notifies me that this line is broken """
874 log
.msg('Error in line: %s (%s)' % (line
, why
))
876 def gotError(self
, errorCode
):
878 called when the server sends an error which is not in
879 response to a sent command (ie. it has no matching transaction ID)
881 log
.msg('Error %s' % (errorCodes
[errorCode
]))
884 class DispatchClient(MSNEventBase
):
886 This class provides support for clients connecting to the dispatch server
887 @ivar userHandle: your user handle (passport) needed before connecting.
890 def connectionMade(self
):
891 MSNEventBase
.connectionMade(self
)
892 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
894 ### protocol command handlers ( there is no need to override these )
896 def handle_VER(self
, params
):
897 versions
= params
[1:]
898 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
899 self
.transport
.loseConnection()
900 raise MSNProtocolError
, "Invalid version response"
901 id = self
._nextTransactionID
()
902 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.factory
.userHandle
))
904 def handle_CVR(self
, params
):
905 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
907 def handle_XFR(self
, params
):
908 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
909 id, refType
, addr
= params
[:3]
910 # was addr a host:port pair?
912 host
, port
= addr
.split(':')
917 self
.gotNotificationReferral(host
, int(port
))
921 def gotNotificationReferral(self
, host
, port
):
923 called when we get a referral to the notification server.
925 @param host: the notification server's hostname
926 @param port: the port to connect to
931 class DispatchFactory(ClientFactory
):
933 This class keeps the state for the DispatchClient.
935 @ivar userHandle: the userHandle to request a notification
938 protocol
= DispatchClient
943 class NotificationClient(MSNEventBase
):
945 This class provides support for clients connecting
946 to the notification server.
949 factory
= None # sssh pychecker
951 def __init__(self
, currentID
=0):
952 MSNEventBase
.__init
__(self
)
953 self
.currentID
= currentID
954 self
._state
= ['DISCONNECTED', {}]
956 self
.pingCheckTask
= None
957 self
.msnobj
= MSNObject()
959 def _setState(self
, state
):
960 self
._state
[0] = state
963 return self
._state
[0]
965 def _getStateData(self
, key
):
966 return self
._state
[1][key
]
968 def _setStateData(self
, key
, value
):
969 self
._state
[1][key
] = value
971 def _remStateData(self
, *args
):
972 for key
in args
: del self
._state
[1][key
]
974 def connectionMade(self
):
975 MSNEventBase
.connectionMade(self
)
976 self
._setState
('CONNECTED')
977 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
978 self
.factory
.resetDelay()
980 def connectionLost(self
, reason
):
981 self
._setState
('DISCONNECTED')
983 if self
.pingCheckTask
:
984 self
.pingCheckTask
.stop()
985 self
.pingCheckTask
= None
986 MSNEventBase
.connectionLost(self
, reason
)
988 def _getEmailFields(self
, message
):
989 fields
= message
.getMessage().strip().split('\n')
993 if len(a
) != 2: continue
1000 def _gotInitialEmailNotification(self
, message
):
1001 values
= self
._getEmailFields
(message
)
1003 inboxunread
= int(values
["Inbox-Unread"])
1004 foldersunread
= int(values
["Folders-Unread"])
1007 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
1008 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
1010 def _gotEmailNotification(self
, message
):
1011 values
= self
._getEmailFields
(message
)
1013 mailfrom
= values
["From"]
1014 fromaddr
= values
["From-Addr"]
1015 subject
= values
["Subject"]
1016 junkbeginning
= "=?\"us-ascii\"?Q?"
1018 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
1020 # If any of the fields weren't found then it's not a big problem. We just ignore the message
1022 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
1024 def _gotMSNAlert(self
, message
):
1025 notification
= xmlw
.parseText(message
.message
, beExtremelyLenient
=True)
1026 siteurl
= notification
.getAttribute("siteurl")
1027 notid
= notification
.getAttribute("id")
1030 for e
in notification
.elements():
1036 msgid
= msg
.getAttribute("id")
1041 for e
in msg
.elements():
1042 if e
.name
== "ACTION":
1043 action
= e
.getAttribute("url")
1044 if e
.name
== "SUBSCR":
1045 subscr
= e
.getAttribute("url")
1046 if e
.name
== "BODY":
1047 for e2
in e
.elements():
1048 if e2
.name
== "TEXT":
1049 bodytext
= e2
.__str__()
1050 if not (action
and subscr
and bodytext
): return
1052 actionurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (action
, notid
, msgid
) # Used to have $siteurl// at the beginning, but it seems to not work with that now. Weird
1053 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
1055 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
1057 def _gotUBX(self
, message
):
1058 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
1059 if not msnContact
: return
1060 lm
= message
.message
.lower()
1061 p1
= lm
.find("<psm>") + 5
1062 p2
= lm
.find("</psm>")
1063 if p1
>= 0 and p2
>= 0:
1064 personal
= xmlw
.unescapeFromXml(message
.message
[p1
:p2
])
1065 msnContact
.personal
= personal
1066 self
.contactPersonalChanged(message
.userHandle
, personal
)
1068 msnContact
.personal
= ''
1069 self
.contactPersonalChanged(message
.userHandle
, '')
1071 def checkMessage(self
, message
):
1072 """ hook used for detecting specific notification messages """
1073 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
1074 if 'text/x-msmsgsprofile' in cTypes
:
1075 self
.gotProfile(message
)
1077 elif "text/x-msmsgsinitialemailnotification" in cTypes
:
1078 self
._gotInitialEmailNotification
(message
)
1080 elif "text/x-msmsgsemailnotification" in cTypes
:
1081 self
._gotEmailNotification
(message
)
1083 elif "NOTIFICATION" == message
.userHandle
and message
.specialMessage
== True:
1084 self
._gotMSNAlert
(message
)
1086 elif "UBX" == message
.screenName
and message
.specialMessage
== True:
1087 self
._gotUBX
(message
)
1091 ### protocol command handlers - no need to override these
1093 def handle_VER(self
, params
):
1094 versions
= params
[1:]
1095 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
1096 self
.transport
.loseConnection()
1097 raise MSNProtocolError
, "Invalid version response"
1098 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
1100 def handle_CVR(self
, params
):
1101 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
1103 def handle_USR(self
, params
):
1104 if not (4 <= len(params
) <= 6):
1105 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
1107 mechanism
= params
[1]
1108 if mechanism
== "OK":
1109 self
.loggedIn(params
[2], int(params
[3]))
1110 elif params
[2].upper() == "S":
1111 # we need to obtain auth from a passport server
1113 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3])
1114 d
.addCallback(self
._passportLogin
)
1115 d
.addErrback(self
._passportError
)
1117 def _passportLogin(self
, result
):
1118 if result
[0] == LOGIN_REDIRECT
:
1119 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
1120 result
[1], cached
=1, authData
=result
[2])
1121 d
.addCallback(self
._passportLogin
)
1122 d
.addErrback(self
._passportError
)
1123 elif result
[0] == LOGIN_SUCCESS
:
1124 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
1125 elif result
[0] == LOGIN_FAILURE
:
1126 self
.loginFailure(result
[1])
1128 def _passportError(self
, failure
):
1129 self
.loginFailure("Exception while authenticating: %s" % failure
)
1131 def handle_CHG(self
, params
):
1133 if not self
._fireCallback
(id, params
[1]):
1134 if self
.factory
: self
.factory
.status
= params
[1]
1135 self
.statusChanged(params
[1])
1137 def handle_ILN(self
, params
):
1138 #checkParamLen(len(params), 6, 'ILN')
1139 msnContact
= self
.factory
.contacts
.getContact(params
[2])
1140 if not msnContact
: return
1141 msnContact
.status
= params
[1]
1142 msnContact
.screenName
= unquote(params
[3])
1143 if len(params
) > 4: msnContact
.caps
= int(params
[4])
1145 self
.handleAvatarHelper(msnContact
, params
[5])
1147 self
.handleAvatarGoneHelper(msnContact
)
1148 self
.gotContactStatus(params
[2], params
[1], unquote(params
[3]))
1150 def handleAvatarGoneHelper(self
, msnContact
):
1151 if msnContact
.msnobj
:
1152 msnContact
.msnobj
= None
1153 msnContact
.msnobjGot
= True
1154 self
.contactAvatarChanged(msnContact
.userHandle
, "")
1156 def handleAvatarHelper(self
, msnContact
, msnobjStr
):
1157 msnobj
= MSNObject(unquote(msnobjStr
))
1158 if not msnContact
.msnobj
or msnobj
.sha1d
!= msnContact
.msnobj
.sha1d
:
1159 if MSNP2PDEBUG
: log
.msg("Updated MSNObject received!" + msnobjStr
)
1160 msnContact
.msnobj
= msnobj
1161 msnContact
.msnobjGot
= False
1162 self
.contactAvatarChanged(msnContact
.userHandle
, binascii
.hexlify(b64dec(msnContact
.msnobj
.sha1d
)))
1164 def handle_CHL(self
, params
):
1165 checkParamLen(len(params
), 2, 'CHL')
1166 response
= msnp11chl
.doChallenge(params
[1])
1167 self
.sendLine("QRY %s %s %s" % (self
._nextTransactionID
(), msnp11chl
.MSNP11_PRODUCT_ID
, len(response
)))
1168 self
.transport
.write(response
)
1170 def handle_QRY(self
, params
):
1173 def handle_NLN(self
, params
):
1174 if not self
.factory
: return
1175 msnContact
= self
.factory
.contacts
.getContact(params
[1])
1176 if not msnContact
: return
1177 msnContact
.status
= params
[0]
1178 msnContact
.screenName
= unquote(params
[2])
1179 if len(params
) > 3: msnContact
.caps
= int(params
[3])
1181 self
.handleAvatarHelper(msnContact
, params
[4])
1183 self
.handleAvatarGoneHelper(msnContact
)
1184 self
.contactStatusChanged(params
[1], params
[0], unquote(params
[2]))
1186 def handle_FLN(self
, params
):
1187 checkParamLen(len(params
), 1, 'FLN')
1188 msnContact
= self
.factory
.contacts
.getContact(params
[0])
1190 msnContact
.status
= STATUS_OFFLINE
1191 self
.contactOffline(params
[0])
1193 def handle_LST(self
, params
):
1194 if self
._getState
() != 'SYNC': return
1196 userHandle
, screenName
, userGuid
, lists
, groups
= getVals(params
)
1198 if not userHandle
or lists
< 1:
1199 raise MSNProtocolError
, "Unknown LST " + str(params
) # debug
1200 contact
= MSNContact(userGuid
, userHandle
, screenName
, lists
)
1201 if contact
.lists
& FORWARD_LIST
:
1202 contact
.groups
.extend(map(str, groups
))
1203 self
._getStateData
('list').addContact(contact
)
1204 self
._setStateData
('last_contact', contact
)
1205 sofar
= self
._getStateData
('lst_sofar') + 1
1206 if sofar
== self
._getStateData
('lst_reply'):
1207 # this is the best place to determine that
1208 # a syn realy has finished - msn _may_ send
1209 # BPR information for the last contact
1210 # which is unfortunate because it means
1211 # that the real end of a syn is non-deterministic.
1212 # to handle this we'll keep 'last_contact' hanging
1213 # around in the state data and update it if we need
1215 self
._setState
('SESSION')
1216 contacts
= self
._getStateData
('list')
1217 phone
= self
._getStateData
('phone')
1218 id = self
._getStateData
('synid')
1219 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1220 self
._fireCallback
(id, contacts
, phone
)
1222 self
._setStateData
('lst_sofar',sofar
)
1224 def handle_BLP(self
, params
):
1225 # check to see if this is in response to a SYN
1226 if self
._getState
() == 'SYNC':
1227 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
1230 self
.factory
.contacts
.privacy
= listCodeToID
[params
[1].lower()]
1231 self
._fireCallback
(id, params
[1])
1233 def handle_GTC(self
, params
):
1234 # check to see if this is in response to a SYN
1235 if self
._getState
() == 'SYNC':
1236 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
1237 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
1238 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1241 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
1242 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
1243 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1245 def handle_SYN(self
, params
):
1247 self
._setStateData
('phone', []) # Always needs to be set
1248 if params
[3] == 0: # No LST will be received. New account?
1249 self
._setState
('SESSION')
1250 self
._fireCallback
(id, None, None)
1252 contacts
= MSNContactList()
1253 self
._setStateData
('list', contacts
)
1254 self
._setStateData
('lst_reply', int(params
[3]))
1255 self
._setStateData
('lsg_reply', int(params
[4]))
1256 self
._setStateData
('lst_sofar', 0)
1258 def handle_LSG(self
, params
):
1259 if self
._getState
() == 'SYNC':
1260 self
._getStateData
('list').groups
[params
[1]] = unquote(params
[0])
1262 def handle_PRP(self
, params
):
1263 if params
[1] == "MFN":
1264 self
._fireCallback
(int(params
[0]))
1265 elif self
._getState
() == 'SYNC':
1266 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
1268 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
1270 def handle_BPR(self
, params
):
1271 numParams
= len(params
)
1272 if numParams
== 2: # part of a syn
1273 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
1274 elif numParams
== 4:
1275 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_BPR called with no contact list" # debug
1276 self
.factory
.contacts
.version
= int(params
[0])
1277 userHandle
, phoneType
, number
= params
[1], params
[2], unquote(params
[3])
1278 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1279 self
.gotPhoneNumber(userHandle
, phoneType
, number
)
1282 def handle_ADG(self
, params
):
1283 checkParamLen(len(params
), 5, 'ADG')
1285 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
1286 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
1288 def handle_RMG(self
, params
):
1289 checkParamLen(len(params
), 3, 'RMG')
1291 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
1292 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
1294 def handle_REG(self
, params
):
1295 checkParamLen(len(params
), 5, 'REG')
1297 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
1298 raise MSNProtocolError
, "REG response does not match up to a request" # debug
1300 def handle_ADC(self
, params
):
1301 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_ADC called with no contact list"
1302 numParams
= len(params
)
1303 if numParams
< 3 or params
[1].upper() not in ('AL','BL','RL','FL','PL'):
1304 raise MSNProtocolError
, "Invalid Paramaters for ADC" # debug
1306 listType
= params
[1].lower()
1307 userHandle
, screenName
, userGuid
, ignored1
, groups
= getVals(params
[2:])
1309 if groups
and listType
.upper() != FORWARD_LIST
:
1310 raise MSNProtocolError
, "Only forward list can contain groups" # debug
1312 if not self
._fireCallback
(id, listCodeToID
[listType
], userGuid
, userHandle
, screenName
):
1313 c
= self
.factory
.contacts
.getContact(userHandle
)
1315 c
= MSNContact(userGuid
=userGuid
, userHandle
=userHandle
, screenName
=screenName
)
1316 self
.factory
.contacts
.addContact(c
)
1317 c
.addToList(PENDING_LIST
)
1318 self
.userAddedMe(userGuid
, userHandle
, screenName
)
1320 def handle_REM(self
, params
):
1321 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_REM called with no contact list available!"
1322 numParams
= len(params
)
1323 if numParams
< 3 or params
[1].upper() not in ('AL','BL','FL','RL','PL'):
1324 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1326 listType
= params
[1].lower()
1327 userHandle
= params
[2]
1330 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1331 groupID
= int(params
[3])
1332 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, groupID
):
1333 if listType
.upper() != "RL": return
1334 c
= self
.factory
.contacts
.getContact(userHandle
)
1336 c
.removeFromList(REVERSE_LIST
)
1337 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1338 self
.userRemovedMe(userHandle
)
1340 def handle_XFR(self
, params
):
1341 checkParamLen(len(params
), 5, 'XFR')
1343 # check to see if they sent a host/port pair
1345 host
, port
= params
[2].split(':')
1350 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1351 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1353 def handle_RNG(self
, params
):
1354 checkParamLen(len(params
), 6, 'RNG')
1355 # check for host:port pair
1357 host
, port
= params
[1].split(":")
1362 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1365 def handle_NOT(self
, params
):
1366 checkParamLen(len(params
), 1, 'NOT')
1368 messageLen
= int(params
[0])
1369 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1370 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", specialMessage
=True)
1373 def handle_UBX(self
, params
):
1374 checkParamLen(len(params
), 2, 'UBX')
1376 messageLen
= int(params
[1])
1377 except ValueError: raise MSNProtocolError
, "Invalid Parameter for UBX length argument"
1379 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
="UBX", specialMessage
=True)
1382 self
._gotUBX
(MSNMessage(userHandle
=params
[0]))
1384 def handle_UUX(self
, params
):
1385 checkParamLen(len(params
), 2, 'UUX')
1386 if params
[1] != '0': return
1388 self
._fireCallback
(id)
1390 def handle_OUT(self
, params
):
1391 checkParamLen(len(params
), 1, 'OUT')
1392 self
.factory
.stopTrying()
1393 if params
[0] == "OTH": self
.multipleLogin()
1394 elif params
[0] == "SSD": self
.serverGoingDown()
1395 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1397 def handle_QNG(self
, params
):
1398 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1402 def pingChecker(self
):
1403 if self
.pingCounter
> 5:
1404 # The server has ignored 5 pings, lets kill the connection
1405 self
.transport
.loseConnection()
1407 self
.sendLine("PNG")
1408 self
.pingCounter
+= 1
1410 def pingCheckerStart(self
, *args
):
1411 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1412 self
.pingCheckTask
.start(PINGSPEED
)
1414 def loggedIn(self
, userHandle
, verified
):
1416 Called when the client has logged in.
1417 The default behaviour of this method is to
1418 update the factory with our screenName and
1419 to sync the contact list (factory.contacts).
1420 When this is complete self.listSynchronized
1423 @param userHandle: our userHandle
1424 @param verified: 1 if our passport has been (verified), 0 if not.
1425 (i'm not sure of the significace of this)
1429 d
.addCallback(self
.listSynchronized
)
1430 d
.addCallback(self
.pingCheckerStart
)
1432 def loginFailure(self
, message
):
1434 Called when the client fails to login.
1436 @param message: a message indicating the problem that was encountered
1440 def gotProfile(self
, message
):
1442 Called after logging in when the server sends an initial
1443 message with MSN/passport specific profile information
1444 such as country, number of kids, etc.
1445 Check the message headers for the specific values.
1447 @param message: The profile message
1451 def listSynchronized(self
, *args
):
1453 Lists are now synchronized by default upon logging in, this
1454 method is called after the synchronization has finished
1455 and the factory now has the up-to-date contacts.
1459 def contactAvatarChanged(self
, userHandle
, hash):
1461 Called when we receive the first, or a new <msnobj/> from a
1464 @param userHandle: contact who's msnobj has been changed
1465 @param hash: sha1 hash of their avatar as hex string
1468 def statusChanged(self
, statusCode
):
1470 Called when our status changes and its not in response to a
1473 @param statusCode: 3-letter status code
1477 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
1479 Called when we receive a list of statuses upon login.
1481 @param userHandle: the contact's user handle (passport)
1482 @param statusCode: 3-letter status code
1483 @param screenName: the contact's screen name
1487 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
1489 Called when we're notified that a contact's status has changed.
1491 @param userHandle: the contact's user handle (passport)
1492 @param statusCode: 3-letter status code
1493 @param screenName: the contact's screen name
1497 def contactPersonalChanged(self
, userHandle
, personal
):
1499 Called when a contact's personal message changes.
1501 @param userHandle: the contact who changed their personal message
1502 @param personal : the new personal message
1506 def contactOffline(self
, userHandle
):
1508 Called when a contact goes offline.
1510 @param userHandle: the contact's user handle
1514 def gotMessage(self
, message
):
1516 Called when there is a message from the notification server
1517 that is not understood by default.
1519 @param message: the MSNMessage.
1523 def gotMSNAlert(self
, body
, action
, subscr
):
1525 Called when the server sends an MSN Alert (http://alerts.msn.com)
1527 @param body : the alert text
1528 @param action: a URL with more information for the user to view
1529 @param subscr: a URL the user can use to modify their alert subscription
1533 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1535 Called when the server sends you details about your hotmail
1536 inbox. This is only ever called once, on login.
1538 @param inboxunread : the number of unread items in your inbox
1539 @param foldersunread: the number of unread items in other folders
1543 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1545 Called when the server sends us realtime email
1546 notification. This means that you have received
1547 a new email in your hotmail inbox.
1549 @param mailfrom: the sender of the email
1550 @param fromaddr: the sender of the email (I don't know :P)
1551 @param subject : the email subject
1555 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1557 Called when the server sends us phone details about
1558 a specific user (for example after a user is added
1559 the server will send their status, phone details etc.
1561 @param userHandle: the contact's user handle (passport)
1562 @param phoneType: the specific phoneType
1563 (*_PHONE constants or HAS_PAGER)
1564 @param number: the value/phone number.
1568 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1570 Called when a user adds me to their list. (ie. they have been added to
1573 @param userHandle: the userHandle of the user
1574 @param screenName: the screen name of the user
1578 def userRemovedMe(self
, userHandle
):
1580 Called when a user removes us from their contact list
1581 (they are no longer on our reverseContacts list.
1583 @param userHandle: the contact's user handle (passport)
1587 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1588 key
, userHandle
, screenName
):
1590 Called when we get an invitation to a switchboard server.
1591 This happens when a user requests a chat session with us.
1593 @param sessionID: session ID number, must be remembered for logging in
1594 @param host: the hostname of the switchboard server
1595 @param port: the port to connect to
1596 @param key: used for authorization when connecting
1597 @param userHandle: the user handle of the person who invited us
1598 @param screenName: the screen name of the person who invited us
1602 def multipleLogin(self
):
1604 Called when the server says there has been another login
1605 under our account, the server should disconnect us right away.
1609 def serverGoingDown(self
):
1611 Called when the server has notified us that it is going down for
1618 def changeStatus(self
, status
):
1620 Change my current status. This method will add
1621 a default callback to the returned Deferred
1622 which will update the status attribute of the
1625 @param status: 3-letter status code (as defined by
1626 the STATUS_* constants)
1627 @return: A Deferred, the callback of which will be
1628 fired when the server confirms the change
1629 of status. The callback argument will be
1630 a tuple with the new status code as the
1634 id, d
= self
._createIDMapping
()
1635 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1637 self
.factory
.status
= r
[0]
1639 return d
.addCallback(_cb
)
1641 def setPrivacyMode(self
, privLevel
):
1643 Set my privacy mode on the server.
1646 This only keeps the current privacy setting on
1647 the server for later retrieval, it does not
1648 effect the way the server works at all.
1650 @param privLevel: This parameter can be true, in which
1651 case the server will keep the state as
1652 'al' which the official client interprets
1653 as -> allow messages from only users on
1654 the allow list. Alternatively it can be
1655 false, in which case the server will keep
1656 the state as 'bl' which the official client
1657 interprets as -> allow messages from all
1658 users except those on the block list.
1660 @return: A Deferred, the callback of which will be fired when
1661 the server replies with the new privacy setting.
1662 The callback argument will be a tuple, the only element
1663 of which being either 'al' or 'bl' (the new privacy setting).
1666 id, d
= self
._createIDMapping
()
1667 if privLevel
: self
.sendLine("BLP %s AL" % id)
1668 else: self
.sendLine("BLP %s BL" % id)
1673 Used for keeping an up-to-date contact list.
1674 A callback is added to the returned Deferred
1675 that updates the contact list on the factory
1676 and also sets my state to STATUS_ONLINE.
1679 This is called automatically upon signing
1680 in using the version attribute of
1681 factory.contacts, so you may want to persist
1682 this object accordingly. Because of this there
1683 is no real need to ever call this method
1686 @return: A Deferred, the callback of which will be
1687 fired when the server sends an adequate reply.
1688 The callback argument will be a tuple with two
1689 elements, the new list (MSNContactList) and
1690 your current state (a dictionary). If the version
1691 you sent _was_ the latest list version, both elements
1692 will be None. To just request the list send a version of 0.
1695 self
._setState
('SYNC')
1696 id, d
= self
._createIDMapping
(data
=None)
1697 self
._setStateData
('synid',id)
1698 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1700 self
.changeStatus(STATUS_ONLINE
)
1701 if r
[0] is not None:
1702 self
.factory
.contacts
= r
[0]
1704 return d
.addCallback(_cb
)
1706 def setPhoneDetails(self
, phoneType
, value
):
1708 Set/change my phone numbers stored on the server.
1710 @param phoneType: phoneType can be one of the following
1711 constants - HOME_PHONE, WORK_PHONE,
1712 MOBILE_PHONE, HAS_PAGER.
1713 These are pretty self-explanatory, except
1714 maybe HAS_PAGER which refers to whether or
1715 not you have a pager.
1716 @param value: for all of the *_PHONE constants the value is a
1717 phone number (str), for HAS_PAGER accepted values
1718 are 'Y' (for yes) and 'N' (for no).
1720 @return: A Deferred, the callback for which will be fired when
1721 the server confirms the change has been made. The
1722 callback argument will be a tuple with 2 elements, the
1723 first being the new list version (int) and the second
1724 being the new phone number value (str).
1726 raise "ProbablyDoesntWork"
1727 # XXX: Add a default callback which updates
1728 # factory.contacts.version and the relevant phone
1730 id, d
= self
._createIDMapping
()
1731 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1734 def addListGroup(self
, name
):
1736 Used to create a new list group.
1737 A default callback is added to the
1738 returned Deferred which updates the
1739 contacts attribute of the factory.
1741 @param name: The desired name of the new group.
1743 @return: A Deferred, the callbacck for which will be called
1744 when the server clarifies that the new group has been
1745 created. The callback argument will be a tuple with 3
1746 elements: the new list version (int), the new group name
1747 (str) and the new group ID (int).
1750 raise "ProbablyDoesntWork"
1751 id, d
= self
._createIDMapping
()
1752 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1754 if self
.factory
.contacts
:
1755 self
.factory
.contacts
.version
= r
[0]
1756 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1758 return d
.addCallback(_cb
)
1760 def remListGroup(self
, groupID
):
1762 Used to remove a list group.
1763 A default callback is added to the
1764 returned Deferred which updates the
1765 contacts attribute of the factory.
1767 @param groupID: the ID of the desired group to be removed.
1769 @return: A Deferred, the callback for which will be called when
1770 the server clarifies the deletion of the group.
1771 The callback argument will be a tuple with 2 elements:
1772 the new list version (int) and the group ID (int) of
1776 raise "ProbablyDoesntWork"
1777 id, d
= self
._createIDMapping
()
1778 self
.sendLine("RMG %s %s" % (id, groupID
))
1780 self
.factory
.contacts
.version
= r
[0]
1781 self
.factory
.contacts
.remGroup(r
[1])
1783 return d
.addCallback(_cb
)
1785 def renameListGroup(self
, groupID
, newName
):
1787 Used to rename an existing list group.
1788 A default callback is added to the returned
1789 Deferred which updates the contacts attribute
1792 @param groupID: the ID of the desired group to rename.
1793 @param newName: the desired new name for the group.
1795 @return: A Deferred, the callback for which will be called
1796 when the server clarifies the renaming.
1797 The callback argument will be a tuple of 3 elements,
1798 the new list version (int), the group id (int) and
1799 the new group name (str).
1802 raise "ProbablyDoesntWork"
1803 id, d
= self
._createIDMapping
()
1804 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1806 self
.factory
.contacts
.version
= r
[0]
1807 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1809 return d
.addCallback(_cb
)
1811 def addContact(self
, listType
, userHandle
):
1813 Used to add a contact to the desired list.
1814 A default callback is added to the returned
1815 Deferred which updates the contacts attribute of
1816 the factory with the new contact information.
1817 If you are adding a contact to the forward list
1818 and you want to associate this contact with multiple
1819 groups then you will need to call this method for each
1820 group you would like to add them to, changing the groupID
1821 parameter. The default callback will take care of updating
1822 the group information on the factory's contact list.
1824 @param listType: (as defined by the *_LIST constants)
1825 @param userHandle: the user handle (passport) of the contact
1828 @return: A Deferred, the callback for which will be called when
1829 the server has clarified that the user has been added.
1830 The callback argument will be a tuple with 4 elements:
1831 the list type, the contact's user handle, the new list
1832 version, and the group id (if relevant, otherwise it
1836 id, d
= self
._createIDMapping
()
1837 try: # Make sure the contact isn't actually on the list
1838 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1839 except AttributeError: pass
1840 listType
= listIDToCode
[listType
].upper()
1841 if listType
== "FL":
1842 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1844 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1847 if not self
.factory
: return
1848 c
= self
.factory
.contacts
.getContact(r
[2])
1850 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1851 self
.factory
.contacts
.addContact(c
)
1852 #if r[3]: c.groups.append(r[3])
1855 return d
.addCallback(_cb
)
1857 def remContact(self
, listType
, userHandle
):
1859 Used to remove a contact from the desired list.
1860 A default callback is added to the returned deferred
1861 which updates the contacts attribute of the factory
1862 to reflect the new contact information.
1864 @param listType: (as defined by the *_LIST constants)
1865 @param userHandle: the user handle (passport) of the
1866 contact being removed
1868 @return: A Deferred, the callback for which will be called when
1869 the server has clarified that the user has been removed.
1870 The callback argument will be a tuple of 3 elements:
1871 the list type, the contact's user handle and the group ID
1872 (if relevant, otherwise it will be None)
1875 id, d
= self
._createIDMapping
()
1876 try: # Make sure the contact is actually on this list
1877 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1878 except AttributeError: return
1879 listType
= listIDToCode
[listType
].upper()
1880 if listType
== "FL":
1882 c
= self
.factory
.contacts
.getContact(userHandle
)
1883 userGuid
= c
.userGuid
1884 except AttributeError: return
1885 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1887 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1890 if listType
== "FL":
1891 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1892 l
= self
.factory
.contacts
1893 c
= l
.getContact(r
[1])
1897 if group
: # they may not have been removed from the list
1898 c
.groups
.remove(group
)
1899 if c
.groups
: shouldRemove
= 0
1901 c
.removeFromList(r
[0])
1902 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1904 return d
.addCallback(_cb
)
1906 def changeScreenName(self
, newName
):
1908 Used to change your current screen name.
1909 A default callback is added to the returned
1910 Deferred which updates the screenName attribute
1911 of the factory and also updates the contact list
1914 @param newName: the new screen name
1916 @return: A Deferred, the callback for which will be called
1917 when the server acknowledges the change.
1918 The callback argument will be an empty tuple.
1921 id, d
= self
._createIDMapping
()
1922 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1924 self
.factory
.screenName
= newName
1926 return d
.addCallback(_cb
)
1928 def changePersonalMessage(self
, personal
):
1930 Used to change your personal message.
1932 @param personal: the new screen name
1934 @return: A Deferred, the callback for which will be called
1935 when the server acknowledges the change.
1936 The callback argument will be a tuple of 1 element,
1937 the personal message.
1940 id, d
= self
._createIDMapping
()
1943 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1944 self
.sendLine("UUX %s %s" % (id, len(data
)))
1945 self
.transport
.write(data
)
1947 self
.factory
.personal
= personal
1949 return d
.addCallback(_cb
)
1951 def changeAvatar(self
, imageData
, push
):
1953 Used to change the avatar that other users see.
1955 @param imageData: the PNG image data to set as the avatar
1956 @param push : whether to push the update to the server
1957 (it will otherwise be sent with the next
1960 @return: If push==True, a Deferred, the callback for which
1961 will be called when the server acknowledges the change.
1962 The callback argument will be the same as for changeStatus.
1965 if self
.msnobj
and imageData
== self
.msnobj
.imageData
: return
1967 self
.msnobj
.setData(self
.factory
.userHandle
, imageData
)
1969 self
.msnobj
.setNull()
1970 if push
: return self
.changeStatus(self
.factory
.status
) # Push to server
1973 def requestSwitchboardServer(self
):
1975 Used to request a switchboard server to use for conversations.
1977 @return: A Deferred, the callback for which will be called when
1978 the server responds with the switchboard information.
1979 The callback argument will be a tuple with 3 elements:
1980 the host of the switchboard server, the port and a key
1981 used for logging in.
1984 id, d
= self
._createIDMapping
()
1985 self
.sendLine("XFR %s SB" % id)
1990 Used to log out of the notification server.
1991 After running the method the server is expected
1992 to close the connection.
1995 if self
.pingCheckTask
:
1996 self
.pingCheckTask
.stop()
1997 self
.pingCheckTask
= None
1998 self
.factory
.stopTrying()
1999 self
.sendLine("OUT")
2000 self
.transport
.loseConnection()
2002 class NotificationFactory(ReconnectingClientFactory
):
2004 Factory for the NotificationClient protocol.
2005 This is basically responsible for keeping
2006 the state of the client and thus should be used
2007 in a 1:1 situation with clients.
2009 @ivar contacts: An MSNContactList instance reflecting
2010 the current contact list -- this is
2011 generally kept up to date by the default
2013 @ivar userHandle: The client's userHandle, this is expected
2014 to be set by the client and is used by the
2015 protocol (for logging in etc).
2016 @ivar screenName: The client's current screen-name -- this is
2017 generally kept up to date by the default
2019 @ivar password: The client's password -- this is (obviously)
2020 expected to be set by the client.
2021 @ivar passportServer: This must point to an msn passport server
2022 (the whole URL is required)
2023 @ivar status: The status of the client -- this is generally kept
2024 up to date by the default command handlers
2025 @ivar maxRetries: The number of times the factory will reconnect
2026 if the connection dies because of a network error.
2033 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
2035 protocol
= NotificationClient
2039 class SwitchboardClient(MSNEventBase
):
2041 This class provides support for clients connecting to a switchboard server.
2043 Switchboard servers are used for conversations with other people
2044 on the MSN network. This means that the number of conversations at
2045 any given time will be directly proportional to the number of
2046 connections to varioius switchboard servers.
2048 MSN makes no distinction between single and group conversations,
2049 so any number of users may be invited to join a specific conversation
2050 taking place on a switchboard server.
2052 @ivar key: authorization key, obtained when receiving
2053 invitation / requesting switchboard server.
2054 @ivar userHandle: your user handle (passport)
2055 @ivar sessionID: unique session ID, used if you are replying
2056 to a switchboard invitation
2057 @ivar reply: set this to 1 in connectionMade or before to signifiy
2058 that you are replying to a switchboard invitation.
2059 @ivar msnobj: the MSNObject for the user's avatar. So that the
2060 switchboard can distribute it to anyone who asks.
2072 MSNEventBase
.__init
__(self
)
2073 self
.pendingUsers
= {}
2074 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
2077 def connectionMade(self
):
2078 MSNEventBase
.connectionMade(self
)
2081 def connectionLost(self
, reason
):
2082 self
.cookies
['iCookies'] = {}
2083 MSNEventBase
.connectionLost(self
, reason
)
2085 def _sendInit(self
):
2087 send initial data based on whether we are replying to an invitation
2090 id = self
._nextTransactionID
()
2092 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
2094 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
2096 def _newInvitationCookie(self
):
2098 if self
._iCookie
> 1000: self
._iCookie
= 1
2099 return self
._iCookie
2101 def _checkTyping(self
, message
, cTypes
):
2102 """ helper method for checkMessage """
2103 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
2104 self
.gotContactTyping(message
)
2107 def _checkFileInvitation(self
, message
, info
):
2108 """ helper method for checkMessage """
2109 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
2111 cookie
= info
['Invitation-Cookie']
2112 filename
= info
['Application-File']
2113 filesize
= int(info
['Application-FileSize'])
2114 connectivity
= (info
.get('Connectivity', 'n').lower() == 'y')
2116 log
.msg('Received munged file transfer request ... ignoring.')
2118 raise NotImplementedError
2119 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
2122 def _handleP2PMessage(self
, message
):
2123 """ helper method for msnslp messages (file transfer & avatars) """
2124 if not message
.getHeader("P2P-Dest") == self
.userHandle
: return
2125 packet
= message
.message
2126 binaryFields
= BinaryFields(packet
=packet
)
2127 if binaryFields
[5] == BinaryFields
.BYEGOT
:
2128 pass # Ignore the ACKs to SLP messages
2129 elif binaryFields
[0] != 0:
2130 slpLink
= self
.slpLinks
.get(binaryFields
[0])
2132 # Link has been killed. Ignore
2134 if slpLink
.remoteUser
== message
.userHandle
:
2135 slpLink
.handlePacket(packet
)
2136 elif binaryFields
[5] == BinaryFields
.ACK
:
2137 pass # Ignore the ACKs to SLP messages
2139 slpMessage
= MSNSLPMessage(packet
)
2141 # Always try and give a slpMessage to a slpLink first.
2142 # If none can be found, and it was INVITE, then create
2143 # one to handle the session.
2144 for slpLink
in self
.slpLinks
.values():
2145 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
2146 slpLink
.handleSLPMessage(slpMessage
)
2149 slpLink
= None # Was not handled
2151 if not slpLink
and slpMessage
.method
== "INVITE":
2152 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
2153 context
= FileContext(slpMessage
.context
)
2154 slpLink
= SLPLink_FileReceive(remoteUser
=slpMessage
.fro
, switchboard
=self
, filename
=context
.filename
, filesize
=context
.filesize
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
, branch
=slpMessage
.branch
)
2155 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2156 self
.gotFileReceive(slpLink
)
2157 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
2158 # Check that we have an avatar to send
2160 slpLink
= SLPLink_AvatarSend(remoteUser
=slpMessage
.fro
, switchboard
=self
, filesize
=self
.msnobj
.size
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2161 slpLink
.write(self
.msnobj
.imageData
)
2164 # They shouldn't have sent a request if we have
2165 # no avatar. So we'll just ignore them.
2166 # FIXME We should really send an error
2169 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2171 # Always need to ACK these packets if we can
2172 slpLink
.sendP2PACK(binaryFields
)
2175 def checkMessage(self
, message
):
2177 hook for detecting any notification type messages
2178 (e.g. file transfer)
2180 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2181 if self
._checkTyping
(message
, cTypes
): return 0
2182 if 'text/x-msmsgsinvite' in cTypes
:
2183 # header like info is sent as part of the message body.
2185 for line
in message
.message
.split('\r\n'):
2187 key
, val
= line
.split(':')
2188 info
[key
] = val
.lstrip()
2189 except ValueError: continue
2190 if self
._checkFileInvitation
(message
, info
): return 0
2191 elif 'application/x-msnmsgrp2p' in cTypes
:
2192 self
._handleP
2PMessage
(message
)
2197 def handle_USR(self
, params
):
2198 checkParamLen(len(params
), 4, 'USR')
2199 if params
[1] == "OK":
2203 def handle_CAL(self
, params
):
2204 checkParamLen(len(params
), 3, 'CAL')
2206 if params
[1].upper() == "RINGING":
2207 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2210 def handle_JOI(self
, params
):
2211 checkParamLen(len(params
), 2, 'JOI')
2212 self
.userJoined(params
[0], unquote(params
[1]))
2214 # users participating in the current chat
2215 def handle_IRO(self
, params
):
2216 checkParamLen(len(params
), 5, 'IRO')
2217 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2218 if params
[1] == params
[2]:
2219 self
.gotChattingUsers(self
.pendingUsers
)
2220 self
.pendingUsers
= {}
2222 # finished listing users
2223 def handle_ANS(self
, params
):
2224 checkParamLen(len(params
), 2, 'ANS')
2225 if params
[1] == "OK":
2228 def handle_ACK(self
, params
):
2229 checkParamLen(len(params
), 1, 'ACK')
2230 self
._fireCallback
(int(params
[0]), None)
2232 def handle_NAK(self
, params
):
2233 checkParamLen(len(params
), 1, 'NAK')
2234 self
._fireCallback
(int(params
[0]), None)
2236 def handle_BYE(self
, params
):
2237 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2238 self
.userLeft(params
[0])
2244 called when all login details have been negotiated.
2245 Messages can now be sent, or new users invited.
2249 def gotChattingUsers(self
, users
):
2251 called after connecting to an existing chat session.
2253 @param users: A dict mapping user handles to screen names
2254 (current users taking part in the conversation)
2258 def userJoined(self
, userHandle
, screenName
):
2260 called when a user has joined the conversation.
2262 @param userHandle: the user handle (passport) of the user
2263 @param screenName: the screen name of the user
2267 def userLeft(self
, userHandle
):
2269 called when a user has left the conversation.
2271 @param userHandle: the user handle (passport) of the user.
2275 def gotMessage(self
, message
):
2277 called when we receive a message.
2279 @param message: the associated MSNMessage object
2283 def gotFileReceive(self
, fileReceive
):
2285 called when we receive a file send request from a contact.
2286 Default action is to reject the file.
2288 @param fileReceive: msnft.MSNFTReceive_Base instance
2290 fileReceive
.reject()
2293 def gotSendRequest(self
, fileReceive
):
2295 called when we receive a file send request from a contact
2297 @param fileReceive: msnft.MSNFTReceive_Base instance
2301 def gotContactTyping(self
, message
):
2303 called when we receive the special type of message notifying
2304 us that a contact is typing a message.
2306 @param message: the associated MSNMessage object
2312 def inviteUser(self
, userHandle
):
2314 used to invite a user to the current switchboard server.
2316 @param userHandle: the user handle (passport) of the desired user.
2318 @return: A Deferred, the callback for which will be called
2319 when the server notifies us that the user has indeed
2320 been invited. The callback argument will be a tuple
2321 with 1 element, the sessionID given to the invited user.
2322 I'm not sure if this is useful or not.
2325 id, d
= self
._createIDMapping
()
2326 self
.sendLine("CAL %s %s" % (id, userHandle
))
2329 def sendMessage(self
, message
):
2331 used to send a message.
2333 @param message: the corresponding MSNMessage object.
2335 @return: Depending on the value of message.ack.
2336 If set to MSNMessage.MESSAGE_ACK or
2337 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2338 the callback for which will be fired when an ACK or
2339 NACK is received - the callback argument will be
2340 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2341 the return value is None.
2344 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2345 else: id, d
= self
._createIDMapping
()
2346 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2347 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2348 # Apparently order matters with these
2349 orderMatters
= ("MIME-Version", "Content-Type", "Message-ID")
2350 for header
in orderMatters
:
2351 if message
.hasHeader(header
):
2352 self
.sendLine("%s: %s" % (header
, message
.getHeader(header
)))
2353 # send the rest of the headers
2354 for header
in [h
for h
in message
.headers
.items() if h
[0] not in orderMatters
]:
2355 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2356 self
.transport
.write("\r\n")
2357 self
.transport
.write(message
.message
)
2358 if MESSAGEDEBUG
: log
.msg(message
.message
)
2361 def sendAvatarRequest(self
, msnContact
):
2363 used to request an avatar from a user in this switchboard
2366 @param msnContact: the msnContact object to request an avatar for
2368 @return: A Deferred, the callback for which will be called
2369 when the avatar transfer succeeds.
2370 The callback argument will be a tuple with one element,
2371 the PNG avatar data.
2373 if not msnContact
.msnobj
: return
2375 def bufferClosed(data
):
2377 buffer = StringBuffer(bufferClosed
)
2378 buffer.error
= lambda: None
2379 slpLink
= SLPLink_AvatarReceive(remoteUser
=msnContact
.userHandle
, switchboard
=self
, consumer
=buffer, context
=msnContact
.msnobj
.text
)
2380 self
.slpLinks
[slpLink
.sessionID
] = slpLink
2383 def sendFile(self
, msnContact
, filename
, filesize
):
2385 used to send a file to a contact.
2387 @param msnContact: the MSNContact object to send a file to.
2388 @param filename: the name of the file to send.
2389 @param filesize: the size of the file to send.
2391 @return: (fileSend, d) A FileSend object and a Deferred.
2392 The Deferred will pass one argument in a tuple,
2393 whether or not the transfer is accepted. If you
2394 receive a True, then you can call write() on the
2395 fileSend object to send your file. Call close()
2396 when the file is done.
2397 NOTE: You MUST write() exactly as much as you
2398 declare in filesize.
2400 if not msnContact
.userHandle
: return
2401 # FIXME, check msnContact.caps to see if we should use old-style
2402 fileSend
= SLPLink_FileSend(remoteUser
=msnContact
.userHandle
, switchboard
=self
, filename
=filename
, filesize
=filesize
)
2403 self
.slpLinks
[fileSend
.sessionID
] = fileSend
2404 return fileSend
, fileSend
.acceptDeferred
2406 def sendTypingNotification(self
):
2408 Used to send a typing notification. Upon receiving this
2409 message the official client will display a 'user is typing'
2410 message to all other users in the chat session for 10 seconds.
2411 You should send one of these every 5 seconds as long as the
2415 m
.ack
= m
.MESSAGE_ACK_NONE
2416 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2417 m
.setHeader('TypingUser', self
.userHandle
)
2421 def sendFileInvitation(self
, fileName
, fileSize
):
2423 send an notification that we want to send a file.
2425 @param fileName: the file name
2426 @param fileSize: the file size
2428 @return: A Deferred, the callback of which will be fired
2429 when the user responds to this invitation with an
2430 appropriate message. The callback argument will be
2431 a tuple with 3 elements, the first being 1 or 0
2432 depending on whether they accepted the transfer
2433 (1=yes, 0=no), the second being an invitation cookie
2434 to identify your follow-up responses and the third being
2435 the message 'info' which is a dict of information they
2436 sent in their reply (this doesn't really need to be used).
2437 If you wish to proceed with the transfer see the
2438 sendTransferInfo method.
2440 cookie
= self
._newInvitationCookie
()
2443 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2444 m
.message
+= 'Application-Name: File Transfer\r\n'
2445 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2446 m
.message
+= 'Invitation-Command: INVITE\r\n'
2447 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2448 m
.message
+= 'Application-File: %s\r\n' % fileName
2449 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2450 m
.ack
= m
.MESSAGE_ACK_NONE
2452 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2455 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2457 send information relating to a file transfer session.
2459 @param accept: whether or not to go ahead with the transfer
2461 @param iCookie: the invitation cookie of previous replies
2462 relating to this transfer
2463 @param authCookie: the authentication cookie obtained from
2464 an FileSend instance
2466 @param port: the port on which an FileSend protocol is listening.
2469 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2470 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2471 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2472 m
.message
+= 'IP-Address: %s\r\n' % ip
2473 m
.message
+= 'Port: %s\r\n' % port
2474 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2476 m
.ack
= m
.MESSAGE_NACK
2481 def __init__(self
, filename
, filesize
, userHandle
):
2482 self
.consumer
= None
2483 self
.finished
= False
2486 self
.filename
, self
.filesize
, self
.userHandle
= filename
, filesize
, userHandle
2489 raise NotImplementedError
2491 def accept(self
, consumer
):
2492 if self
.consumer
: raise "AlreadyAccepted"
2493 self
.consumer
= consumer
2494 for data
in self
.buffer:
2495 self
.consumer
.write(data
)
2498 self
.consumer
.close()
2500 self
.consumer
.error()
2502 def write(self
, data
):
2503 if self
.error
or self
.finished
:
2504 raise IOError, "Attempt to write in an invalid state"
2506 self
.consumer
.write(data
)
2508 self
.buffer.append(data
)
2511 self
.finished
= True
2513 self
.consumer
.close()
2516 """ Represents the Context field for P2P file transfers """
2517 def __init__(self
, data
=""):
2525 if MSNP2PDEBUG
: log
.msg("FileContext packing:", self
.filename
, self
.filesize
)
2526 data
= struct
.pack("<LLQL", 638, 0x03, self
.filesize
, 0x01)
2527 data
= data
[:-1] # Uck, weird, but it works
2528 data
+= utf16net(self
.filename
)
2529 data
= ljust(data
, 570, '\0')
2530 data
+= struct
.pack("<L", 0xFFFFFFFFL
)
2531 data
= ljust(data
, 638, '\0')
2534 def parse(self
, packet
):
2535 self
.filesize
= struct
.unpack("<Q", packet
[8:16])[0]
2536 chunk
= packet
[19:540]
2537 chunk
= chunk
[:chunk
.find('\x00\x00')]
2538 self
.filename
= unicode((codecs
.BOM_UTF16_BE
+ chunk
).decode("utf-16"))
2539 if MSNP2PDEBUG
: log
.msg("FileContext parsed:", self
.filesize
, self
.filename
)
2543 """ Utility class for the binary header & footer in p2p messages """
2552 def __init__(self
, fields
=None, packet
=None):
2554 self
.fields
= fields
2556 self
.fields
= [0] * 10
2558 self
.unpackFields(packet
)
2560 def __getitem__(self
, key
):
2561 return self
.fields
[key
]
2563 def __setitem__(self
, key
, value
):
2564 self
.fields
[key
] = value
2566 def unpackFields(self
, packet
):
2567 self
.fields
= struct
.unpack("<LLQQLLLLQ", packet
[0:48])
2568 self
.fields
+= struct
.unpack(">L", packet
[len(packet
)-4:])
2570 out
= "Unpacked fields: "
2571 for i
in self
.fields
:
2575 def packHeaders(self
):
2576 f
= tuple(self
.fields
)
2578 out
= "Packed fields: "
2579 for i
in self
.fields
:
2582 return struct
.pack("<LLQQLLLLQ", f
[0], f
[1], f
[2], f
[3], f
[4], f
[5], f
[6], f
[7], f
[8])
2584 def packFooter(self
):
2585 return struct
.pack(">L", self
.fields
[9])
2588 class MSNSLPMessage
:
2589 """ Representation of a single MSNSLP message """
2590 def __init__(self
, packet
=None):
2597 self
.sessionGuid
= ""
2598 self
.sessionID
= None
2600 self
.data
= "\r\n" + chr(0)
2604 def create(self
, method
=None, status
=None, to
=None, fro
=None, branch
=None, cseq
=0, sessionGuid
=None, data
=None):
2605 self
.method
= method
2606 self
.status
= status
2609 self
.branch
= branch
2611 self
.sessionGuid
= sessionGuid
2612 if data
: self
.data
= data
2614 def setData(self
, ctype
, data
):
2617 order
= ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
2619 if key
== "Context" and data
.has_key(key
):
2620 s
.append("Context: %s\r\n" % b64enc(data
[key
]))
2621 elif data
.has_key(key
):
2622 s
.append("%s: %s\r\n" % (key
, str(data
[key
])))
2623 s
.append("\r\n"+chr(0))
2625 self
.data
= "".join(s
)
2629 if s
.find("MSNSLP/1.0") < 0: return
2631 lines
= s
.split("\r\n")
2633 # Get the MSNSLP method or status
2634 msnslp
= lines
[0].split(" ")
2635 if MSNP2PDEBUG
: log
.msg("Parsing MSNSLPMessage %s %s" % (len(s
), s
))
2636 if msnslp
[0] in ("INVITE", "BYE"):
2637 self
.method
= msnslp
[0].strip()
2639 self
.status
= msnslp
[1].strip()
2641 lines
.remove(lines
[0])
2644 line
= line
.split(":")
2645 if len(line
) < 1: continue
2647 if len(line
) > 2 and line
[0] == "To":
2648 self
.to
= line
[2][:line
[2].find('>')]
2649 elif len(line
) > 2 and line
[0] == "From":
2650 self
.fro
= line
[2][:line
[2].find('>')]
2651 elif line
[0] == "Call-ID":
2652 self
.sessionGuid
= line
[1].strip()
2653 elif line
[0] == "CSeq":
2654 self
.cseq
= int(line
[1].strip())
2655 elif line
[0] == "SessionID":
2656 self
.sessionID
= int(line
[1].strip())
2657 elif line
[0] == "EUF-GUID":
2658 self
.euf_guid
= line
[1].strip()
2659 elif line
[0] == "Content-Type":
2660 self
.ctype
= line
[1].strip()
2661 elif line
[0] == "Context":
2662 self
.context
= b64dec(line
[1])
2663 elif line
[0] == "Via":
2664 self
.branch
= line
[1].split(";")[1].split("=")[1].strip()
2667 log
.msg("Error parsing MSNSLP message.")
2673 s
.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self
.method
, self
.to
))
2675 if self
.status
== "200": status
= "200 OK"
2676 elif self
.status
== "603": status
= "603 Decline"
2677 s
.append("MSNSLP/1.0 %s\r\n" % status
)
2678 s
.append("To: <msnmsgr:%s>\r\n" % self
.to
)
2679 s
.append("From: <msnmsgr:%s>\r\n" % self
.fro
)
2680 s
.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % self
.branch
)
2681 s
.append("CSeq: %s \r\n" % str(self
.cseq
))
2682 s
.append("Call-ID: %s\r\n" % self
.sessionGuid
)
2683 s
.append("Max-Forwards: 0\r\n")
2684 s
.append("Content-Type: %s\r\n" % self
.ctype
)
2685 s
.append("Content-Length: %s\r\n\r\n" % len(self
.data
))
2690 """ Utility for handling the weird sequence IDs in p2p messages """
2691 def __init__(self
, baseID
=None):
2693 self
.baseID
= baseID
2695 self
.baseID
= random
.randint(1000, sys
.maxint
)
2699 return p2pseq(self
.pos
) + self
.baseID
2706 class StringBuffer(StringIO
.StringIO
):
2707 def __init__(self
, notifyFunc
=None):
2708 self
.notifyFunc
= notifyFunc
2709 StringIO
.StringIO
.__init
__(self
)
2713 self
.notifyFunc(self
.getvalue())
2714 self
.notifyFunc
= None
2715 StringIO
.StringIO
.close(self
)
2719 def __init__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
):
2722 sessionID
= random
.randint(1000, sys
.maxint
)
2724 sessionGuid
= random_guid()
2725 self
.remoteUser
= remoteUser
2726 self
.switchboard
= switchboard
2727 self
.sessionID
= sessionID
2728 self
.sessionGuid
= sessionGuid
2729 self
.seqID
= SeqID()
2732 if MSNP2PDEBUG
: log
.msg("killLink")
2734 if MSNP2PDEBUG
: log
.msg("killLink - kill()")
2735 if not self
.switchboard
: return
2736 del self
.switchboard
.slpLinks
[self
.sessionID
]
2737 self
.switchboard
= None
2738 # This is so that handleP2PMessage can still use the SLPLink
2739 # one last time, for ACKing BYEs and 601s.
2740 reactor
.callLater(0, kill
)
2742 def warn(self
, text
):
2743 log
.msg("Warning in transfer: %s %s" % (self
, text
))
2745 def sendP2PACK(self
, ackHeaders
):
2746 binaryFields
= BinaryFields()
2747 binaryFields
[0] = ackHeaders
[0]
2748 binaryFields
[1] = self
.seqID
.next()
2749 binaryFields
[3] = ackHeaders
[3]
2750 binaryFields
[5] = BinaryFields
.ACK
2751 binaryFields
[6] = ackHeaders
[1]
2752 binaryFields
[7] = ackHeaders
[6]
2753 binaryFields
[8] = ackHeaders
[3]
2754 self
.sendP2PMessage(binaryFields
, "")
2756 def sendSLPMessage(self
, cmd
, ctype
, data
, branch
=None):
2757 msg
= MSNSLPMessage()
2759 msg
.create(status
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=branch
, cseq
=1, sessionGuid
=self
.sessionGuid
)
2761 msg
.create(method
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=random_guid(), cseq
=0, sessionGuid
=self
.sessionGuid
)
2762 msg
.setData(ctype
, data
)
2764 binaryFields
= BinaryFields()
2765 binaryFields
[1] = self
.seqID
.next()
2766 binaryFields
[3] = len(msgStr
)
2767 binaryFields
[4] = binaryFields
[3]
2768 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2769 self
.sendP2PMessage(binaryFields
, msgStr
)
2771 def sendP2PMessage(self
, binaryFields
, msgStr
):
2772 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2774 message
= MSNMessage(message
=packet
)
2775 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2776 message
.setHeader("P2P-Dest", self
.remoteUser
)
2777 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2778 self
.switchboard
.sendMessage(message
)
2780 def handleSLPMessage(self
, slpMessage
):
2781 raise NotImplementedError
2787 class SLPLink_Send(SLPLink
):
2788 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2789 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2790 self
.handlePacket
= None
2792 self
.filesize
= filesize
2795 def send_dataprep(self
):
2796 if MSNP2PDEBUG
: log
.msg("send_dataprep")
2797 binaryFields
= BinaryFields()
2798 binaryFields
[0] = self
.sessionID
2799 binaryFields
[1] = self
.seqID
.next()
2802 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2804 self
.sendP2PMessage(binaryFields
, chr(0) * 4)
2806 def write(self
, data
):
2807 if MSNP2PDEBUG
: log
.msg("write")
2809 data
= self
.data
+ data
2813 if i
+ 1202 < length
:
2814 self
._writeChunk
(data
[i
:i
+1202])
2817 self
.data
= data
[i
:]
2820 def _writeChunk(self
, chunk
):
2821 if MSNP2PDEBUG
: log
.msg("writing chunk")
2822 binaryFields
= BinaryFields()
2823 binaryFields
[0] = self
.sessionID
2824 if self
.offset
== 0:
2825 binaryFields
[1] = self
.seqID
.next()
2827 binaryFields
[1] = self
.seqID
.get()
2828 binaryFields
[2] = self
.offset
2829 binaryFields
[3] = self
.filesize
2830 binaryFields
[4] = len(chunk
)
2831 binaryFields
[5] = self
.dataFlag
2832 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2834 self
.offset
+= len(chunk
)
2835 self
.sendP2PMessage(binaryFields
, chunk
)
2839 self
._writeChunk
(self
.data
)
2844 # FIXME, should send 601 or something
2846 class SLPLink_FileSend(SLPLink_Send
):
2847 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
):
2848 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
)
2849 self
.dataFlag
= BinaryFields
.DATAFT
2850 # Send invite & wait for 200OK before sending dataprep
2851 context
= FileContext()
2852 context
.filename
= filename
2853 context
.filesize
= filesize
2854 data
= {"EUF-GUID" : MSN_MSNFTP_GUID
,\
2855 "SessionID": self
.sessionID
,\
2857 "Context" : context
.pack() }
2858 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2859 self
.acceptDeferred
= Deferred()
2861 def handleSLPMessage(self
, slpMessage
):
2862 if slpMessage
.status
== "200":
2863 if slpMessage
.ctype
== "application/x-msnmsgr-sessionreqbody":
2864 data
= {"Bridges" : "TRUDPv1 TCPv1",\
2866 "Conn-Type" : "Firewall",\
2867 "UPnPNat" : "false",\
2869 #"Hashed-Nonce": random_guid()}
2870 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data
)
2871 elif slpMessage
.ctype
== "application/x-msnmsgr-transrespbody":
2872 self
.acceptDeferred
.callback((True,))
2873 self
.handlePacket
= self
.wait_data_ack
2875 if slpMessage
.status
== "603":
2876 self
.acceptDeferred
.callback((False,))
2877 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to decline, error or BYE")
2880 def wait_data_ack(self
, packet
):
2881 if MSNP2PDEBUG
: log
.msg("wait_data_ack")
2882 binaryFields
= BinaryFields()
2883 binaryFields
.unpackFields(packet
)
2885 if binaryFields
[5] != BinaryFields
.ACK
:
2886 self
.warn("field5," + str(binaryFields
[5]))
2889 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2890 self
.handlePacket
= None
2893 self
.handlePacket
= self
.wait_data_ack
2894 SLPLink_Send
.close(self
)
2897 class SLPLink_AvatarSend(SLPLink_Send
):
2898 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2899 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2900 self
.dataFlag
= BinaryFields
.DATA
2901 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2902 self
.send_dataprep()
2903 self
.handlePacket
= lambda packet
: None
2905 def handleSLPMessage(self
, slpMessage
):
2906 if MSNP2PDEBUG
: log
.msg("BYE or error")
2910 SLPLink_Send
.close(self
)
2911 # Keep the link open to wait for a BYE
2913 class SLPLink_Receive(SLPLink
):
2914 def __init__(self
, remoteUser
, switchboard
, consumer
, context
=None, sessionID
=None, sessionGuid
=None):
2915 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2916 self
.handlePacket
= None
2917 self
.consumer
= consumer
2920 def wait_dataprep(self
, packet
):
2921 if MSNP2PDEBUG
: log
.msg("wait_dataprep")
2922 binaryFields
= BinaryFields()
2923 binaryFields
.unpackFields(packet
)
2925 if binaryFields
[3] != 4:
2926 self
.warn("field3," + str(binaryFields
[3]))
2928 if binaryFields
[4] != 4:
2929 self
.warn("field4," + str(binaryFields
[4]))
2931 # Just ignore the footer
2932 #if binaryFields[9] != 1:
2933 # self.warn("field9," + str(binaryFields[9]))
2936 self
.sendP2PACK(binaryFields
)
2937 self
.handlePacket
= self
.wait_data
2939 def wait_data(self
, packet
):
2940 if MSNP2PDEBUG
: log
.msg("wait_data")
2941 binaryFields
= BinaryFields()
2942 binaryFields
.unpackFields(packet
)
2944 if binaryFields
[5] != self
.dataFlag
:
2945 self
.warn("field5," + str(binaryFields
[5]))
2947 # Just ignore the footer
2948 #if binaryFields[9] != 1:
2949 # self.warn("field9," + str(binaryFields[9]))
2951 offset
= binaryFields
[2]
2952 total
= binaryFields
[3]
2953 length
= binaryFields
[4]
2955 data
= packet
[48:-4]
2956 if offset
!= self
.pos
:
2957 self
.warn("Received packet out of order")
2958 self
.consumer
.error()
2960 if len(data
) != length
:
2961 self
.warn("Received bad length of slp")
2962 self
.consumer
.error()
2967 self
.consumer
.write(str(data
))
2969 if self
.pos
== total
:
2970 self
.sendP2PACK(binaryFields
)
2971 self
.consumer
.close()
2972 self
.handlePacket
= None
2975 def doFinished(self
):
2976 raise NotImplementedError
2979 class SLPLink_FileReceive(SLPLink_Receive
, FileReceive
):
2980 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
, sessionID
, sessionGuid
, branch
):
2981 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=self
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2982 self
.dataFlag
= BinaryFields
.DATAFT
2983 self
.initialBranch
= branch
2984 FileReceive
.__init
__(self
, filename
, filesize
, remoteUser
)
2987 # Send a 603 decline
2988 if not self
.switchboard
: return
2989 self
.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2992 def accept(self
, consumer
):
2993 FileReceive
.accept(self
, consumer
)
2994 if not self
.switchboard
: return
2995 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2996 self
.handlePacket
= self
.wait_data
# Moved here because sometimes the second INVITE seems to be skipped
2998 def handleSLPMessage(self
, slpMessage
):
2999 if slpMessage
.method
== "INVITE": # The second invite
3000 data
= {"Bridge" : "TCPv1",\
3001 "Listening" : "false",\
3002 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
3003 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
3004 # self.handlePacket = self.wait_data # Moved up
3006 if MSNP2PDEBUG
: log
.msg("It's either a BYE or an error")
3008 # FIXME, do some error handling if it was an error
3010 def doFinished(self
):
3011 #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3013 # Wait for BYE? #FIXME
3016 class SLPLink_AvatarReceive(SLPLink_Receive
):
3017 def __init__(self
, remoteUser
, switchboard
, consumer
, context
):
3018 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=consumer
, context
=context
)
3019 self
.dataFlag
= BinaryFields
.DATA
3020 data
= {"EUF-GUID" : MSN_AVATAR_GUID
,\
3021 "SessionID": self
.sessionID
,\
3023 "Context" : context
}
3024 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
3025 self
.handlePacket
= self
.wait_dataprep
3027 def handleSLPMessage(self
, slpMessage
):
3028 if slpMessage
.status
== "200":
3030 #self.handlePacket = self.wait_dataprep # Moved upwards
3032 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to error or BYE")
3035 def doFinished(self
):
3036 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3038 # mapping of error codes to error messages
3041 200 : "Syntax error",
3042 201 : "Invalid parameter",
3043 205 : "Invalid user",
3044 206 : "Domain name missing",
3045 207 : "Already logged in",
3046 208 : "Invalid username",
3047 209 : "Invalid screen name",
3048 210 : "User list full",
3049 215 : "User already there",
3050 216 : "User already on list",
3051 217 : "User not online",
3052 218 : "Already in mode",
3053 219 : "User is in the opposite list",
3054 223 : "Too many groups",
3055 224 : "Invalid group",
3056 225 : "User not in group",
3057 229 : "Group name too long",
3058 230 : "Cannot remove group 0",
3059 231 : "Invalid group",
3060 280 : "Switchboard failed",
3061 281 : "Transfer to switchboard failed",
3063 300 : "Required field missing",
3064 301 : "Too many FND responses",
3065 302 : "Not logged in",
3067 402 : "Error accessing contact list",
3068 403 : "Error accessing contact list",
3070 500 : "Internal server error",
3071 501 : "Database server error",
3072 502 : "Command disabled",
3073 510 : "File operation failed",
3074 520 : "Memory allocation failed",
3075 540 : "Wrong CHL value sent to server",
3077 600 : "Server is busy",
3078 601 : "Server is unavaliable",
3079 602 : "Peer nameserver is down",
3080 603 : "Database connection failed",
3081 604 : "Server is going down",
3082 605 : "Server unavailable",
3084 707 : "Could not create connection",
3085 710 : "Invalid CVR parameters",
3086 711 : "Write is blocking",
3087 712 : "Session is overloaded",
3088 713 : "Too many active users",
3089 714 : "Too many sessions",
3090 715 : "Not expected",
3091 717 : "Bad friend file",
3092 731 : "Not expected",
3094 800 : "Requests too rapid",
3096 910 : "Server too busy",
3097 911 : "Authentication failed",
3098 912 : "Server too busy",
3099 913 : "Not allowed when offline",
3100 914 : "Server too busy",
3101 915 : "Server too busy",
3102 916 : "Server too busy",
3103 917 : "Server too busy",
3104 918 : "Server too busy",
3105 919 : "Server too busy",
3106 920 : "Not accepting new users",
3107 921 : "Server too busy",
3108 922 : "Server too busy",
3109 923 : "No parent consent",
3110 924 : "Passport account not yet verified"
3114 # mapping of status codes to readable status format
3117 STATUS_ONLINE
: "Online",
3118 STATUS_OFFLINE
: "Offline",
3119 STATUS_HIDDEN
: "Appear Offline",
3120 STATUS_IDLE
: "Idle",
3121 STATUS_AWAY
: "Away",
3122 STATUS_BUSY
: "Busy",
3123 STATUS_BRB
: "Be Right Back",
3124 STATUS_PHONE
: "On the Phone",
3125 STATUS_LUNCH
: "Out to Lunch"
3129 # mapping of list ids to list codes
3132 FORWARD_LIST
: 'fl',
3135 REVERSE_LIST
: 'rl',
3140 # mapping of list codes to list ids
3142 for id,code
in listIDToCode
.items():
3143 listCodeToID
[code
] = id