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 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 self
.currentMessage
:
795 self
.currentMessage
.readPos
+= len(line
+"\r\n")
797 header
, value
= line
.split(':')
798 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
801 #raise MSNProtocolError, "Invalid Message Header"
803 if line
== "" or self
.currentMessage
.specialMessage
:
805 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
808 cmd
, params
= line
.split(' ', 1)
810 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
812 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
814 if self
.ids
.has_key(params
.split(' ')[0]):
815 self
.ids
[id].errback(int(cmd
))
818 else: # we received an error which doesn't map to a sent command
819 self
.gotError(int(cmd
))
822 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
824 try: handler(params
.split(' '))
825 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
827 self
.handle_UNKNOWN(cmd
, params
.split(' '))
829 def rawDataReceived(self
, data
):
831 self
.currentMessage
.readPos
+= len(data
)
832 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
834 self
.currentMessage
.message
+= data
[:-diff
]
837 self
.currentMessage
.message
+= data
839 self
.currentMessage
.message
+= data
841 del self
.currentMessage
.readPos
842 m
= self
.currentMessage
843 self
.currentMessage
= None
844 if MESSAGEDEBUG
: log
.msg(m
.message
)
846 if not self
.checkMessage(m
):
847 self
.setLineMode(extra
)
850 log
.msg("Traceback - ERROR in checkMessage: " + str(e
))
851 self
.setLineMode(extra
)
854 self
.setLineMode(extra
)
856 ### protocol command handlers - no need to override these.
858 def handle_MSG(self
, params
):
859 checkParamLen(len(params
), 3, 'MSG')
861 messageLen
= int(params
[2])
862 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
863 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
865 def handle_UNKNOWN(self
, cmd
, params
):
866 """ implement me in subclasses if you want to handle unknown events """
867 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
871 def gotBadLine(self
, line
, why
):
872 """ called when a handler notifies me that this line is broken """
873 log
.msg('Error in line: %s (%s)' % (line
, why
))
875 def gotError(self
, errorCode
):
877 called when the server sends an error which is not in
878 response to a sent command (ie. it has no matching transaction ID)
880 log
.msg('Error %s' % (errorCodes
[errorCode
]))
883 class DispatchClient(MSNEventBase
):
885 This class provides support for clients connecting to the dispatch server
886 @ivar userHandle: your user handle (passport) needed before connecting.
889 def connectionMade(self
):
890 MSNEventBase
.connectionMade(self
)
891 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
893 ### protocol command handlers ( there is no need to override these )
895 def handle_VER(self
, params
):
896 versions
= params
[1:]
897 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
898 self
.transport
.loseConnection()
899 raise MSNProtocolError
, "Invalid version response"
900 id = self
._nextTransactionID
()
901 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.factory
.userHandle
))
903 def handle_CVR(self
, params
):
904 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
906 def handle_XFR(self
, params
):
907 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
908 id, refType
, addr
= params
[:3]
909 # was addr a host:port pair?
911 host
, port
= addr
.split(':')
916 self
.gotNotificationReferral(host
, int(port
))
920 def gotNotificationReferral(self
, host
, port
):
922 called when we get a referral to the notification server.
924 @param host: the notification server's hostname
925 @param port: the port to connect to
930 class DispatchFactory(ClientFactory
):
932 This class keeps the state for the DispatchClient.
934 @ivar userHandle: the userHandle to request a notification
937 protocol
= DispatchClient
942 class NotificationClient(MSNEventBase
):
944 This class provides support for clients connecting
945 to the notification server.
948 factory
= None # sssh pychecker
950 def __init__(self
, currentID
=0):
951 MSNEventBase
.__init
__(self
)
952 self
.currentID
= currentID
953 self
._state
= ['DISCONNECTED', {}]
955 self
.pingCheckTask
= None
956 self
.msnobj
= MSNObject()
958 def _setState(self
, state
):
959 self
._state
[0] = state
962 return self
._state
[0]
964 def _getStateData(self
, key
):
965 return self
._state
[1][key
]
967 def _setStateData(self
, key
, value
):
968 self
._state
[1][key
] = value
970 def _remStateData(self
, *args
):
971 for key
in args
: del self
._state
[1][key
]
973 def connectionMade(self
):
974 MSNEventBase
.connectionMade(self
)
975 self
._setState
('CONNECTED')
976 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
978 def connectionLost(self
, reason
):
979 self
._setState
('DISCONNECTED')
981 if self
.pingCheckTask
:
982 self
.pingCheckTask
.stop()
983 self
.pingCheckTask
= None
984 MSNEventBase
.connectionLost(self
, reason
)
986 def _getEmailFields(self
, message
):
987 fields
= message
.getMessage().strip().split('\n')
991 if len(a
) != 2: continue
998 def _gotInitialEmailNotification(self
, message
):
999 values
= self
._getEmailFields
(message
)
1001 inboxunread
= int(values
["Inbox-Unread"])
1002 foldersunread
= int(values
["Folders-Unread"])
1005 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
1006 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
1008 def _gotEmailNotification(self
, message
):
1009 values
= self
._getEmailFields
(message
)
1011 mailfrom
= values
["From"]
1012 fromaddr
= values
["From-Addr"]
1013 subject
= values
["Subject"]
1014 junkbeginning
= "=?\"us-ascii\"?Q?"
1016 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
1018 # If any of the fields weren't found then it's not a big problem. We just ignore the message
1020 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
1022 def _gotMSNAlert(self
, message
):
1023 notification
= xmlw
.parseText(message
.message
, beExtremelyLenient
=True)
1024 siteurl
= notification
.getAttribute("siteurl")
1025 notid
= notification
.getAttribute("id")
1028 for e
in notification
.elements():
1034 msgid
= msg
.getAttribute("id")
1039 for e
in msg
.elements():
1040 if e
.name
== "ACTION":
1041 action
= e
.getAttribute("url")
1042 if e
.name
== "SUBSCR":
1043 subscr
= e
.getAttribute("url")
1044 if e
.name
== "BODY":
1045 for e2
in e
.elements():
1046 if e2
.name
== "TEXT":
1047 bodytext
= e2
.__str__()
1048 if not (action
and subscr
and bodytext
): return
1050 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
1051 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
1053 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
1055 def _gotUBX(self
, message
):
1056 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
1057 if not msnContact
: return
1058 lm
= message
.message
.lower()
1059 p1
= lm
.find("<psm>") + 5
1060 p2
= lm
.find("</psm>")
1061 if p1
>= 0 and p2
>= 0:
1062 personal
= xmlw
.unescapeFromXml(message
.message
[p1
:p2
])
1063 msnContact
.personal
= personal
1064 self
.contactPersonalChanged(message
.userHandle
, personal
)
1066 msnContact
.personal
= ''
1067 self
.contactPersonalChanged(message
.userHandle
, '')
1069 def checkMessage(self
, message
):
1070 """ hook used for detecting specific notification messages """
1071 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
1072 if 'text/x-msmsgsprofile' in cTypes
:
1073 self
.gotProfile(message
)
1075 elif "text/x-msmsgsinitialemailnotification" in cTypes
:
1076 self
._gotInitialEmailNotification
(message
)
1078 elif "text/x-msmsgsemailnotification" in cTypes
:
1079 self
._gotEmailNotification
(message
)
1081 elif "NOTIFICATION" == message
.userHandle
and message
.specialMessage
== True:
1082 self
._gotMSNAlert
(message
)
1084 elif "UBX" == message
.screenName
and message
.specialMessage
== True:
1085 self
._gotUBX
(message
)
1089 ### protocol command handlers - no need to override these
1091 def handle_VER(self
, params
):
1092 versions
= params
[1:]
1093 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
1094 self
.transport
.loseConnection()
1095 raise MSNProtocolError
, "Invalid version response"
1096 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
1098 def handle_CVR(self
, params
):
1099 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
1101 def handle_USR(self
, params
):
1102 if not (4 <= len(params
) <= 6):
1103 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
1105 mechanism
= params
[1]
1106 if mechanism
== "OK":
1107 self
.loggedIn(params
[2], int(params
[3]))
1108 elif params
[2].upper() == "S":
1109 # we need to obtain auth from a passport server
1111 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3])
1112 d
.addCallback(self
._passportLogin
)
1113 d
.addErrback(self
._passportError
)
1115 def _passportLogin(self
, result
):
1116 if result
[0] == LOGIN_REDIRECT
:
1117 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
1118 result
[1], cached
=1, authData
=result
[2])
1119 d
.addCallback(self
._passportLogin
)
1120 d
.addErrback(self
._passportError
)
1121 elif result
[0] == LOGIN_SUCCESS
:
1122 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
1123 elif result
[0] == LOGIN_FAILURE
:
1124 self
.loginFailure(result
[1])
1126 def _passportError(self
, failure
):
1127 self
.loginFailure("Exception while authenticating: %s" % failure
)
1129 def handle_CHG(self
, params
):
1131 if not self
._fireCallback
(id, params
[1]):
1132 if self
.factory
: self
.factory
.status
= params
[1]
1133 self
.statusChanged(params
[1])
1135 def handle_ILN(self
, params
):
1136 #checkParamLen(len(params), 6, 'ILN')
1137 msnContact
= self
.factory
.contacts
.getContact(params
[2])
1138 if not msnContact
: return
1139 msnContact
.status
= params
[1]
1140 msnContact
.screenName
= unquote(params
[3])
1141 if len(params
) > 4: msnContact
.caps
= int(params
[4])
1143 self
.handleAvatarHelper(msnContact
, params
[5])
1145 self
.handleAvatarGoneHelper(msnContact
)
1146 self
.gotContactStatus(params
[2], params
[1], unquote(params
[3]))
1148 def handleAvatarGoneHelper(self
, msnContact
):
1149 if msnContact
.msnobj
:
1150 msnContact
.msnobj
= None
1151 msnContact
.msnobjGot
= True
1152 self
.contactAvatarChanged(msnContact
.userHandle
, "")
1154 def handleAvatarHelper(self
, msnContact
, msnobjStr
):
1155 msnobj
= MSNObject(unquote(msnobjStr
))
1156 if not msnContact
.msnobj
or msnobj
.sha1d
!= msnContact
.msnobj
.sha1d
:
1157 if MSNP2PDEBUG
: log
.msg("Updated MSNObject received!" + msnobjStr
)
1158 msnContact
.msnobj
= msnobj
1159 msnContact
.msnobjGot
= False
1160 self
.contactAvatarChanged(msnContact
.userHandle
, binascii
.hexlify(b64dec(msnContact
.msnobj
.sha1d
)))
1162 def handle_CHL(self
, params
):
1163 checkParamLen(len(params
), 2, 'CHL')
1164 response
= msnp11chl
.doChallenge(params
[1])
1165 self
.sendLine("QRY %s %s %s" % (self
._nextTransactionID
(), msnp11chl
.MSNP11_PRODUCT_ID
, len(response
)))
1166 self
.transport
.write(response
)
1168 def handle_QRY(self
, params
):
1171 def handle_NLN(self
, params
):
1172 if not self
.factory
: return
1173 msnContact
= self
.factory
.contacts
.getContact(params
[1])
1174 if not msnContact
: return
1175 msnContact
.status
= params
[0]
1176 msnContact
.screenName
= unquote(params
[2])
1177 if len(params
) > 3: msnContact
.caps
= int(params
[3])
1179 self
.handleAvatarHelper(msnContact
, params
[4])
1181 self
.handleAvatarGoneHelper(msnContact
)
1182 self
.contactStatusChanged(params
[1], params
[0], unquote(params
[2]))
1184 def handle_FLN(self
, params
):
1185 checkParamLen(len(params
), 1, 'FLN')
1186 msnContact
= self
.factory
.contacts
.getContact(params
[0])
1188 msnContact
.status
= STATUS_OFFLINE
1189 self
.contactOffline(params
[0])
1191 def handle_LST(self
, params
):
1192 if self
._getState
() != 'SYNC': return
1194 userHandle
, screenName
, userGuid
, lists
, groups
= getVals(params
)
1196 if not userHandle
or lists
< 1:
1197 raise MSNProtocolError
, "Unknown LST " + str(params
) # debug
1198 contact
= MSNContact(userGuid
, userHandle
, screenName
, lists
)
1199 if contact
.lists
& FORWARD_LIST
:
1200 contact
.groups
.extend(map(str, groups
))
1201 self
._getStateData
('list').addContact(contact
)
1202 self
._setStateData
('last_contact', contact
)
1203 sofar
= self
._getStateData
('lst_sofar') + 1
1204 if sofar
== self
._getStateData
('lst_reply'):
1205 # this is the best place to determine that
1206 # a syn realy has finished - msn _may_ send
1207 # BPR information for the last contact
1208 # which is unfortunate because it means
1209 # that the real end of a syn is non-deterministic.
1210 # to handle this we'll keep 'last_contact' hanging
1211 # around in the state data and update it if we need
1213 self
._setState
('SESSION')
1214 contacts
= self
._getStateData
('list')
1215 phone
= self
._getStateData
('phone')
1216 id = self
._getStateData
('synid')
1217 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1218 self
._fireCallback
(id, contacts
, phone
)
1220 self
._setStateData
('lst_sofar',sofar
)
1222 def handle_BLP(self
, params
):
1223 # check to see if this is in response to a SYN
1224 if self
._getState
() == 'SYNC':
1225 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
1228 self
.factory
.contacts
.privacy
= listCodeToID
[params
[1].lower()]
1229 self
._fireCallback
(id, params
[1])
1231 def handle_GTC(self
, params
):
1232 # check to see if this is in response to a SYN
1233 if self
._getState
() == 'SYNC':
1234 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
1235 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
1236 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1239 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
1240 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
1241 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1243 def handle_SYN(self
, params
):
1245 self
._setStateData
('phone', []) # Always needs to be set
1246 if params
[3] == 0: # No LST will be received. New account?
1247 self
._setState
('SESSION')
1248 self
._fireCallback
(id, None, None)
1250 contacts
= MSNContactList()
1251 self
._setStateData
('list', contacts
)
1252 self
._setStateData
('lst_reply', int(params
[3]))
1253 self
._setStateData
('lsg_reply', int(params
[4]))
1254 self
._setStateData
('lst_sofar', 0)
1256 def handle_LSG(self
, params
):
1257 if self
._getState
() == 'SYNC':
1258 self
._getStateData
('list').groups
[params
[1]] = unquote(params
[0])
1260 def handle_PRP(self
, params
):
1261 if params
[1] == "MFN":
1262 self
._fireCallback
(int(params
[0]))
1263 elif self
._getState
() == 'SYNC':
1264 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
1266 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
1268 def handle_BPR(self
, params
):
1269 numParams
= len(params
)
1270 if numParams
== 2: # part of a syn
1271 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
1272 elif numParams
== 4:
1273 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_BPR called with no contact list" # debug
1274 self
.factory
.contacts
.version
= int(params
[0])
1275 userHandle
, phoneType
, number
= params
[1], params
[2], unquote(params
[3])
1276 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1277 self
.gotPhoneNumber(userHandle
, phoneType
, number
)
1280 def handle_ADG(self
, params
):
1281 checkParamLen(len(params
), 5, 'ADG')
1283 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
1284 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
1286 def handle_RMG(self
, params
):
1287 checkParamLen(len(params
), 3, 'RMG')
1289 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
1290 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
1292 def handle_REG(self
, params
):
1293 checkParamLen(len(params
), 5, 'REG')
1295 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
1296 raise MSNProtocolError
, "REG response does not match up to a request" # debug
1298 def handle_ADC(self
, params
):
1299 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_ADC called with no contact list"
1300 numParams
= len(params
)
1301 if numParams
< 3 or params
[1].upper() not in ('AL','BL','RL','FL','PL'):
1302 raise MSNProtocolError
, "Invalid Paramaters for ADC" # debug
1304 listType
= params
[1].lower()
1305 userHandle
, screenName
, userGuid
, ignored1
, groups
= getVals(params
[2:])
1307 if groups
and listType
.upper() != FORWARD_LIST
:
1308 raise MSNProtocolError
, "Only forward list can contain groups" # debug
1310 if not self
._fireCallback
(id, listCodeToID
[listType
], userGuid
, userHandle
, screenName
):
1311 c
= self
.factory
.contacts
.getContact(userHandle
)
1313 c
= MSNContact(userGuid
=userGuid
, userHandle
=userHandle
, screenName
=screenName
)
1314 self
.factory
.contacts
.addContact(c
)
1315 c
.addToList(PENDING_LIST
)
1316 self
.userAddedMe(userGuid
, userHandle
, screenName
)
1318 def handle_REM(self
, params
):
1319 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_REM called with no contact list available!"
1320 numParams
= len(params
)
1321 if numParams
< 3 or params
[1].upper() not in ('AL','BL','FL','RL','PL'):
1322 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1324 listType
= params
[1].lower()
1325 userHandle
= params
[2]
1328 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1329 groupID
= int(params
[3])
1330 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, groupID
):
1331 if listType
.upper() != "RL": return
1332 c
= self
.factory
.contacts
.getContact(userHandle
)
1334 c
.removeFromList(REVERSE_LIST
)
1335 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1336 self
.userRemovedMe(userHandle
)
1338 def handle_XFR(self
, params
):
1339 checkParamLen(len(params
), 5, 'XFR')
1341 # check to see if they sent a host/port pair
1343 host
, port
= params
[2].split(':')
1348 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1349 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1351 def handle_RNG(self
, params
):
1352 checkParamLen(len(params
), 6, 'RNG')
1353 # check for host:port pair
1355 host
, port
= params
[1].split(":")
1360 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1363 def handle_NOT(self
, params
):
1364 checkParamLen(len(params
), 1, 'NOT')
1366 messageLen
= int(params
[0])
1367 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1368 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", specialMessage
=True)
1371 def handle_UBX(self
, params
):
1372 checkParamLen(len(params
), 2, 'UBX')
1374 messageLen
= int(params
[1])
1375 except ValueError: raise MSNProtocolError
, "Invalid Parameter for UBX length argument"
1377 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
="UBX", specialMessage
=True)
1380 self
._gotUBX
(MSNMessage(userHandle
=params
[0]))
1382 def handle_UUX(self
, params
):
1383 checkParamLen(len(params
), 2, 'UUX')
1384 if params
[1] != '0': return
1386 self
._fireCallback
(id)
1388 def handle_OUT(self
, params
):
1389 checkParamLen(len(params
), 1, 'OUT')
1390 if params
[0] == "OTH": self
.multipleLogin()
1391 elif params
[0] == "SSD": self
.serverGoingDown()
1392 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1394 def handle_QNG(self
, params
):
1395 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1399 def pingChecker(self
):
1400 if self
.pingCounter
> 5:
1401 # The server has ignored 5 pings, lets kill the connection
1402 self
.transport
.loseConnection()
1404 self
.sendLine("PNG")
1405 self
.pingCounter
+= 1
1407 def pingCheckerStart(self
, *args
):
1408 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1409 self
.pingCheckTask
.start(PINGSPEED
)
1411 def loggedIn(self
, userHandle
, verified
):
1413 Called when the client has logged in.
1414 The default behaviour of this method is to
1415 update the factory with our screenName and
1416 to sync the contact list (factory.contacts).
1417 When this is complete self.listSynchronized
1420 @param userHandle: our userHandle
1421 @param verified: 1 if our passport has been (verified), 0 if not.
1422 (i'm not sure of the significace of this)
1426 d
.addCallback(self
.listSynchronized
)
1427 d
.addCallback(self
.pingCheckerStart
)
1429 def loginFailure(self
, message
):
1431 Called when the client fails to login.
1433 @param message: a message indicating the problem that was encountered
1437 def gotProfile(self
, message
):
1439 Called after logging in when the server sends an initial
1440 message with MSN/passport specific profile information
1441 such as country, number of kids, etc.
1442 Check the message headers for the specific values.
1444 @param message: The profile message
1448 def listSynchronized(self
, *args
):
1450 Lists are now synchronized by default upon logging in, this
1451 method is called after the synchronization has finished
1452 and the factory now has the up-to-date contacts.
1456 def contactAvatarChanged(self
, userHandle
, hash):
1458 Called when we receive the first, or a new <msnobj/> from a
1461 @param userHandle: contact who's msnobj has been changed
1462 @param hash: sha1 hash of their avatar as hex string
1465 def statusChanged(self
, statusCode
):
1467 Called when our status changes and its not in response to a
1470 @param statusCode: 3-letter status code
1474 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
1476 Called when we receive a list of statuses upon login.
1478 @param userHandle: the contact's user handle (passport)
1479 @param statusCode: 3-letter status code
1480 @param screenName: the contact's screen name
1484 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
1486 Called when we're notified that a contact's status has changed.
1488 @param userHandle: the contact's user handle (passport)
1489 @param statusCode: 3-letter status code
1490 @param screenName: the contact's screen name
1494 def contactPersonalChanged(self
, userHandle
, personal
):
1496 Called when a contact's personal message changes.
1498 @param userHandle: the contact who changed their personal message
1499 @param personal : the new personal message
1503 def contactOffline(self
, userHandle
):
1505 Called when a contact goes offline.
1507 @param userHandle: the contact's user handle
1511 def gotMessage(self
, message
):
1513 Called when there is a message from the notification server
1514 that is not understood by default.
1516 @param message: the MSNMessage.
1520 def gotMSNAlert(self
, body
, action
, subscr
):
1522 Called when the server sends an MSN Alert (http://alerts.msn.com)
1524 @param body : the alert text
1525 @param action: a URL with more information for the user to view
1526 @param subscr: a URL the user can use to modify their alert subscription
1530 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1532 Called when the server sends you details about your hotmail
1533 inbox. This is only ever called once, on login.
1535 @param inboxunread : the number of unread items in your inbox
1536 @param foldersunread: the number of unread items in other folders
1540 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1542 Called when the server sends us realtime email
1543 notification. This means that you have received
1544 a new email in your hotmail inbox.
1546 @param mailfrom: the sender of the email
1547 @param fromaddr: the sender of the email (I don't know :P)
1548 @param subject : the email subject
1552 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1554 Called when the server sends us phone details about
1555 a specific user (for example after a user is added
1556 the server will send their status, phone details etc.
1558 @param userHandle: the contact's user handle (passport)
1559 @param phoneType: the specific phoneType
1560 (*_PHONE constants or HAS_PAGER)
1561 @param number: the value/phone number.
1565 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1567 Called when a user adds me to their list. (ie. they have been added to
1570 @param userHandle: the userHandle of the user
1571 @param screenName: the screen name of the user
1575 def userRemovedMe(self
, userHandle
):
1577 Called when a user removes us from their contact list
1578 (they are no longer on our reverseContacts list.
1580 @param userHandle: the contact's user handle (passport)
1584 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1585 key
, userHandle
, screenName
):
1587 Called when we get an invitation to a switchboard server.
1588 This happens when a user requests a chat session with us.
1590 @param sessionID: session ID number, must be remembered for logging in
1591 @param host: the hostname of the switchboard server
1592 @param port: the port to connect to
1593 @param key: used for authorization when connecting
1594 @param userHandle: the user handle of the person who invited us
1595 @param screenName: the screen name of the person who invited us
1599 def multipleLogin(self
):
1601 Called when the server says there has been another login
1602 under our account, the server should disconnect us right away.
1606 def serverGoingDown(self
):
1608 Called when the server has notified us that it is going down for
1615 def changeStatus(self
, status
):
1617 Change my current status. This method will add
1618 a default callback to the returned Deferred
1619 which will update the status attribute of the
1622 @param status: 3-letter status code (as defined by
1623 the STATUS_* constants)
1624 @return: A Deferred, the callback of which will be
1625 fired when the server confirms the change
1626 of status. The callback argument will be
1627 a tuple with the new status code as the
1631 id, d
= self
._createIDMapping
()
1632 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1634 self
.factory
.status
= r
[0]
1636 return d
.addCallback(_cb
)
1638 def setPrivacyMode(self
, privLevel
):
1640 Set my privacy mode on the server.
1643 This only keeps the current privacy setting on
1644 the server for later retrieval, it does not
1645 effect the way the server works at all.
1647 @param privLevel: This parameter can be true, in which
1648 case the server will keep the state as
1649 'al' which the official client interprets
1650 as -> allow messages from only users on
1651 the allow list. Alternatively it can be
1652 false, in which case the server will keep
1653 the state as 'bl' which the official client
1654 interprets as -> allow messages from all
1655 users except those on the block list.
1657 @return: A Deferred, the callback of which will be fired when
1658 the server replies with the new privacy setting.
1659 The callback argument will be a tuple, the only element
1660 of which being either 'al' or 'bl' (the new privacy setting).
1663 id, d
= self
._createIDMapping
()
1664 if privLevel
: self
.sendLine("BLP %s AL" % id)
1665 else: self
.sendLine("BLP %s BL" % id)
1670 Used for keeping an up-to-date contact list.
1671 A callback is added to the returned Deferred
1672 that updates the contact list on the factory
1673 and also sets my state to STATUS_ONLINE.
1676 This is called automatically upon signing
1677 in using the version attribute of
1678 factory.contacts, so you may want to persist
1679 this object accordingly. Because of this there
1680 is no real need to ever call this method
1683 @return: A Deferred, the callback of which will be
1684 fired when the server sends an adequate reply.
1685 The callback argument will be a tuple with two
1686 elements, the new list (MSNContactList) and
1687 your current state (a dictionary). If the version
1688 you sent _was_ the latest list version, both elements
1689 will be None. To just request the list send a version of 0.
1692 self
._setState
('SYNC')
1693 id, d
= self
._createIDMapping
(data
=None)
1694 self
._setStateData
('synid',id)
1695 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1697 self
.changeStatus(STATUS_ONLINE
)
1698 if r
[0] is not None:
1699 self
.factory
.contacts
= r
[0]
1701 return d
.addCallback(_cb
)
1703 def setPhoneDetails(self
, phoneType
, value
):
1705 Set/change my phone numbers stored on the server.
1707 @param phoneType: phoneType can be one of the following
1708 constants - HOME_PHONE, WORK_PHONE,
1709 MOBILE_PHONE, HAS_PAGER.
1710 These are pretty self-explanatory, except
1711 maybe HAS_PAGER which refers to whether or
1712 not you have a pager.
1713 @param value: for all of the *_PHONE constants the value is a
1714 phone number (str), for HAS_PAGER accepted values
1715 are 'Y' (for yes) and 'N' (for no).
1717 @return: A Deferred, the callback for which will be fired when
1718 the server confirms the change has been made. The
1719 callback argument will be a tuple with 2 elements, the
1720 first being the new list version (int) and the second
1721 being the new phone number value (str).
1723 raise "ProbablyDoesntWork"
1724 # XXX: Add a default callback which updates
1725 # factory.contacts.version and the relevant phone
1727 id, d
= self
._createIDMapping
()
1728 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1731 def addListGroup(self
, name
):
1733 Used to create a new list group.
1734 A default callback is added to the
1735 returned Deferred which updates the
1736 contacts attribute of the factory.
1738 @param name: The desired name of the new group.
1740 @return: A Deferred, the callbacck for which will be called
1741 when the server clarifies that the new group has been
1742 created. The callback argument will be a tuple with 3
1743 elements: the new list version (int), the new group name
1744 (str) and the new group ID (int).
1747 raise "ProbablyDoesntWork"
1748 id, d
= self
._createIDMapping
()
1749 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1751 if self
.factory
.contacts
:
1752 self
.factory
.contacts
.version
= r
[0]
1753 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1755 return d
.addCallback(_cb
)
1757 def remListGroup(self
, groupID
):
1759 Used to remove a list group.
1760 A default callback is added to the
1761 returned Deferred which updates the
1762 contacts attribute of the factory.
1764 @param groupID: the ID of the desired group to be removed.
1766 @return: A Deferred, the callback for which will be called when
1767 the server clarifies the deletion of the group.
1768 The callback argument will be a tuple with 2 elements:
1769 the new list version (int) and the group ID (int) of
1773 raise "ProbablyDoesntWork"
1774 id, d
= self
._createIDMapping
()
1775 self
.sendLine("RMG %s %s" % (id, groupID
))
1777 self
.factory
.contacts
.version
= r
[0]
1778 self
.factory
.contacts
.remGroup(r
[1])
1780 return d
.addCallback(_cb
)
1782 def renameListGroup(self
, groupID
, newName
):
1784 Used to rename an existing list group.
1785 A default callback is added to the returned
1786 Deferred which updates the contacts attribute
1789 @param groupID: the ID of the desired group to rename.
1790 @param newName: the desired new name for the group.
1792 @return: A Deferred, the callback for which will be called
1793 when the server clarifies the renaming.
1794 The callback argument will be a tuple of 3 elements,
1795 the new list version (int), the group id (int) and
1796 the new group name (str).
1799 raise "ProbablyDoesntWork"
1800 id, d
= self
._createIDMapping
()
1801 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1803 self
.factory
.contacts
.version
= r
[0]
1804 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1806 return d
.addCallback(_cb
)
1808 def addContact(self
, listType
, userHandle
):
1810 Used to add a contact to the desired list.
1811 A default callback is added to the returned
1812 Deferred which updates the contacts attribute of
1813 the factory with the new contact information.
1814 If you are adding a contact to the forward list
1815 and you want to associate this contact with multiple
1816 groups then you will need to call this method for each
1817 group you would like to add them to, changing the groupID
1818 parameter. The default callback will take care of updating
1819 the group information on the factory's contact list.
1821 @param listType: (as defined by the *_LIST constants)
1822 @param userHandle: the user handle (passport) of the contact
1825 @return: A Deferred, the callback for which will be called when
1826 the server has clarified that the user has been added.
1827 The callback argument will be a tuple with 4 elements:
1828 the list type, the contact's user handle, the new list
1829 version, and the group id (if relevant, otherwise it
1833 id, d
= self
._createIDMapping
()
1834 try: # Make sure the contact isn't actually on the list
1835 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1836 except AttributeError: pass
1837 listType
= listIDToCode
[listType
].upper()
1838 if listType
== "FL":
1839 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1841 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1844 if not self
.factory
: return
1845 c
= self
.factory
.contacts
.getContact(r
[2])
1847 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1848 self
.factory
.contacts
.addContact(c
)
1849 #if r[3]: c.groups.append(r[3])
1852 return d
.addCallback(_cb
)
1854 def remContact(self
, listType
, userHandle
):
1856 Used to remove a contact from the desired list.
1857 A default callback is added to the returned deferred
1858 which updates the contacts attribute of the factory
1859 to reflect the new contact information.
1861 @param listType: (as defined by the *_LIST constants)
1862 @param userHandle: the user handle (passport) of the
1863 contact being removed
1865 @return: A Deferred, the callback for which will be called when
1866 the server has clarified that the user has been removed.
1867 The callback argument will be a tuple of 3 elements:
1868 the list type, the contact's user handle and the group ID
1869 (if relevant, otherwise it will be None)
1872 id, d
= self
._createIDMapping
()
1873 try: # Make sure the contact is actually on this list
1874 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1875 except AttributeError: return
1876 listType
= listIDToCode
[listType
].upper()
1877 if listType
== "FL":
1879 c
= self
.factory
.contacts
.getContact(userHandle
)
1880 userGuid
= c
.userGuid
1881 except AttributeError: return
1882 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1884 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1887 if listType
== "FL":
1888 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1889 l
= self
.factory
.contacts
1890 c
= l
.getContact(r
[1])
1894 if group
: # they may not have been removed from the list
1895 c
.groups
.remove(group
)
1896 if c
.groups
: shouldRemove
= 0
1898 c
.removeFromList(r
[0])
1899 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1901 return d
.addCallback(_cb
)
1903 def changeScreenName(self
, newName
):
1905 Used to change your current screen name.
1906 A default callback is added to the returned
1907 Deferred which updates the screenName attribute
1908 of the factory and also updates the contact list
1911 @param newName: the new screen name
1913 @return: A Deferred, the callback for which will be called
1914 when the server acknowledges the change.
1915 The callback argument will be an empty tuple.
1918 id, d
= self
._createIDMapping
()
1919 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1921 self
.factory
.screenName
= newName
1923 return d
.addCallback(_cb
)
1925 def changePersonalMessage(self
, personal
):
1927 Used to change your personal message.
1929 @param personal: the new screen name
1931 @return: A Deferred, the callback for which will be called
1932 when the server acknowledges the change.
1933 The callback argument will be a tuple of 1 element,
1934 the personal message.
1937 id, d
= self
._createIDMapping
()
1940 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1941 self
.sendLine("UUX %s %s" % (id, len(data
)))
1942 self
.transport
.write(data
)
1944 self
.factory
.personal
= personal
1946 return d
.addCallback(_cb
)
1948 def changeAvatar(self
, imageData
, push
):
1950 Used to change the avatar that other users see.
1952 @param imageData: the PNG image data to set as the avatar
1953 @param push : whether to push the update to the server
1954 (it will otherwise be sent with the next
1957 @return: If push==True, a Deferred, the callback for which
1958 will be called when the server acknowledges the change.
1959 The callback argument will be the same as for changeStatus.
1962 if self
.msnobj
and imageData
== self
.msnobj
.imageData
: return
1964 self
.msnobj
.setData(self
.factory
.userHandle
, imageData
)
1966 self
.msnobj
.setNull()
1967 if push
: return self
.changeStatus(self
.factory
.status
) # Push to server
1970 def requestSwitchboardServer(self
):
1972 Used to request a switchboard server to use for conversations.
1974 @return: A Deferred, the callback for which will be called when
1975 the server responds with the switchboard information.
1976 The callback argument will be a tuple with 3 elements:
1977 the host of the switchboard server, the port and a key
1978 used for logging in.
1981 id, d
= self
._createIDMapping
()
1982 self
.sendLine("XFR %s SB" % id)
1987 Used to log out of the notification server.
1988 After running the method the server is expected
1989 to close the connection.
1992 if self
.pingCheckTask
:
1993 self
.pingCheckTask
.stop()
1994 self
.pingCheckTask
= None
1995 self
.sendLine("OUT")
1996 self
.transport
.loseConnection()
1998 class NotificationFactory(ClientFactory
):
2000 Factory for the NotificationClient protocol.
2001 This is basically responsible for keeping
2002 the state of the client and thus should be used
2003 in a 1:1 situation with clients.
2005 @ivar contacts: An MSNContactList instance reflecting
2006 the current contact list -- this is
2007 generally kept up to date by the default
2009 @ivar userHandle: The client's userHandle, this is expected
2010 to be set by the client and is used by the
2011 protocol (for logging in etc).
2012 @ivar screenName: The client's current screen-name -- this is
2013 generally kept up to date by the default
2015 @ivar password: The client's password -- this is (obviously)
2016 expected to be set by the client.
2017 @ivar passportServer: This must point to an msn passport server
2018 (the whole URL is required)
2019 @ivar status: The status of the client -- this is generally kept
2020 up to date by the default command handlers
2027 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
2029 protocol
= NotificationClient
2032 class SwitchboardClient(MSNEventBase
):
2034 This class provides support for clients connecting to a switchboard server.
2036 Switchboard servers are used for conversations with other people
2037 on the MSN network. This means that the number of conversations at
2038 any given time will be directly proportional to the number of
2039 connections to varioius switchboard servers.
2041 MSN makes no distinction between single and group conversations,
2042 so any number of users may be invited to join a specific conversation
2043 taking place on a switchboard server.
2045 @ivar key: authorization key, obtained when receiving
2046 invitation / requesting switchboard server.
2047 @ivar userHandle: your user handle (passport)
2048 @ivar sessionID: unique session ID, used if you are replying
2049 to a switchboard invitation
2050 @ivar reply: set this to 1 in connectionMade or before to signifiy
2051 that you are replying to a switchboard invitation.
2052 @ivar msnobj: the MSNObject for the user's avatar. So that the
2053 switchboard can distribute it to anyone who asks.
2065 MSNEventBase
.__init
__(self
)
2066 self
.pendingUsers
= {}
2067 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
2070 def connectionMade(self
):
2071 MSNEventBase
.connectionMade(self
)
2074 def connectionLost(self
, reason
):
2075 self
.cookies
['iCookies'] = {}
2076 MSNEventBase
.connectionLost(self
, reason
)
2078 def _sendInit(self
):
2080 send initial data based on whether we are replying to an invitation
2083 id = self
._nextTransactionID
()
2085 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
2087 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
2089 def _newInvitationCookie(self
):
2091 if self
._iCookie
> 1000: self
._iCookie
= 1
2092 return self
._iCookie
2094 def _checkTyping(self
, message
, cTypes
):
2095 """ helper method for checkMessage """
2096 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
2097 self
.gotContactTyping(message
)
2100 def _checkFileInvitation(self
, message
, info
):
2101 """ helper method for checkMessage """
2102 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
2104 cookie
= info
['Invitation-Cookie']
2105 filename
= info
['Application-File']
2106 filesize
= int(info
['Application-FileSize'])
2107 connectivity
= (info
.get('Connectivity', 'n').lower() == 'y')
2109 log
.msg('Received munged file transfer request ... ignoring.')
2111 raise NotImplementedError
2112 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
2115 def _handleP2PMessage(self
, message
):
2116 """ helper method for msnslp messages (file transfer & avatars) """
2117 if not message
.getHeader("P2P-Dest") == self
.userHandle
: return
2118 packet
= message
.message
2119 binaryFields
= BinaryFields(packet
=packet
)
2120 if binaryFields
[5] == BinaryFields
.BYEGOT
:
2121 pass # Ignore the ACKs to SLP messages
2122 elif binaryFields
[0] != 0:
2123 slpLink
= self
.slpLinks
.get(binaryFields
[0])
2125 # Link has been killed. Ignore
2127 if slpLink
.remoteUser
== message
.userHandle
:
2128 slpLink
.handlePacket(packet
)
2129 elif binaryFields
[5] == BinaryFields
.ACK
:
2130 pass # Ignore the ACKs to SLP messages
2132 slpMessage
= MSNSLPMessage(packet
)
2134 # Always try and give a slpMessage to a slpLink first.
2135 # If none can be found, and it was INVITE, then create
2136 # one to handle the session.
2137 for slpLink
in self
.slpLinks
.values():
2138 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
2139 slpLink
.handleSLPMessage(slpMessage
)
2142 slpLink
= None # Was not handled
2144 if not slpLink
and slpMessage
.method
== "INVITE":
2145 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
2146 context
= FileContext(slpMessage
.context
)
2147 slpLink
= SLPLink_FileReceive(remoteUser
=slpMessage
.fro
, switchboard
=self
, filename
=context
.filename
, filesize
=context
.filesize
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
, branch
=slpMessage
.branch
)
2148 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2149 self
.gotFileReceive(slpLink
)
2150 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
2151 # Check that we have an avatar to send
2153 slpLink
= SLPLink_AvatarSend(remoteUser
=slpMessage
.fro
, switchboard
=self
, filesize
=self
.msnobj
.size
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2154 slpLink
.write(self
.msnobj
.imageData
)
2157 # They shouldn't have sent a request if we have
2158 # no avatar. So we'll just ignore them.
2159 # FIXME We should really send an error
2162 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2164 # Always need to ACK these packets if we can
2165 slpLink
.sendP2PACK(binaryFields
)
2168 def checkMessage(self
, message
):
2170 hook for detecting any notification type messages
2171 (e.g. file transfer)
2173 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2174 if self
._checkTyping
(message
, cTypes
): return 0
2175 if 'text/x-msmsgsinvite' in cTypes
:
2176 # header like info is sent as part of the message body.
2178 for line
in message
.message
.split('\r\n'):
2180 key
, val
= line
.split(':')
2181 info
[key
] = val
.lstrip()
2182 except ValueError: continue
2183 if self
._checkFileInvitation
(message
, info
): return 0
2184 elif 'application/x-msnmsgrp2p' in cTypes
:
2185 self
._handleP
2PMessage
(message
)
2190 def handle_USR(self
, params
):
2191 checkParamLen(len(params
), 4, 'USR')
2192 if params
[1] == "OK":
2196 def handle_CAL(self
, params
):
2197 checkParamLen(len(params
), 3, 'CAL')
2199 if params
[1].upper() == "RINGING":
2200 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2203 def handle_JOI(self
, params
):
2204 checkParamLen(len(params
), 2, 'JOI')
2205 self
.userJoined(params
[0], unquote(params
[1]))
2207 # users participating in the current chat
2208 def handle_IRO(self
, params
):
2209 checkParamLen(len(params
), 5, 'IRO')
2210 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2211 if params
[1] == params
[2]:
2212 self
.gotChattingUsers(self
.pendingUsers
)
2213 self
.pendingUsers
= {}
2215 # finished listing users
2216 def handle_ANS(self
, params
):
2217 checkParamLen(len(params
), 2, 'ANS')
2218 if params
[1] == "OK":
2221 def handle_ACK(self
, params
):
2222 checkParamLen(len(params
), 1, 'ACK')
2223 self
._fireCallback
(int(params
[0]), None)
2225 def handle_NAK(self
, params
):
2226 checkParamLen(len(params
), 1, 'NAK')
2227 self
._fireCallback
(int(params
[0]), None)
2229 def handle_BYE(self
, params
):
2230 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2231 self
.userLeft(params
[0])
2237 called when all login details have been negotiated.
2238 Messages can now be sent, or new users invited.
2242 def gotChattingUsers(self
, users
):
2244 called after connecting to an existing chat session.
2246 @param users: A dict mapping user handles to screen names
2247 (current users taking part in the conversation)
2251 def userJoined(self
, userHandle
, screenName
):
2253 called when a user has joined the conversation.
2255 @param userHandle: the user handle (passport) of the user
2256 @param screenName: the screen name of the user
2260 def userLeft(self
, userHandle
):
2262 called when a user has left the conversation.
2264 @param userHandle: the user handle (passport) of the user.
2268 def gotMessage(self
, message
):
2270 called when we receive a message.
2272 @param message: the associated MSNMessage object
2276 def gotFileReceive(self
, fileReceive
):
2278 called when we receive a file send request from a contact.
2279 Default action is to reject the file.
2281 @param fileReceive: msnft.MSNFTReceive_Base instance
2283 fileReceive
.reject()
2286 def gotSendRequest(self
, fileReceive
):
2288 called when we receive a file send request from a contact
2290 @param fileReceive: msnft.MSNFTReceive_Base instance
2294 def gotContactTyping(self
, message
):
2296 called when we receive the special type of message notifying
2297 us that a contact is typing a message.
2299 @param message: the associated MSNMessage object
2305 def inviteUser(self
, userHandle
):
2307 used to invite a user to the current switchboard server.
2309 @param userHandle: the user handle (passport) of the desired user.
2311 @return: A Deferred, the callback for which will be called
2312 when the server notifies us that the user has indeed
2313 been invited. The callback argument will be a tuple
2314 with 1 element, the sessionID given to the invited user.
2315 I'm not sure if this is useful or not.
2318 id, d
= self
._createIDMapping
()
2319 self
.sendLine("CAL %s %s" % (id, userHandle
))
2322 def sendMessage(self
, message
):
2324 used to send a message.
2326 @param message: the corresponding MSNMessage object.
2328 @return: Depending on the value of message.ack.
2329 If set to MSNMessage.MESSAGE_ACK or
2330 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2331 the callback for which will be fired when an ACK or
2332 NACK is received - the callback argument will be
2333 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2334 the return value is None.
2337 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2338 else: id, d
= self
._createIDMapping
()
2339 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2340 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2341 # Apparently order matters with these
2342 orderMatters
= ("MIME-Version", "Content-Type", "Message-ID")
2343 for header
in orderMatters
:
2344 if message
.hasHeader(header
):
2345 self
.sendLine("%s: %s" % (header
, message
.getHeader(header
)))
2346 # send the rest of the headers
2347 for header
in [h
for h
in message
.headers
.items() if h
[0] not in orderMatters
]:
2348 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2349 self
.transport
.write("\r\n")
2350 self
.transport
.write(message
.message
)
2351 if MESSAGEDEBUG
: log
.msg(message
.message
)
2354 def sendAvatarRequest(self
, msnContact
):
2356 used to request an avatar from a user in this switchboard
2359 @param msnContact: the msnContact object to request an avatar for
2361 @return: A Deferred, the callback for which will be called
2362 when the avatar transfer succeeds.
2363 The callback argument will be a tuple with one element,
2364 the PNG avatar data.
2366 if not msnContact
.msnobj
: return
2368 def bufferClosed(data
):
2370 buffer = StringBuffer(bufferClosed
)
2371 slpLink
= SLPLink_AvatarReceive(remoteUser
=msnContact
.userHandle
, switchboard
=self
, consumer
=buffer, context
=msnContact
.msnobj
.text
)
2372 self
.slpLinks
[slpLink
.sessionID
] = slpLink
2375 def sendFile(self
, msnContact
, filename
, filesize
):
2377 used to send a file to a contact.
2379 @param msnContact: the MSNContact object to send a file to.
2380 @param filename: the name of the file to send.
2381 @param filesize: the size of the file to send.
2383 @return: (fileSend, d) A FileSend object and a Deferred.
2384 The Deferred will pass one argument in a tuple,
2385 whether or not the transfer is accepted. If you
2386 receive a True, then you can call write() on the
2387 fileSend object to send your file. Call close()
2388 when the file is done.
2389 NOTE: You MUST write() exactly as much as you
2390 declare in filesize.
2392 if not msnContact
.userHandle
: return
2393 # FIXME, check msnContact.caps to see if we should use old-style
2394 fileSend
= SLPLink_FileSend(remoteUser
=msnContact
.userHandle
, switchboard
=self
, filename
=filename
, filesize
=filesize
)
2395 self
.slpLinks
[fileSend
.sessionID
] = fileSend
2396 return fileSend
, fileSend
.acceptDeferred
2398 def sendTypingNotification(self
):
2400 Used to send a typing notification. Upon receiving this
2401 message the official client will display a 'user is typing'
2402 message to all other users in the chat session for 10 seconds.
2403 You should send one of these every 5 seconds as long as the
2407 m
.ack
= m
.MESSAGE_ACK_NONE
2408 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2409 m
.setHeader('TypingUser', self
.userHandle
)
2413 def sendFileInvitation(self
, fileName
, fileSize
):
2415 send an notification that we want to send a file.
2417 @param fileName: the file name
2418 @param fileSize: the file size
2420 @return: A Deferred, the callback of which will be fired
2421 when the user responds to this invitation with an
2422 appropriate message. The callback argument will be
2423 a tuple with 3 elements, the first being 1 or 0
2424 depending on whether they accepted the transfer
2425 (1=yes, 0=no), the second being an invitation cookie
2426 to identify your follow-up responses and the third being
2427 the message 'info' which is a dict of information they
2428 sent in their reply (this doesn't really need to be used).
2429 If you wish to proceed with the transfer see the
2430 sendTransferInfo method.
2432 cookie
= self
._newInvitationCookie
()
2435 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2436 m
.message
+= 'Application-Name: File Transfer\r\n'
2437 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2438 m
.message
+= 'Invitation-Command: INVITE\r\n'
2439 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2440 m
.message
+= 'Application-File: %s\r\n' % fileName
2441 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2442 m
.ack
= m
.MESSAGE_ACK_NONE
2444 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2447 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2449 send information relating to a file transfer session.
2451 @param accept: whether or not to go ahead with the transfer
2453 @param iCookie: the invitation cookie of previous replies
2454 relating to this transfer
2455 @param authCookie: the authentication cookie obtained from
2456 an FileSend instance
2458 @param port: the port on which an FileSend protocol is listening.
2461 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2462 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2463 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2464 m
.message
+= 'IP-Address: %s\r\n' % ip
2465 m
.message
+= 'Port: %s\r\n' % port
2466 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2468 m
.ack
= m
.MESSAGE_NACK
2473 def __init__(self
, filename
, filesize
, userHandle
):
2474 self
.consumer
= None
2475 self
.finished
= False
2478 self
.filename
, self
.filesize
, self
.userHandle
= filename
, filesize
, userHandle
2481 raise NotImplementedError
2483 def accept(self
, consumer
):
2484 if self
.consumer
: raise "AlreadyAccepted"
2485 self
.consumer
= consumer
2486 for data
in self
.buffer:
2487 self
.consumer
.write(data
)
2490 self
.consumer
.close()
2492 self
.consumer
.error()
2494 def write(self
, data
):
2495 if self
.error
or self
.finished
:
2496 raise IOError, "Attempt to write in an invalid state"
2498 self
.consumer
.write(data
)
2500 self
.buffer.append(data
)
2503 self
.finished
= True
2505 self
.consumer
.close()
2508 """ Represents the Context field for P2P file transfers """
2509 def __init__(self
, data
=""):
2517 if MSNP2PDEBUG
: log
.msg("FileContext packing:", self
.filename
, self
.filesize
)
2518 data
= struct
.pack("<LLQL", 638, 0x03, self
.filesize
, 0x01)
2519 data
= data
[:-1] # Uck, weird, but it works
2520 data
+= utf16net(self
.filename
)
2521 data
= ljust(data
, 570, '\0')
2522 data
+= struct
.pack("<L", 0xFFFFFFFFL
)
2523 data
= ljust(data
, 638, '\0')
2526 def parse(self
, packet
):
2527 self
.filesize
= struct
.unpack("<Q", packet
[8:16])[0]
2528 chunk
= packet
[19:540]
2529 chunk
= chunk
[:chunk
.find('\x00\x00')]
2530 self
.filename
= unicode((codecs
.BOM_UTF16_BE
+ chunk
).decode("utf-16"))
2531 if MSNP2PDEBUG
: log
.msg("FileContext parsed:", self
.filesize
, self
.filename
)
2535 """ Utility class for the binary header & footer in p2p messages """
2544 def __init__(self
, fields
=None, packet
=None):
2546 self
.fields
= fields
2548 self
.fields
= [0] * 10
2550 self
.unpackFields(packet
)
2552 def __getitem__(self
, key
):
2553 return self
.fields
[key
]
2555 def __setitem__(self
, key
, value
):
2556 self
.fields
[key
] = value
2558 def unpackFields(self
, packet
):
2559 self
.fields
= struct
.unpack("<LLQQLLLLQ", packet
[0:48])
2560 self
.fields
+= struct
.unpack(">L", packet
[len(packet
)-4:])
2562 out
= "Unpacked fields: "
2563 for i
in self
.fields
:
2567 def packHeaders(self
):
2568 f
= tuple(self
.fields
)
2570 out
= "Packed fields: "
2571 for i
in self
.fields
:
2574 return struct
.pack("<LLQQLLLLQ", f
[0], f
[1], f
[2], f
[3], f
[4], f
[5], f
[6], f
[7], f
[8])
2576 def packFooter(self
):
2577 return struct
.pack(">L", self
.fields
[9])
2580 class MSNSLPMessage
:
2581 """ Representation of a single MSNSLP message """
2582 def __init__(self
, packet
=None):
2589 self
.sessionGuid
= ""
2590 self
.sessionID
= None
2592 self
.data
= "\r\n" + chr(0)
2596 def create(self
, method
=None, status
=None, to
=None, fro
=None, branch
=None, cseq
=0, sessionGuid
=None, data
=None):
2597 self
.method
= method
2598 self
.status
= status
2601 self
.branch
= branch
2603 self
.sessionGuid
= sessionGuid
2604 if data
: self
.data
= data
2606 def setData(self
, ctype
, data
):
2609 order
= ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
2611 if key
== "Context" and data
.has_key(key
):
2612 s
.append("Context: %s\r\n" % b64enc(data
[key
]))
2613 elif data
.has_key(key
):
2614 s
.append("%s: %s\r\n" % (key
, str(data
[key
])))
2615 s
.append("\r\n"+chr(0))
2617 self
.data
= "".join(s
)
2621 if s
.find("MSNSLP/1.0") < 0: return
2623 lines
= s
.split("\r\n")
2625 # Get the MSNSLP method or status
2626 msnslp
= lines
[0].split(" ")
2627 if MSNP2PDEBUG
: log
.msg("Parsing MSNSLPMessage %s %s" % (len(s
), s
))
2628 if msnslp
[0] in ("INVITE", "BYE"):
2629 self
.method
= msnslp
[0].strip()
2631 self
.status
= msnslp
[1].strip()
2633 lines
.remove(lines
[0])
2636 line
= line
.split(":")
2637 if len(line
) < 1: continue
2639 if len(line
) > 2 and line
[0] == "To":
2640 self
.to
= line
[2][:line
[2].find('>')]
2641 elif len(line
) > 2 and line
[0] == "From":
2642 self
.fro
= line
[2][:line
[2].find('>')]
2643 elif line
[0] == "Call-ID":
2644 self
.sessionGuid
= line
[1].strip()
2645 elif line
[0] == "CSeq":
2646 self
.cseq
= int(line
[1].strip())
2647 elif line
[0] == "SessionID":
2648 self
.sessionID
= int(line
[1].strip())
2649 elif line
[0] == "EUF-GUID":
2650 self
.euf_guid
= line
[1].strip()
2651 elif line
[0] == "Content-Type":
2652 self
.ctype
= line
[1].strip()
2653 elif line
[0] == "Context":
2654 self
.context
= b64dec(line
[1])
2655 elif line
[0] == "Via":
2656 self
.branch
= line
[1].split(";")[1].split("=")[1].strip()
2659 log
.msg("Error parsing MSNSLP message.")
2665 s
.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self
.method
, self
.to
))
2667 if self
.status
== "200": status
= "200 OK"
2668 elif self
.status
== "603": status
= "603 Decline"
2669 s
.append("MSNSLP/1.0 %s\r\n" % status
)
2670 s
.append("To: <msnmsgr:%s>\r\n" % self
.to
)
2671 s
.append("From: <msnmsgr:%s>\r\n" % self
.fro
)
2672 s
.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % self
.branch
)
2673 s
.append("CSeq: %s \r\n" % str(self
.cseq
))
2674 s
.append("Call-ID: %s\r\n" % self
.sessionGuid
)
2675 s
.append("Max-Forwards: 0\r\n")
2676 s
.append("Content-Type: %s\r\n" % self
.ctype
)
2677 s
.append("Content-Length: %s\r\n\r\n" % len(self
.data
))
2682 """ Utility for handling the weird sequence IDs in p2p messages """
2683 def __init__(self
, baseID
=None):
2685 self
.baseID
= baseID
2687 self
.baseID
= random
.randint(1000, sys
.maxint
)
2691 return p2pseq(self
.pos
) + self
.baseID
2698 class StringBuffer(StringIO
.StringIO
):
2699 def __init__(self
, notifyFunc
=None):
2700 self
.notifyFunc
= notifyFunc
2701 StringIO
.StringIO
.__init
__(self
)
2705 self
.notifyFunc(self
.getvalue())
2706 self
.notifyFunc
= None
2707 StringIO
.StringIO
.close(self
)
2711 def __init__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
):
2714 sessionID
= random
.randint(1000, sys
.maxint
)
2716 sessionGuid
= random_guid()
2717 self
.remoteUser
= remoteUser
2718 self
.switchboard
= switchboard
2719 self
.sessionID
= sessionID
2720 self
.sessionGuid
= sessionGuid
2721 self
.seqID
= SeqID()
2724 if MSNP2PDEBUG
: log
.msg("killLink")
2726 if MSNP2PDEBUG
: log
.msg("killLink - kill()")
2727 if not self
.switchboard
: return
2728 del self
.switchboard
.slpLinks
[self
.sessionID
]
2729 self
.switchboard
= None
2730 # This is so that handleP2PMessage can still use the SLPLink
2731 # one last time, for ACKing BYEs and 601s.
2732 reactor
.callLater(0, kill
)
2734 def warn(self
, text
):
2735 log
.msg("Warning in transfer: %s %s" % (self
, text
))
2737 def sendP2PACK(self
, ackHeaders
):
2738 binaryFields
= BinaryFields()
2739 binaryFields
[0] = ackHeaders
[0]
2740 binaryFields
[1] = self
.seqID
.next()
2741 binaryFields
[3] = ackHeaders
[3]
2742 binaryFields
[5] = BinaryFields
.ACK
2743 binaryFields
[6] = ackHeaders
[1]
2744 binaryFields
[7] = ackHeaders
[6]
2745 binaryFields
[8] = ackHeaders
[3]
2746 self
.sendP2PMessage(binaryFields
, "")
2748 def sendSLPMessage(self
, cmd
, ctype
, data
, branch
=None):
2749 msg
= MSNSLPMessage()
2751 msg
.create(status
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=branch
, cseq
=1, sessionGuid
=self
.sessionGuid
)
2753 msg
.create(method
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=random_guid(), cseq
=0, sessionGuid
=self
.sessionGuid
)
2754 msg
.setData(ctype
, data
)
2756 binaryFields
= BinaryFields()
2757 binaryFields
[1] = self
.seqID
.next()
2758 binaryFields
[3] = len(msgStr
)
2759 binaryFields
[4] = binaryFields
[3]
2760 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2761 self
.sendP2PMessage(binaryFields
, msgStr
)
2763 def sendP2PMessage(self
, binaryFields
, msgStr
):
2764 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2766 message
= MSNMessage(message
=packet
)
2767 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2768 message
.setHeader("P2P-Dest", self
.remoteUser
)
2769 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2770 self
.switchboard
.sendMessage(message
)
2772 def handleSLPMessage(self
, slpMessage
):
2773 raise NotImplementedError
2779 class SLPLink_Send(SLPLink
):
2780 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2781 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2782 self
.handlePacket
= None
2784 self
.filesize
= filesize
2787 def send_dataprep(self
):
2788 if MSNP2PDEBUG
: log
.msg("send_dataprep")
2789 binaryFields
= BinaryFields()
2790 binaryFields
[0] = self
.sessionID
2791 binaryFields
[1] = self
.seqID
.next()
2794 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2796 self
.sendP2PMessage(binaryFields
, chr(0) * 4)
2798 def write(self
, data
):
2799 if MSNP2PDEBUG
: log
.msg("write")
2801 data
= self
.data
+ data
2805 if i
+ 1202 < length
:
2806 self
._writeChunk
(data
[i
:i
+1202])
2809 self
.data
= data
[i
:]
2812 def _writeChunk(self
, chunk
):
2813 if MSNP2PDEBUG
: log
.msg("writing chunk")
2814 binaryFields
= BinaryFields()
2815 binaryFields
[0] = self
.sessionID
2816 if self
.offset
== 0:
2817 binaryFields
[1] = self
.seqID
.next()
2819 binaryFields
[1] = self
.seqID
.get()
2820 binaryFields
[2] = self
.offset
2821 binaryFields
[3] = self
.filesize
2822 binaryFields
[4] = len(chunk
)
2823 binaryFields
[5] = self
.dataFlag
2824 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2826 self
.offset
+= len(chunk
)
2827 self
.sendP2PMessage(binaryFields
, chunk
)
2831 self
._writeChunk
(self
.data
)
2836 # FIXME, should send 601 or something
2838 class SLPLink_FileSend(SLPLink_Send
):
2839 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
):
2840 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
)
2841 self
.dataFlag
= BinaryFields
.DATAFT
2842 # Send invite & wait for 200OK before sending dataprep
2843 context
= FileContext()
2844 context
.filename
= filename
2845 context
.filesize
= filesize
2846 data
= {"EUF-GUID" : MSN_MSNFTP_GUID
,\
2847 "SessionID": self
.sessionID
,\
2849 "Context" : context
.pack() }
2850 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2851 self
.acceptDeferred
= Deferred()
2853 def handleSLPMessage(self
, slpMessage
):
2854 if slpMessage
.status
== "200":
2855 if slpMessage
.ctype
== "application/x-msnmsgr-sessionreqbody":
2856 data
= {"Bridges" : "TRUDPv1 TCPv1",\
2858 "Conn-Type" : "Firewall",\
2859 "UPnPNat" : "false",\
2861 #"Hashed-Nonce": random_guid()}
2862 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data
)
2863 elif slpMessage
.ctype
== "application/x-msnmsgr-transrespbody":
2864 self
.acceptDeferred
.callback((True,))
2865 self
.handlePacket
= self
.wait_data_ack
2867 if slpMessage
.status
== "603":
2868 self
.acceptDeferred
.callback((False,))
2869 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to decline, error or BYE")
2872 def wait_data_ack(self
, packet
):
2873 if MSNP2PDEBUG
: log
.msg("wait_data_ack")
2874 binaryFields
= BinaryFields()
2875 binaryFields
.unpackFields(packet
)
2877 if binaryFields
[5] != BinaryFields
.ACK
:
2878 self
.warn("field5," + str(binaryFields
[5]))
2881 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2882 self
.handlePacket
= None
2885 self
.handlePacket
= self
.wait_data_ack
2886 SLPLink_Send
.close(self
)
2889 class SLPLink_AvatarSend(SLPLink_Send
):
2890 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2891 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2892 self
.dataFlag
= BinaryFields
.DATA
2893 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2894 self
.send_dataprep()
2895 self
.handlePacket
= lambda packet
: None
2897 def handleSLPMessage(self
, slpMessage
):
2898 if MSNP2PDEBUG
: log
.msg("BYE or error")
2902 SLPLink_Send
.close(self
)
2903 # Keep the link open to wait for a BYE
2905 class SLPLink_Receive(SLPLink
):
2906 def __init__(self
, remoteUser
, switchboard
, consumer
, context
=None, sessionID
=None, sessionGuid
=None):
2907 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2908 self
.handlePacket
= None
2909 self
.consumer
= consumer
2912 def wait_dataprep(self
, packet
):
2913 if MSNP2PDEBUG
: log
.msg("wait_dataprep")
2914 binaryFields
= BinaryFields()
2915 binaryFields
.unpackFields(packet
)
2917 if binaryFields
[3] != 4:
2918 self
.warn("field3," + str(binaryFields
[3]))
2920 if binaryFields
[4] != 4:
2921 self
.warn("field4," + str(binaryFields
[4]))
2923 # Just ignore the footer
2924 #if binaryFields[9] != 1:
2925 # self.warn("field9," + str(binaryFields[9]))
2928 self
.sendP2PACK(binaryFields
)
2929 self
.handlePacket
= self
.wait_data
2931 def wait_data(self
, packet
):
2932 if MSNP2PDEBUG
: log
.msg("wait_data")
2933 binaryFields
= BinaryFields()
2934 binaryFields
.unpackFields(packet
)
2936 if binaryFields
[5] != self
.dataFlag
:
2937 self
.warn("field5," + str(binaryFields
[5]))
2939 # Just ignore the footer
2940 #if binaryFields[9] != 1:
2941 # self.warn("field9," + str(binaryFields[9]))
2943 offset
= binaryFields
[2]
2944 total
= binaryFields
[3]
2945 length
= binaryFields
[4]
2947 data
= packet
[48:-4]
2948 if offset
!= self
.pos
:
2949 self
.warn("Received packet out of order")
2950 self
.consumer
.error()
2952 if len(data
) != length
:
2953 self
.warn("Received bad length of slp")
2954 self
.consumer
.error()
2959 self
.consumer
.write(str(data
))
2961 if self
.pos
== total
:
2962 self
.sendP2PACK(binaryFields
)
2963 self
.consumer
.close()
2964 self
.handlePacket
= None
2967 def doFinished(self
):
2968 raise NotImplementedError
2971 class SLPLink_FileReceive(SLPLink_Receive
, FileReceive
):
2972 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
, sessionID
, sessionGuid
, branch
):
2973 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=self
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2974 self
.dataFlag
= BinaryFields
.DATAFT
2975 self
.initialBranch
= branch
2976 FileReceive
.__init
__(self
, filename
, filesize
, remoteUser
)
2979 # Send a 603 decline
2980 if not self
.switchboard
: return
2981 self
.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2984 def accept(self
, consumer
):
2985 FileReceive
.accept(self
, consumer
)
2986 if not self
.switchboard
: return
2987 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2988 self
.handlePacket
= self
.wait_data
# Moved here because sometimes the second INVITE seems to be skipped
2990 def handleSLPMessage(self
, slpMessage
):
2991 if slpMessage
.method
== "INVITE": # The second invite
2992 data
= {"Bridge" : "TCPv1",\
2993 "Listening" : "false",\
2994 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
2995 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
2996 # self.handlePacket = self.wait_data # Moved up
2998 if MSNP2PDEBUG
: log
.msg("It's either a BYE or an error")
3000 # FIXME, do some error handling if it was an error
3002 def doFinished(self
):
3003 #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3005 # Wait for BYE? #FIXME
3008 class SLPLink_AvatarReceive(SLPLink_Receive
):
3009 def __init__(self
, remoteUser
, switchboard
, consumer
, context
):
3010 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=consumer
, context
=context
)
3011 self
.dataFlag
= BinaryFields
.DATA
3012 data
= {"EUF-GUID" : MSN_AVATAR_GUID
,\
3013 "SessionID": self
.sessionID
,\
3015 "Context" : context
}
3016 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
3018 def handleSLPMessage(self
, slpMessage
):
3019 if slpMessage
.status
== "200":
3020 self
.handlePacket
= self
.wait_dataprep
3022 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to error or BYE")
3025 def doFinished(self
):
3026 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3028 # mapping of error codes to error messages
3031 200 : "Syntax error",
3032 201 : "Invalid parameter",
3033 205 : "Invalid user",
3034 206 : "Domain name missing",
3035 207 : "Already logged in",
3036 208 : "Invalid username",
3037 209 : "Invalid screen name",
3038 210 : "User list full",
3039 215 : "User already there",
3040 216 : "User already on list",
3041 217 : "User not online",
3042 218 : "Already in mode",
3043 219 : "User is in the opposite list",
3044 223 : "Too many groups",
3045 224 : "Invalid group",
3046 225 : "User not in group",
3047 229 : "Group name too long",
3048 230 : "Cannot remove group 0",
3049 231 : "Invalid group",
3050 280 : "Switchboard failed",
3051 281 : "Transfer to switchboard failed",
3053 300 : "Required field missing",
3054 301 : "Too many FND responses",
3055 302 : "Not logged in",
3057 402 : "Error accessing contact list",
3058 403 : "Error accessing contact list",
3060 500 : "Internal server error",
3061 501 : "Database server error",
3062 502 : "Command disabled",
3063 510 : "File operation failed",
3064 520 : "Memory allocation failed",
3065 540 : "Wrong CHL value sent to server",
3067 600 : "Server is busy",
3068 601 : "Server is unavaliable",
3069 602 : "Peer nameserver is down",
3070 603 : "Database connection failed",
3071 604 : "Server is going down",
3072 605 : "Server unavailable",
3074 707 : "Could not create connection",
3075 710 : "Invalid CVR parameters",
3076 711 : "Write is blocking",
3077 712 : "Session is overloaded",
3078 713 : "Too many active users",
3079 714 : "Too many sessions",
3080 715 : "Not expected",
3081 717 : "Bad friend file",
3082 731 : "Not expected",
3084 800 : "Requests too rapid",
3086 910 : "Server too busy",
3087 911 : "Authentication failed",
3088 912 : "Server too busy",
3089 913 : "Not allowed when offline",
3090 914 : "Server too busy",
3091 915 : "Server too busy",
3092 916 : "Server too busy",
3093 917 : "Server too busy",
3094 918 : "Server too busy",
3095 919 : "Server too busy",
3096 920 : "Not accepting new users",
3097 921 : "Server too busy",
3098 922 : "Server too busy",
3099 923 : "No parent consent",
3100 924 : "Passport account not yet verified"
3104 # mapping of status codes to readable status format
3107 STATUS_ONLINE
: "Online",
3108 STATUS_OFFLINE
: "Offline",
3109 STATUS_HIDDEN
: "Appear Offline",
3110 STATUS_IDLE
: "Idle",
3111 STATUS_AWAY
: "Away",
3112 STATUS_BUSY
: "Busy",
3113 STATUS_BRB
: "Be Right Back",
3114 STATUS_PHONE
: "On the Phone",
3115 STATUS_LUNCH
: "Out to Lunch"
3119 # mapping of list ids to list codes
3122 FORWARD_LIST
: 'fl',
3125 REVERSE_LIST
: 'rl',
3130 # mapping of list codes to list ids
3132 for id,code
in listIDToCode
.items():
3133 listCodeToID
[code
] = id