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
91 from twisted
.protocols
.basic
import LineReceiver
92 from twisted
.web
.http
import HTTPClient
96 from twisted
.internet
import reactor
, task
97 from twisted
.internet
.defer
import Deferred
98 from twisted
.internet
.protocol
import ReconnectingClientFactory
, ClientFactory
100 from twisted
.internet
.ssl
import ClientContextFactory
102 print "You must install pycrypto and pyopenssl."
104 from twisted
.python
import failure
, log
105 from twisted
.words
.xish
.domish
import parseText
, unescapeFromXml
109 import types
, operator
, os
, sys
, base64
, random
, struct
, random
, sha
, base64
, StringIO
, array
, codecs
, binascii
110 from urllib
import quote
, unquote
113 MSN_PROTOCOL_VERSION
= "MSNP11 CVR0" # protocol version
114 MSN_PORT
= 1863 # default dispatch server port
115 MSN_MAX_MESSAGE
= 1664 # max message length
116 MSN_CVR_STR
= "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
117 MSN_AVATAR_GUID
= "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
118 MSN_MSNFTP_GUID
= "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
119 MSN_MAXINT
= 2**31 - 1
141 STATUS_ONLINE
= 'NLN'
142 STATUS_OFFLINE
= 'FLN'
143 STATUS_HIDDEN
= 'HDN'
158 P2PSEQ
= [-3, -2, 0, -1, 1, 2, 3, 4, 5, 6, 7, 8]
167 return inp
.split('=')[1]
179 userHandle
= getVal(p
)
181 screenName
= unquote(getVal(p
))
186 else: # Must be the groups
188 groups
= p
.split(',')
190 raise MSNProtocolError
, "Unknown LST/ADC response" + str(params
) # debug
192 return userHandle
, screenName
, userGuid
, lists
, groups
195 """ Needed for Python 2.3 compatibility """
196 return s
+ (n
-len(s
))*c
198 if sys
.byteorder
== "little":
200 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
201 a
= array
.array("h", s
.encode("utf-16")[2:])
206 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
207 return s
.encode("utf-16")[2:]
210 return base64
.encodestring(s
).replace("\n", "")
213 for pad
in ["", "=", "==", "A", "A=", "A=="]: # Stupid MSN client!
215 return base64
.decodestring(s
+ pad
)
218 raise ValueError("Got some very bad base64!")
221 format
= "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
224 data
.append(random
.random() * 0xAAFF + 0x1111)
229 def checkParamLen(num
, expected
, cmd
, error
=None):
230 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
231 if num
!= expected
: raise MSNProtocolError
, error
233 def _parseHeader(h
, v
):
235 Split a certin number of known
236 header values with the format:
237 field1=val,field2=val,field3=val into
238 a dict mapping fields to values.
239 @param h: the header's key
240 @param v: the header's value as a string
243 if h
in ('passporturls','authentication-info','www-authenticate'):
244 v
= v
.replace('Passport1.4','').lstrip()
246 for fieldPair
in v
.split(','):
248 field
,value
= fieldPair
.split('=',1)
249 fields
[field
.lower()] = value
251 fields
[field
.lower()] = ''
255 def _parsePrimitiveHost(host
):
257 h
,p
= host
.replace('https://','').split('/',1)
261 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
=''):
263 This function is used internally and should not ever be called
267 def _cb(server
, auth
):
268 loginFac
= ClientFactory()
269 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
270 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
273 _cb(nexusServer
, authData
)
275 fac
= ClientFactory()
277 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
278 d
.addErrback(lambda f
: cb
.errback(f
))
279 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
280 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
284 class PassportNexus(HTTPClient
):
287 Used to obtain the URL of a valid passport
290 This class is used internally and should
291 not be instantiated directly -- that is,
292 The passport logging in process is handled
293 transparantly by NotificationClient.
296 def __init__(self
, deferred
, host
):
297 self
.deferred
= deferred
298 self
.host
, self
.path
= _parsePrimitiveHost(host
)
300 def connectionMade(self
):
301 HTTPClient
.connectionMade(self
)
302 self
.sendCommand('GET', self
.path
)
303 self
.sendHeader('Host', self
.host
)
307 def handleHeader(self
, header
, value
):
309 self
.headers
[h
] = _parseHeader(h
, value
)
311 def handleEndHeaders(self
):
312 if self
.connected
: self
.transport
.loseConnection()
313 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
314 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
316 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
318 def handleResponse(self
, r
): pass
320 class PassportLogin(HTTPClient
):
322 This class is used internally to obtain
323 a login ticket from a passport HTTPS
324 server -- it should not be used directly.
329 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
330 self
.deferred
= deferred
331 self
.userHandle
= userHandle
333 self
.authData
= authData
334 self
.host
, self
.path
= _parsePrimitiveHost(host
)
336 def connectionMade(self
):
337 self
.sendCommand('GET', self
.path
)
338 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
339 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), quote(self
.passwd
), self
.authData
))
340 self
.sendHeader('Host', self
.host
)
344 def handleHeader(self
, header
, value
):
346 self
.headers
[h
] = _parseHeader(h
, value
)
348 def handleEndHeaders(self
):
349 if self
._finished
: return
350 self
._finished
= 1 # I think we need this because of HTTPClient
351 if self
.connected
: self
.transport
.loseConnection()
352 authHeader
= 'authentication-info'
353 _interHeader
= 'www-authenticate'
354 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
356 info
= self
.headers
[authHeader
]
357 status
= info
['da-status']
358 handler
= getattr(self
, 'login_%s' % (status
,), None)
361 else: raise Exception()
363 self
.deferred
.errback(failure
.Failure(e
))
365 def handleResponse(self
, r
): pass
367 def login_success(self
, info
):
368 ticket
= info
['from-pp']
369 ticket
= ticket
[1:len(ticket
)-1]
370 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
372 def login_failed(self
, info
):
373 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
375 def login_redir(self
, info
):
376 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
378 class MSNProtocolError(Exception):
380 This Exception is basically used for debugging
381 purposes, as the official MSN server should never
382 send anything _wrong_ and nobody in their right
383 mind would run their B{own} MSN server.
384 If it is raised by default command handlers
385 (handle_BLAH) the error will be logged.
392 I am the class used to represent an 'instant' message.
394 @ivar userHandle: The user handle (passport) of the sender
395 (this is only used when receiving a message)
396 @ivar screenName: The screen name of the sender (this is only used
397 when receiving a message)
398 @ivar message: The message
399 @ivar headers: The message headers
401 @ivar length: The message length (including headers and line endings)
402 @ivar ack: This variable is used to tell the server how to respond
403 once the message has been sent. If set to MESSAGE_ACK
404 (default) the server will respond with an ACK upon receiving
405 the message, if set to MESSAGE_NACK the server will respond
406 with a NACK upon failure to receive the message.
407 If set to MESSAGE_ACK_NONE the server will do nothing.
408 This is relevant for the return value of
409 SwitchboardClient.sendMessage (which will return
410 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
411 and will fire when the respective ACK or NACK is received).
412 If set to MESSAGE_ACK_NONE sendMessage will return None.
415 MESSAGE_ACK_FAT
= 'D'
417 MESSAGE_ACK_NONE
= 'U'
421 def __init__(self
, length
=0, userHandle
="", screenName
="", message
="", specialMessage
=False):
422 self
.userHandle
= userHandle
423 self
.screenName
= screenName
424 self
.specialMessage
= specialMessage
425 self
.message
= message
426 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain; charset=UTF-8'}
430 def _calcMessageLen(self
):
432 used to calculte the number to send
433 as the message length when sending a message.
435 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
437 def delHeader(self
, header
):
438 """ delete the desired header """
439 if self
.headers
.has_key(header
):
440 del self
.headers
[header
]
442 def setHeader(self
, header
, value
):
443 """ set the desired header """
444 self
.headers
[header
] = value
446 def getHeader(self
, header
):
448 get the desired header value
449 @raise KeyError: if no such header exists.
451 return self
.headers
[header
]
453 def hasHeader(self
, header
):
454 """ check to see if the desired header exists """
455 return self
.headers
.has_key(header
)
457 def getMessage(self
):
458 """ return the message - not including headers """
461 def setMessage(self
, message
):
462 """ set the message text """
463 self
.message
= message
468 Used to represent a MSNObject. This can be currently only be an avatar.
470 @ivar creator: The userHandle of the creator of this picture.
471 @ivar imageDataFunc: A function to return the PNG image data (only for our own avatar)
472 @ivar type: Always set to 3, for avatar.
473 @ivar size: The size of the image.
474 @ivar location: The filename of the image.
475 @ivar friendly: Unknown.
476 @ivar text: The textual representation of this MSNObject.
478 def __init__(self
, s
=""):
479 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
484 def setData(self
, creator
, imageDataFunc
):
485 """ Set the creator and imageData for this object """
486 imageData
= imageDataFunc()
487 self
.creator
= creator
488 self
.imageDataFunc
= imageDataFunc
489 self
.size
= len(imageData
)
491 self
.location
= "TMP" + str(random
.randint(1000,9999))
492 self
.friendly
= "AAA="
493 self
.sha1d
= b64enc(sha
.sha(imageData
).digest())
498 self
.imageDataFunc
= lambda: None
507 """ Makes a textual representation of this MSNObject. Stores it in self.text """
510 h
.append(self
.creator
)
512 h
.append(str(self
.size
))
514 h
.append(str(self
.type))
516 h
.append(self
.location
)
518 h
.append(self
.friendly
)
521 sha1c
= b64enc(sha
.sha("".join(h
)).digest())
522 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
)
525 e
= parseText(s
, True)
527 return # Parse failed
529 self
.creator
= e
.getAttribute("Creator")
530 self
.size
= int(e
.getAttribute("Size"))
531 self
.type = int(e
.getAttribute("Type"))
532 self
.location
= e
.getAttribute("Location")
533 self
.friendly
= e
.getAttribute("Friendly")
534 self
.sha1d
= e
.getAttribute("SHA1D")
545 This class represents a contact (user).
547 @ivar userGuid: The contact's user guid (unique string)
548 @ivar userHandle: The contact's user handle (passport).
549 @ivar screenName: The contact's screen name.
550 @ivar groups: A list of all the group IDs which this
552 @ivar lists: An integer representing the sum of all lists
553 that this contact belongs to.
554 @ivar caps: int, The capabilities of this client
555 @ivar msnobj: The MSNObject representing the contact's avatar
556 @ivar status: The contact's status code.
557 @type status: str if contact's status is known, None otherwise.
558 @ivar personal: The contact's personal message .
559 @type personal: str if contact's personal message is known, None otherwise.
561 @ivar homePhone: The contact's home phone number.
562 @type homePhone: str if known, otherwise None.
563 @ivar workPhone: The contact's work phone number.
564 @type workPhone: str if known, otherwise None.
565 @ivar mobilePhone: The contact's mobile phone number.
566 @type mobilePhone: str if known, otherwise None.
567 @ivar hasPager: Whether or not this user has a mobile pager
568 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
576 def __init__(self
, userGuid
="", userHandle
="", screenName
="", lists
=0, caps
=0, msnobj
=None, groups
={}, status
=None, personal
=""):
577 self
.userGuid
= userGuid
578 self
.userHandle
= userHandle
579 self
.screenName
= screenName
583 self
.msnobjGot
= True
584 self
.groups
= [] # if applicable
585 self
.status
= status
# current status
586 self
.personal
= personal
589 self
.homePhone
= None
590 self
.workPhone
= None
591 self
.mobilePhone
= None
595 def setPhone(self
, phoneType
, value
):
597 set phone numbers/values for this specific user.
598 for phoneType check the *_PHONE constants and HAS_PAGER
601 t
= phoneType
.upper()
602 if t
== HOME_PHONE
: self
.homePhone
= value
603 elif t
== WORK_PHONE
: self
.workPhone
= value
604 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
605 elif t
== HAS_PAGER
: self
.hasPager
= value
606 elif t
== HAS_BLOG
: self
.hasBlog
= value
607 #else: raise ValueError, "Invalid Phone Type: " + t
609 def addToList(self
, listType
):
611 Update the lists attribute to
612 reflect being part of the
615 self
.lists |
= listType
617 def removeFromList(self
, listType
):
619 Update the lists attribute to
620 reflect being removed from the
623 self
.lists ^
= listType
625 class MSNContactList
:
627 This class represents a basic MSN contact list.
629 @ivar contacts: All contacts on my various lists
630 @type contacts: dict (mapping user handles to MSNContact objects)
631 @ivar groups: a mapping of group ids to group names
632 (groups can only exist on the forward list)
636 This is used only for storage and doesn't effect the
637 server's contact list.
647 def _getContactsFromList(self
, listType
):
649 Obtain all contacts which belong
650 to the given list type.
652 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
654 def addContact(self
, contact
):
658 self
.contacts
[contact
.userHandle
] = contact
660 def remContact(self
, userHandle
):
665 del self
.contacts
[userHandle
]
666 except KeyError: pass
668 def getContact(self
, userHandle
):
670 Obtain the MSNContact object
671 associated with the given
673 @return: the MSNContact object if
674 the user exists, or None.
677 return self
.contacts
[userHandle
]
681 def getBlockedContacts(self
):
683 Obtain all the contacts on my block list
685 return self
._getContactsFromList
(BLOCK_LIST
)
687 def getAuthorizedContacts(self
):
689 Obtain all the contacts on my auth list.
690 (These are contacts which I have verified
691 can view my state changes).
693 return self
._getContactsFromList
(ALLOW_LIST
)
695 def getReverseContacts(self
):
697 Get all contacts on my reverse list.
698 (These are contacts which have added me
699 to their forward list).
701 return self
._getContactsFromList
(REVERSE_LIST
)
703 def getContacts(self
):
705 Get all contacts on my forward list.
706 (These are the contacts which I have added
709 return self
._getContactsFromList
(FORWARD_LIST
)
711 def setGroup(self
, id, name
):
713 Keep a mapping from the given id
716 self
.groups
[id] = name
718 def remGroup(self
, id):
720 Removed the stored group
721 mapping for the given id.
725 except KeyError: pass
726 for c
in self
.contacts
:
727 if id in c
.groups
: c
.groups
.remove(id)
730 class MSNEventBase(LineReceiver
):
732 This class provides support for handling / dispatching events and is the
733 base class of the three main client protocols (DispatchClient,
734 NotificationClient, SwitchboardClient)
738 self
.ids
= {} # mapping of ids to Deferreds
742 self
.currentMessage
= None
744 def connectionLost(self
, reason
):
748 def connectionMade(self
):
751 def _fireCallback(self
, id, *args
):
753 Fire the callback for the given id
754 if one exists and return 1, else return false
756 if self
.ids
.has_key(id):
757 self
.ids
[id][0].callback(args
)
762 def _nextTransactionID(self
):
763 """ return a usable transaction ID """
765 if self
.currentID
> 1000: self
.currentID
= 1
766 return self
.currentID
768 def _createIDMapping(self
, data
=None):
770 return a unique transaction ID that is mapped internally to a
771 deferred .. also store arbitrary data if it is needed
773 id = self
._nextTransactionID
()
775 self
.ids
[id] = (d
, data
)
778 def checkMessage(self
, message
):
780 process received messages to check for file invitations and
781 typing notifications and other control type messages
783 raise NotImplementedError
785 def sendLine(self
, line
):
786 if LINEDEBUG
: log
.msg("<< " + line
)
787 LineReceiver
.sendLine(self
, line
)
789 def lineReceived(self
, line
):
790 if LINEDEBUG
: log
.msg(">> " + line
)
791 if not self
.connected
: return
792 if self
.currentMessage
:
793 self
.currentMessage
.readPos
+= len(line
+"\r\n")
795 header
, value
= line
.split(':')
796 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
799 #raise MSNProtocolError, "Invalid Message Header"
801 if line
== "" or self
.currentMessage
.specialMessage
:
803 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
806 cmd
, params
= line
.split(' ', 1)
808 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
810 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
812 id = params
.split(' ')[0]
813 if id.isdigit() and self
.ids
.has_key(int(id)):
815 self
.ids
[id][0].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
):
830 if not self
.connected
: return
832 self
.currentMessage
.readPos
+= len(data
)
833 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
835 self
.currentMessage
.message
+= data
[:-diff
]
838 self
.currentMessage
.message
+= data
840 self
.currentMessage
.message
+= data
842 del self
.currentMessage
.readPos
843 m
= self
.currentMessage
844 self
.currentMessage
= None
845 if MESSAGEDEBUG
: log
.msg(m
.message
)
847 if not self
.checkMessage(m
):
848 self
.setLineMode(extra
)
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
))
977 self
.factory
.resetDelay()
979 def connectionLost(self
, reason
):
980 self
._setState
('DISCONNECTED')
982 if self
.pingCheckTask
:
983 self
.pingCheckTask
.stop()
984 self
.pingCheckTask
= None
985 MSNEventBase
.connectionLost(self
, reason
)
987 def _getEmailFields(self
, message
):
988 fields
= message
.getMessage().strip().split('\n')
992 if len(a
) != 2: continue
999 def _gotInitialEmailNotification(self
, message
):
1000 values
= self
._getEmailFields
(message
)
1002 inboxunread
= int(values
["Inbox-Unread"])
1003 foldersunread
= int(values
["Folders-Unread"])
1006 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
1007 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
1009 def _gotEmailNotification(self
, message
):
1010 values
= self
._getEmailFields
(message
)
1012 mailfrom
= values
["From"]
1013 fromaddr
= values
["From-Addr"]
1014 subject
= values
["Subject"]
1015 junkbeginning
= "=?\"us-ascii\"?Q?"
1017 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
1019 # If any of the fields weren't found then it's not a big problem. We just ignore the message
1021 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
1023 def _gotMSNAlert(self
, message
):
1024 notification
= parseText(message
.message
, beExtremelyLenient
=True)
1025 siteurl
= notification
.getAttribute("siteurl")
1026 notid
= notification
.getAttribute("id")
1029 for e
in notification
.elements():
1035 msgid
= msg
.getAttribute("id")
1040 for e
in msg
.elements():
1041 if e
.name
== "ACTION":
1042 action
= e
.getAttribute("url")
1043 if e
.name
== "SUBSCR":
1044 subscr
= e
.getAttribute("url")
1045 if e
.name
== "BODY":
1046 for e2
in e
.elements():
1047 if e2
.name
== "TEXT":
1048 bodytext
= e2
.__str__()
1049 if not (action
and subscr
and bodytext
): return
1051 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
1052 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
1054 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
1056 def _gotUBX(self
, message
):
1057 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
1058 if not msnContact
: return
1059 lm
= message
.message
.lower()
1060 p1
= lm
.find("<psm>") + 5
1061 p2
= lm
.find("</psm>")
1062 if p1
>= 0 and p2
>= 0:
1063 personal
= unescapeFromXml(message
.message
[p1
:p2
])
1064 msnContact
.personal
= personal
1065 self
.contactPersonalChanged(message
.userHandle
, personal
)
1067 msnContact
.personal
= ''
1068 self
.contactPersonalChanged(message
.userHandle
, '')
1070 def checkMessage(self
, message
):
1071 """ hook used for detecting specific notification messages """
1072 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
1073 if 'text/x-msmsgsprofile' in cTypes
:
1074 self
.gotProfile(message
)
1076 elif "text/x-msmsgsinitialemailnotification" in cTypes
:
1077 self
._gotInitialEmailNotification
(message
)
1079 elif "text/x-msmsgsemailnotification" in cTypes
:
1080 self
._gotEmailNotification
(message
)
1082 elif "NOTIFICATION" == message
.userHandle
and message
.specialMessage
== True:
1083 self
._gotMSNAlert
(message
)
1085 elif "UBX" == message
.screenName
and message
.specialMessage
== True:
1086 self
._gotUBX
(message
)
1090 ### protocol command handlers - no need to override these
1092 def handle_VER(self
, params
):
1093 versions
= params
[1:]
1094 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
1095 self
.transport
.loseConnection()
1096 raise MSNProtocolError
, "Invalid version response"
1097 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
1099 def handle_CVR(self
, params
):
1100 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
1102 def handle_USR(self
, params
):
1103 if not (4 <= len(params
) <= 6):
1104 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
1106 mechanism
= params
[1]
1107 if mechanism
== "OK":
1108 self
.loggedIn(params
[2], int(params
[3]))
1109 elif params
[2].upper() == "S":
1110 # we need to obtain auth from a passport server
1112 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3])
1113 d
.addCallback(self
._passportLogin
)
1114 d
.addErrback(self
._passportError
)
1116 def _passportLogin(self
, result
):
1117 if result
[0] == LOGIN_REDIRECT
:
1118 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
1119 result
[1], cached
=1, authData
=result
[2])
1120 d
.addCallback(self
._passportLogin
)
1121 d
.addErrback(self
._passportError
)
1122 elif result
[0] == LOGIN_SUCCESS
:
1123 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
1124 elif result
[0] == LOGIN_FAILURE
:
1125 self
.loginFailure(result
[1])
1127 def _passportError(self
, failure
):
1128 self
.loginFailure("Exception while authenticating: %s" % failure
)
1130 def handle_CHG(self
, params
):
1132 if not self
._fireCallback
(id, params
[1]):
1133 if self
.factory
: self
.factory
.status
= params
[1]
1134 self
.statusChanged(params
[1])
1136 def handle_ILN(self
, params
):
1137 #checkParamLen(len(params), 6, 'ILN')
1138 msnContact
= self
.factory
.contacts
.getContact(params
[2])
1139 if not msnContact
: return
1140 msnContact
.status
= params
[1]
1141 msnContact
.screenName
= unquote(params
[3])
1142 if len(params
) > 4: msnContact
.caps
= int(params
[4])
1143 if len(params
) > 5 and params
[5] != "0":
1144 self
.handleAvatarHelper(msnContact
, params
[5])
1146 self
.handleAvatarGoneHelper(msnContact
)
1147 self
.gotContactStatus(params
[2], params
[1], unquote(params
[3]))
1149 def handleAvatarGoneHelper(self
, msnContact
):
1150 if msnContact
.msnobj
:
1151 msnContact
.msnobj
= None
1152 msnContact
.msnobjGot
= True
1153 self
.contactAvatarChanged(msnContact
.userHandle
, "")
1155 def handleAvatarHelper(self
, msnContact
, msnobjStr
):
1156 msnobj
= MSNObject(unquote(msnobjStr
))
1157 if not msnContact
.msnobj
or msnobj
.sha1d
!= msnContact
.msnobj
.sha1d
:
1158 if MSNP2PDEBUG
: log
.msg("Updated MSNObject received!" + msnobjStr
)
1159 msnContact
.msnobj
= msnobj
1160 msnContact
.msnobjGot
= False
1161 self
.contactAvatarChanged(msnContact
.userHandle
, binascii
.hexlify(b64dec(msnContact
.msnobj
.sha1d
)))
1163 def handle_CHL(self
, params
):
1164 checkParamLen(len(params
), 2, 'CHL')
1165 response
= msnp11chl
.doChallenge(params
[1])
1166 self
.sendLine("QRY %s %s %s" % (self
._nextTransactionID
(), msnp11chl
.MSNP11_PRODUCT_ID
, len(response
)))
1167 self
.transport
.write(response
)
1169 def handle_QRY(self
, params
):
1172 def handle_NLN(self
, params
):
1173 if not self
.factory
: return
1174 msnContact
= self
.factory
.contacts
.getContact(params
[1])
1175 if not msnContact
: return
1176 msnContact
.status
= params
[0]
1177 msnContact
.screenName
= unquote(params
[2])
1178 if len(params
) > 3: msnContact
.caps
= int(params
[3])
1179 if len(params
) > 4 and params
[4] != "0":
1180 self
.handleAvatarHelper(msnContact
, params
[4])
1182 self
.handleAvatarGoneHelper(msnContact
)
1183 self
.contactStatusChanged(params
[1], params
[0], unquote(params
[2]))
1185 def handle_FLN(self
, params
):
1186 checkParamLen(len(params
), 1, 'FLN')
1187 msnContact
= self
.factory
.contacts
.getContact(params
[0])
1189 msnContact
.status
= STATUS_OFFLINE
1190 self
.contactOffline(params
[0])
1192 def handle_LST(self
, params
):
1193 if self
._getState
() != 'SYNC': return
1195 userHandle
, screenName
, userGuid
, lists
, groups
= getVals(params
)
1197 if not userHandle
or lists
< 1:
1198 raise MSNProtocolError
, "Unknown LST " + str(params
) # debug
1199 contact
= MSNContact(userGuid
, userHandle
, screenName
, lists
)
1200 if contact
.lists
& FORWARD_LIST
:
1201 contact
.groups
.extend(map(str, groups
))
1202 self
._getStateData
('list').addContact(contact
)
1203 self
._setStateData
('last_contact', contact
)
1204 sofar
= self
._getStateData
('lst_sofar') + 1
1205 if sofar
== self
._getStateData
('lst_reply'):
1206 # this is the best place to determine that
1207 # a syn realy has finished - msn _may_ send
1208 # BPR information for the last contact
1209 # which is unfortunate because it means
1210 # that the real end of a syn is non-deterministic.
1211 # to handle this we'll keep 'last_contact' hanging
1212 # around in the state data and update it if we need
1214 self
._setState
('SESSION')
1215 contacts
= self
._getStateData
('list')
1216 phone
= self
._getStateData
('phone')
1217 id = self
._getStateData
('synid')
1218 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1219 self
._fireCallback
(id, contacts
, phone
)
1221 self
._setStateData
('lst_sofar',sofar
)
1223 def handle_BLP(self
, params
):
1224 # check to see if this is in response to a SYN
1225 if self
._getState
() == 'SYNC':
1226 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
1229 self
.factory
.contacts
.privacy
= listCodeToID
[params
[1].lower()]
1230 self
._fireCallback
(id, params
[1])
1232 def handle_GTC(self
, params
):
1233 # check to see if this is in response to a SYN
1234 if self
._getState
() == 'SYNC':
1235 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
1236 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
1237 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1240 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
1241 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
1242 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1244 def handle_SYN(self
, params
):
1246 self
._setStateData
('phone', []) # Always needs to be set
1247 if params
[3] == 0: # No LST will be received. New account?
1248 self
._setState
('SESSION')
1249 self
._fireCallback
(id, None, None)
1251 contacts
= MSNContactList()
1252 self
._setStateData
('list', contacts
)
1253 self
._setStateData
('lst_reply', int(params
[3]))
1254 self
._setStateData
('lsg_reply', int(params
[4]))
1255 self
._setStateData
('lst_sofar', 0)
1257 def handle_LSG(self
, params
):
1258 if self
._getState
() == 'SYNC':
1259 self
._getStateData
('list').groups
[params
[1]] = unquote(params
[0])
1261 def handle_PRP(self
, params
):
1262 if params
[1] == "MFN":
1263 self
._fireCallback
(int(params
[0]))
1264 elif self
._getState
() == 'SYNC':
1265 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
1267 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
1269 def handle_BPR(self
, params
):
1270 numParams
= len(params
)
1271 if numParams
== 2: # part of a syn
1272 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
1273 elif numParams
== 4:
1274 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_BPR called with no contact list" # debug
1275 self
.factory
.contacts
.version
= int(params
[0])
1276 userHandle
, phoneType
, number
= params
[1], params
[2], unquote(params
[3])
1277 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1278 self
.gotPhoneNumber(userHandle
, phoneType
, number
)
1281 def handle_ADG(self
, params
):
1282 checkParamLen(len(params
), 5, 'ADG')
1284 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
1285 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
1287 def handle_RMG(self
, params
):
1288 checkParamLen(len(params
), 3, 'RMG')
1290 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
1291 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
1293 def handle_REG(self
, params
):
1294 checkParamLen(len(params
), 5, 'REG')
1296 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
1297 raise MSNProtocolError
, "REG response does not match up to a request" # debug
1299 def handle_ADC(self
, params
):
1300 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_ADC called with no contact list"
1301 numParams
= len(params
)
1302 if numParams
< 3 or params
[1].upper() not in ('AL','BL','RL','FL','PL'):
1303 raise MSNProtocolError
, "Invalid Paramaters for ADC" # debug
1305 listType
= params
[1].lower()
1306 userHandle
, screenName
, userGuid
, ignored1
, groups
= getVals(params
[2:])
1308 if groups
and listType
.upper() != FORWARD_LIST
:
1309 raise MSNProtocolError
, "Only forward list can contain groups" # debug
1311 if not self
._fireCallback
(id, listCodeToID
[listType
], userGuid
, userHandle
, screenName
):
1312 c
= self
.factory
.contacts
.getContact(userHandle
)
1314 c
= MSNContact(userGuid
=userGuid
, userHandle
=userHandle
, screenName
=screenName
)
1315 self
.factory
.contacts
.addContact(c
)
1316 c
.addToList(PENDING_LIST
)
1317 self
.userAddedMe(userGuid
, userHandle
, screenName
)
1319 def handle_REM(self
, params
):
1320 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_REM called with no contact list available!"
1321 numParams
= len(params
)
1322 if numParams
< 3 or params
[1].upper() not in ('AL','BL','FL','RL','PL'):
1323 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1325 listType
= params
[1].lower()
1326 userHandle
= params
[2]
1329 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1330 groupID
= int(params
[3])
1331 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, groupID
):
1332 if listType
.upper() != "RL": return
1333 c
= self
.factory
.contacts
.getContact(userHandle
)
1335 c
.removeFromList(REVERSE_LIST
)
1336 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1337 self
.userRemovedMe(userHandle
)
1339 def handle_XFR(self
, params
):
1340 checkParamLen(len(params
), 5, 'XFR')
1342 # check to see if they sent a host/port pair
1344 host
, port
= params
[2].split(':')
1349 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1350 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1352 def handle_RNG(self
, params
):
1353 checkParamLen(len(params
), 6, 'RNG')
1354 # check for host:port pair
1356 host
, port
= params
[1].split(":")
1361 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1364 def handle_NOT(self
, params
):
1365 checkParamLen(len(params
), 1, 'NOT')
1367 messageLen
= int(params
[0])
1368 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1369 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", specialMessage
=True)
1372 def handle_UBX(self
, params
):
1373 checkParamLen(len(params
), 2, 'UBX')
1375 messageLen
= int(params
[1])
1376 except ValueError: raise MSNProtocolError
, "Invalid Parameter for UBX length argument"
1378 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
="UBX", specialMessage
=True)
1381 self
._gotUBX
(MSNMessage(userHandle
=params
[0]))
1383 def handle_UUX(self
, params
):
1384 checkParamLen(len(params
), 2, 'UUX')
1385 if params
[1] != '0': return
1387 self
._fireCallback
(id)
1389 def handle_OUT(self
, params
):
1390 checkParamLen(len(params
), 1, 'OUT')
1391 self
.factory
.stopTrying()
1392 if params
[0] == "OTH": self
.multipleLogin()
1393 elif params
[0] == "SSD": self
.serverGoingDown()
1394 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1396 def handle_QNG(self
, params
):
1397 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1401 def pingChecker(self
):
1402 if self
.pingCounter
> 5:
1403 # The server has ignored 5 pings, lets kill the connection
1404 self
.transport
.loseConnection()
1406 self
.sendLine("PNG")
1407 self
.pingCounter
+= 1
1409 def pingCheckerStart(self
, *args
):
1410 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1411 self
.pingCheckTask
.start(PINGSPEED
)
1413 def loggedIn(self
, userHandle
, verified
):
1415 Called when the client has logged in.
1416 The default behaviour of this method is to
1417 update the factory with our screenName and
1418 to sync the contact list (factory.contacts).
1419 When this is complete self.listSynchronized
1422 @param userHandle: our userHandle
1423 @param verified: 1 if our passport has been (verified), 0 if not.
1424 (i'm not sure of the significace of this)
1428 d
.addCallback(self
.listSynchronized
)
1429 d
.addCallback(self
.pingCheckerStart
)
1431 def loginFailure(self
, message
):
1433 Called when the client fails to login.
1435 @param message: a message indicating the problem that was encountered
1439 def gotProfile(self
, message
):
1441 Called after logging in when the server sends an initial
1442 message with MSN/passport specific profile information
1443 such as country, number of kids, etc.
1444 Check the message headers for the specific values.
1446 @param message: The profile message
1450 def listSynchronized(self
, *args
):
1452 Lists are now synchronized by default upon logging in, this
1453 method is called after the synchronization has finished
1454 and the factory now has the up-to-date contacts.
1458 def contactAvatarChanged(self
, userHandle
, hash):
1460 Called when we receive the first, or a new <msnobj/> from a
1463 @param userHandle: contact who's msnobj has been changed
1464 @param hash: sha1 hash of their avatar as hex string
1467 def statusChanged(self
, statusCode
):
1469 Called when our status changes and its not in response to a
1472 @param statusCode: 3-letter status code
1476 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
1478 Called when we receive a list of statuses upon login.
1480 @param userHandle: the contact's user handle (passport)
1481 @param statusCode: 3-letter status code
1482 @param screenName: the contact's screen name
1486 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
1488 Called when we're notified that a contact's status has changed.
1490 @param userHandle: the contact's user handle (passport)
1491 @param statusCode: 3-letter status code
1492 @param screenName: the contact's screen name
1496 def contactPersonalChanged(self
, userHandle
, personal
):
1498 Called when a contact's personal message changes.
1500 @param userHandle: the contact who changed their personal message
1501 @param personal : the new personal message
1505 def contactOffline(self
, userHandle
):
1507 Called when a contact goes offline.
1509 @param userHandle: the contact's user handle
1513 def gotMessage(self
, message
):
1515 Called when there is a message from the notification server
1516 that is not understood by default.
1518 @param message: the MSNMessage.
1522 def gotMSNAlert(self
, body
, action
, subscr
):
1524 Called when the server sends an MSN Alert (http://alerts.msn.com)
1526 @param body : the alert text
1527 @param action: a URL with more information for the user to view
1528 @param subscr: a URL the user can use to modify their alert subscription
1532 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1534 Called when the server sends you details about your hotmail
1535 inbox. This is only ever called once, on login.
1537 @param inboxunread : the number of unread items in your inbox
1538 @param foldersunread: the number of unread items in other folders
1542 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1544 Called when the server sends us realtime email
1545 notification. This means that you have received
1546 a new email in your hotmail inbox.
1548 @param mailfrom: the sender of the email
1549 @param fromaddr: the sender of the email (I don't know :P)
1550 @param subject : the email subject
1554 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1556 Called when the server sends us phone details about
1557 a specific user (for example after a user is added
1558 the server will send their status, phone details etc.
1560 @param userHandle: the contact's user handle (passport)
1561 @param phoneType: the specific phoneType
1562 (*_PHONE constants or HAS_PAGER)
1563 @param number: the value/phone number.
1567 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1569 Called when a user adds me to their list. (ie. they have been added to
1572 @param userHandle: the userHandle of the user
1573 @param screenName: the screen name of the user
1577 def userRemovedMe(self
, userHandle
):
1579 Called when a user removes us from their contact list
1580 (they are no longer on our reverseContacts list.
1582 @param userHandle: the contact's user handle (passport)
1586 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1587 key
, userHandle
, screenName
):
1589 Called when we get an invitation to a switchboard server.
1590 This happens when a user requests a chat session with us.
1592 @param sessionID: session ID number, must be remembered for logging in
1593 @param host: the hostname of the switchboard server
1594 @param port: the port to connect to
1595 @param key: used for authorization when connecting
1596 @param userHandle: the user handle of the person who invited us
1597 @param screenName: the screen name of the person who invited us
1601 def multipleLogin(self
):
1603 Called when the server says there has been another login
1604 under our account, the server should disconnect us right away.
1608 def serverGoingDown(self
):
1610 Called when the server has notified us that it is going down for
1617 def changeStatus(self
, status
):
1619 Change my current status. This method will add
1620 a default callback to the returned Deferred
1621 which will update the status attribute of the
1624 @param status: 3-letter status code (as defined by
1625 the STATUS_* constants)
1626 @return: A Deferred, the callback of which will be
1627 fired when the server confirms the change
1628 of status. The callback argument will be
1629 a tuple with the new status code as the
1633 id, d
= self
._createIDMapping
()
1634 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1636 self
.factory
.status
= r
[0]
1638 return d
.addCallback(_cb
)
1640 def setPrivacyMode(self
, privLevel
):
1642 Set my privacy mode on the server.
1645 This only keeps the current privacy setting on
1646 the server for later retrieval, it does not
1647 effect the way the server works at all.
1649 @param privLevel: This parameter can be true, in which
1650 case the server will keep the state as
1651 'al' which the official client interprets
1652 as -> allow messages from only users on
1653 the allow list. Alternatively it can be
1654 false, in which case the server will keep
1655 the state as 'bl' which the official client
1656 interprets as -> allow messages from all
1657 users except those on the block list.
1659 @return: A Deferred, the callback of which will be fired when
1660 the server replies with the new privacy setting.
1661 The callback argument will be a tuple, the only element
1662 of which being either 'al' or 'bl' (the new privacy setting).
1665 id, d
= self
._createIDMapping
()
1666 if privLevel
: self
.sendLine("BLP %s AL" % id)
1667 else: self
.sendLine("BLP %s BL" % id)
1672 Used for keeping an up-to-date contact list.
1673 A callback is added to the returned Deferred
1674 that updates the contact list on the factory
1675 and also sets my state to STATUS_ONLINE.
1678 This is called automatically upon signing
1679 in using the version attribute of
1680 factory.contacts, so you may want to persist
1681 this object accordingly. Because of this there
1682 is no real need to ever call this method
1685 @return: A Deferred, the callback of which will be
1686 fired when the server sends an adequate reply.
1687 The callback argument will be a tuple with two
1688 elements, the new list (MSNContactList) and
1689 your current state (a dictionary). If the version
1690 you sent _was_ the latest list version, both elements
1691 will be None. To just request the list send a version of 0.
1694 self
._setState
('SYNC')
1695 id, d
= self
._createIDMapping
(data
=None)
1696 self
._setStateData
('synid',id)
1697 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1699 self
.changeStatus(STATUS_ONLINE
)
1700 if r
[0] is not None:
1701 self
.factory
.contacts
= r
[0]
1703 return d
.addCallback(_cb
)
1705 def setPhoneDetails(self
, phoneType
, value
):
1707 Set/change my phone numbers stored on the server.
1709 @param phoneType: phoneType can be one of the following
1710 constants - HOME_PHONE, WORK_PHONE,
1711 MOBILE_PHONE, HAS_PAGER.
1712 These are pretty self-explanatory, except
1713 maybe HAS_PAGER which refers to whether or
1714 not you have a pager.
1715 @param value: for all of the *_PHONE constants the value is a
1716 phone number (str), for HAS_PAGER accepted values
1717 are 'Y' (for yes) and 'N' (for no).
1719 @return: A Deferred, the callback for which will be fired when
1720 the server confirms the change has been made. The
1721 callback argument will be a tuple with 2 elements, the
1722 first being the new list version (int) and the second
1723 being the new phone number value (str).
1725 raise "ProbablyDoesntWork"
1726 # XXX: Add a default callback which updates
1727 # factory.contacts.version and the relevant phone
1729 id, d
= self
._createIDMapping
()
1730 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1733 def addListGroup(self
, name
):
1735 Used to create a new list group.
1736 A default callback is added to the
1737 returned Deferred which updates the
1738 contacts attribute of the factory.
1740 @param name: The desired name of the new group.
1742 @return: A Deferred, the callbacck for which will be called
1743 when the server clarifies that the new group has been
1744 created. The callback argument will be a tuple with 3
1745 elements: the new list version (int), the new group name
1746 (str) and the new group ID (int).
1749 raise "ProbablyDoesntWork"
1750 id, d
= self
._createIDMapping
()
1751 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1753 if self
.factory
.contacts
:
1754 self
.factory
.contacts
.version
= r
[0]
1755 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1757 return d
.addCallback(_cb
)
1759 def remListGroup(self
, groupID
):
1761 Used to remove a list group.
1762 A default callback is added to the
1763 returned Deferred which updates the
1764 contacts attribute of the factory.
1766 @param groupID: the ID of the desired group to be removed.
1768 @return: A Deferred, the callback for which will be called when
1769 the server clarifies the deletion of the group.
1770 The callback argument will be a tuple with 2 elements:
1771 the new list version (int) and the group ID (int) of
1775 raise "ProbablyDoesntWork"
1776 id, d
= self
._createIDMapping
()
1777 self
.sendLine("RMG %s %s" % (id, groupID
))
1779 self
.factory
.contacts
.version
= r
[0]
1780 self
.factory
.contacts
.remGroup(r
[1])
1782 return d
.addCallback(_cb
)
1784 def renameListGroup(self
, groupID
, newName
):
1786 Used to rename an existing list group.
1787 A default callback is added to the returned
1788 Deferred which updates the contacts attribute
1791 @param groupID: the ID of the desired group to rename.
1792 @param newName: the desired new name for the group.
1794 @return: A Deferred, the callback for which will be called
1795 when the server clarifies the renaming.
1796 The callback argument will be a tuple of 3 elements,
1797 the new list version (int), the group id (int) and
1798 the new group name (str).
1801 raise "ProbablyDoesntWork"
1802 id, d
= self
._createIDMapping
()
1803 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1805 self
.factory
.contacts
.version
= r
[0]
1806 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1808 return d
.addCallback(_cb
)
1810 def addContact(self
, listType
, userHandle
):
1812 Used to add a contact to the desired list.
1813 A default callback is added to the returned
1814 Deferred which updates the contacts attribute of
1815 the factory with the new contact information.
1816 If you are adding a contact to the forward list
1817 and you want to associate this contact with multiple
1818 groups then you will need to call this method for each
1819 group you would like to add them to, changing the groupID
1820 parameter. The default callback will take care of updating
1821 the group information on the factory's contact list.
1823 @param listType: (as defined by the *_LIST constants)
1824 @param userHandle: the user handle (passport) of the contact
1827 @return: A Deferred, the callback for which will be called when
1828 the server has clarified that the user has been added.
1829 The callback argument will be a tuple with 4 elements:
1830 the list type, the contact's user handle, the new list
1831 version, and the group id (if relevant, otherwise it
1835 id, d
= self
._createIDMapping
()
1836 try: # Make sure the contact isn't actually on the list
1837 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1838 except AttributeError: pass
1839 listType
= listIDToCode
[listType
].upper()
1840 if listType
== "FL":
1841 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1843 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1846 if not self
.factory
: return
1847 c
= self
.factory
.contacts
.getContact(r
[2])
1849 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1850 self
.factory
.contacts
.addContact(c
)
1851 #if r[3]: c.groups.append(r[3])
1854 return d
.addCallback(_cb
)
1856 def remContact(self
, listType
, userHandle
):
1858 Used to remove a contact from the desired list.
1859 A default callback is added to the returned deferred
1860 which updates the contacts attribute of the factory
1861 to reflect the new contact information.
1863 @param listType: (as defined by the *_LIST constants)
1864 @param userHandle: the user handle (passport) of the
1865 contact being removed
1867 @return: A Deferred, the callback for which will be called when
1868 the server has clarified that the user has been removed.
1869 The callback argument will be a tuple of 3 elements:
1870 the list type, the contact's user handle and the group ID
1871 (if relevant, otherwise it will be None)
1874 id, d
= self
._createIDMapping
()
1875 try: # Make sure the contact is actually on this list
1876 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1877 except AttributeError: return
1878 listType
= listIDToCode
[listType
].upper()
1879 if listType
== "FL":
1881 c
= self
.factory
.contacts
.getContact(userHandle
)
1882 userGuid
= c
.userGuid
1883 except AttributeError: return
1884 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1886 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1889 if listType
== "FL":
1890 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1891 l
= self
.factory
.contacts
1892 c
= l
.getContact(r
[1])
1896 if group
: # they may not have been removed from the list
1897 c
.groups
.remove(group
)
1898 if c
.groups
: shouldRemove
= 0
1900 c
.removeFromList(r
[0])
1901 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1903 return d
.addCallback(_cb
)
1905 def changeScreenName(self
, newName
):
1907 Used to change your current screen name.
1908 A default callback is added to the returned
1909 Deferred which updates the screenName attribute
1910 of the factory and also updates the contact list
1913 @param newName: the new screen name
1915 @return: A Deferred, the callback for which will be called
1916 when the server acknowledges the change.
1917 The callback argument will be an empty tuple.
1920 id, d
= self
._createIDMapping
()
1921 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1923 self
.factory
.screenName
= newName
1925 return d
.addCallback(_cb
)
1927 def changePersonalMessage(self
, personal
):
1929 Used to change your personal message.
1931 @param personal: the new screen name
1933 @return: A Deferred, the callback for which will be called
1934 when the server acknowledges the change.
1935 The callback argument will be a tuple of 1 element,
1936 the personal message.
1939 id, d
= self
._createIDMapping
()
1942 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1943 self
.sendLine("UUX %s %s" % (id, len(data
)))
1944 self
.transport
.write(data
)
1946 self
.factory
.personal
= personal
1948 return d
.addCallback(_cb
)
1950 def changeAvatar(self
, imageDataFunc
, push
):
1952 Used to change the avatar that other users see.
1954 @param imageDataFunc: a function to return the PNG image data to set as the avatar
1955 @param push : whether to push the update to the server
1956 (it will otherwise be sent with the next
1959 @return: If push==True, a Deferred, the callback for which
1960 will be called when the server acknowledges the change.
1961 The callback argument will be the same as for changeStatus.
1964 checkMsnobj
= MSNObject()
1965 checkMsnobj
.setData(self
.factory
.userHandle
, imageDataFunc
)
1966 if self
.msnobj
and self
.msnobj
.sha1d
== checkMsnobj
.sha1d
:
1967 return # Avatar hasn't changed
1969 # We need to keep the same MSNObject instance, as it is
1970 # passed on to SwitchboardClient objects
1971 self
.msnobj
.setData(self
.factory
.userHandle
, imageDataFunc
)
1973 self
.msnobj
.setNull()
1975 return self
.changeStatus(self
.factory
.status
) # Push to server
1978 def requestSwitchboardServer(self
):
1980 Used to request a switchboard server to use for conversations.
1982 @return: A Deferred, the callback for which will be called when
1983 the server responds with the switchboard information.
1984 The callback argument will be a tuple with 3 elements:
1985 the host of the switchboard server, the port and a key
1986 used for logging in.
1989 id, d
= self
._createIDMapping
()
1990 self
.sendLine("XFR %s SB" % id)
1995 Used to log out of the notification server.
1996 After running the method the server is expected
1997 to close the connection.
2000 if self
.pingCheckTask
:
2001 self
.pingCheckTask
.stop()
2002 self
.pingCheckTask
= None
2003 self
.factory
.stopTrying()
2004 self
.sendLine("OUT")
2005 self
.transport
.loseConnection()
2007 class NotificationFactory(ReconnectingClientFactory
):
2009 Factory for the NotificationClient protocol.
2010 This is basically responsible for keeping
2011 the state of the client and thus should be used
2012 in a 1:1 situation with clients.
2014 @ivar contacts: An MSNContactList instance reflecting
2015 the current contact list -- this is
2016 generally kept up to date by the default
2018 @ivar userHandle: The client's userHandle, this is expected
2019 to be set by the client and is used by the
2020 protocol (for logging in etc).
2021 @ivar screenName: The client's current screen-name -- this is
2022 generally kept up to date by the default
2024 @ivar password: The client's password -- this is (obviously)
2025 expected to be set by the client.
2026 @ivar passportServer: This must point to an msn passport server
2027 (the whole URL is required)
2028 @ivar status: The status of the client -- this is generally kept
2029 up to date by the default command handlers
2030 @ivar maxRetries: The number of times the factory will reconnect
2031 if the connection dies because of a network error.
2038 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
2040 protocol
= NotificationClient
2044 class SwitchboardClient(MSNEventBase
):
2046 This class provides support for clients connecting to a switchboard server.
2048 Switchboard servers are used for conversations with other people
2049 on the MSN network. This means that the number of conversations at
2050 any given time will be directly proportional to the number of
2051 connections to varioius switchboard servers.
2053 MSN makes no distinction between single and group conversations,
2054 so any number of users may be invited to join a specific conversation
2055 taking place on a switchboard server.
2057 @ivar key: authorization key, obtained when receiving
2058 invitation / requesting switchboard server.
2059 @ivar userHandle: your user handle (passport)
2060 @ivar sessionID: unique session ID, used if you are replying
2061 to a switchboard invitation
2062 @ivar reply: set this to 1 in connectionMade or before to signifiy
2063 that you are replying to a switchboard invitation.
2064 @ivar msnobj: the MSNObject for the user's avatar. So that the
2065 switchboard can distribute it to anyone who asks.
2077 MSNEventBase
.__init
__(self
)
2078 self
.pendingUsers
= {}
2079 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
2082 def connectionMade(self
):
2083 MSNEventBase
.connectionMade(self
)
2086 def connectionLost(self
, reason
):
2087 self
.cookies
['iCookies'] = {}
2088 MSNEventBase
.connectionLost(self
, reason
)
2090 def _sendInit(self
):
2092 send initial data based on whether we are replying to an invitation
2095 id = self
._nextTransactionID
()
2097 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
2099 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
2101 def _newInvitationCookie(self
):
2103 if self
._iCookie
> 1000: self
._iCookie
= 1
2104 return self
._iCookie
2106 def _checkTyping(self
, message
, cTypes
):
2107 """ helper method for checkMessage """
2108 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
2109 self
.gotContactTyping(message
)
2112 def _checkFileInvitation(self
, message
, info
):
2113 """ helper method for checkMessage """
2114 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
2116 cookie
= info
['Invitation-Cookie']
2117 filename
= info
['Application-File']
2118 filesize
= int(info
['Application-FileSize'])
2119 connectivity
= (info
.get('Connectivity', 'n').lower() == 'y')
2121 log
.msg('Received munged file transfer request ... ignoring.')
2123 raise NotImplementedError
2124 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
2127 def _handleP2PMessage(self
, message
):
2128 """ helper method for msnslp messages (file transfer & avatars) """
2129 if not message
.getHeader("P2P-Dest") == self
.userHandle
: return
2130 packet
= message
.message
2131 binaryFields
= BinaryFields(packet
=packet
)
2132 if binaryFields
[5] == BinaryFields
.BYEGOT
:
2133 pass # Ignore the ACKs to SLP messages
2134 elif binaryFields
[0] != 0:
2135 slpLink
= self
.slpLinks
.get(binaryFields
[0])
2137 # Link has been killed. Ignore
2139 if slpLink
.remoteUser
== message
.userHandle
:
2140 slpLink
.handlePacket(packet
)
2141 elif binaryFields
[5] == BinaryFields
.ACK
:
2142 pass # Ignore the ACKs to SLP messages
2144 slpMessage
= MSNSLPMessage(packet
)
2146 # Always try and give a slpMessage to a slpLink first.
2147 # If none can be found, and it was INVITE, then create
2148 # one to handle the session.
2149 for slpLink
in self
.slpLinks
.values():
2150 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
2151 slpLink
.handleSLPMessage(slpMessage
)
2154 slpLink
= None # Was not handled
2156 if not slpLink
and slpMessage
.method
== "INVITE":
2157 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
2158 context
= FileContext(slpMessage
.context
)
2159 slpLink
= SLPLink_FileReceive(remoteUser
=slpMessage
.fro
, switchboard
=self
, filename
=context
.filename
, filesize
=context
.filesize
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
, branch
=slpMessage
.branch
)
2160 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2161 self
.gotFileReceive(slpLink
)
2162 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
2163 # Check that we have an avatar to send
2165 slpLink
= SLPLink_AvatarSend(remoteUser
=slpMessage
.fro
, switchboard
=self
, filesize
=self
.msnobj
.size
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2166 slpLink
.write(self
.msnobj
.imageDataFunc())
2169 # They shouldn't have sent a request if we have
2170 # no avatar. So we'll just ignore them.
2171 # FIXME We should really send an error
2174 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2176 # Always need to ACK these packets if we can
2177 slpLink
.sendP2PACK(binaryFields
)
2180 def checkMessage(self
, message
):
2182 hook for detecting any notification type messages
2183 (e.g. file transfer)
2185 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2186 if self
._checkTyping
(message
, cTypes
): return 0
2187 # if 'text/x-msmsgsinvite' in cTypes:
2188 # header like info is sent as part of the message body.
2190 # for line in message.message.split('\r\n'):
2192 # key, val = line.split(':')
2193 # info[key] = val.lstrip()
2194 # except ValueError: continue
2195 # if self._checkFileInvitation(message, info): return 0
2196 elif 'application/x-msnmsgrp2p' in cTypes
:
2197 self
._handleP
2PMessage
(message
)
2202 def handle_USR(self
, params
):
2203 checkParamLen(len(params
), 4, 'USR')
2204 if params
[1] == "OK":
2208 def handle_CAL(self
, params
):
2209 checkParamLen(len(params
), 3, 'CAL')
2211 if params
[1].upper() == "RINGING":
2212 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2215 def handle_JOI(self
, params
):
2216 checkParamLen(len(params
), 2, 'JOI')
2217 self
.userJoined(params
[0], unquote(params
[1]))
2219 # users participating in the current chat
2220 def handle_IRO(self
, params
):
2221 checkParamLen(len(params
), 5, 'IRO')
2222 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2223 if params
[1] == params
[2]:
2224 self
.gotChattingUsers(self
.pendingUsers
)
2225 self
.pendingUsers
= {}
2227 # finished listing users
2228 def handle_ANS(self
, params
):
2229 checkParamLen(len(params
), 2, 'ANS')
2230 if params
[1] == "OK":
2233 def handle_ACK(self
, params
):
2234 checkParamLen(len(params
), 1, 'ACK')
2235 self
._fireCallback
(int(params
[0]), None)
2237 def handle_NAK(self
, params
):
2238 checkParamLen(len(params
), 1, 'NAK')
2239 self
._fireCallback
(int(params
[0]), None)
2241 def handle_BYE(self
, params
):
2242 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2243 self
.userLeft(params
[0])
2249 called when all login details have been negotiated.
2250 Messages can now be sent, or new users invited.
2254 def gotChattingUsers(self
, users
):
2256 called after connecting to an existing chat session.
2258 @param users: A dict mapping user handles to screen names
2259 (current users taking part in the conversation)
2263 def userJoined(self
, userHandle
, screenName
):
2265 called when a user has joined the conversation.
2267 @param userHandle: the user handle (passport) of the user
2268 @param screenName: the screen name of the user
2272 def userLeft(self
, userHandle
):
2274 called when a user has left the conversation.
2276 @param userHandle: the user handle (passport) of the user.
2280 def gotMessage(self
, message
):
2282 called when we receive a message.
2284 @param message: the associated MSNMessage object
2288 def gotFileReceive(self
, fileReceive
):
2290 called when we receive a file send request from a contact.
2291 Default action is to reject the file.
2293 @param fileReceive: msnft.MSNFTReceive_Base instance
2295 fileReceive
.reject()
2298 def gotSendRequest(self
, fileReceive
):
2300 called when we receive a file send request from a contact
2302 @param fileReceive: msnft.MSNFTReceive_Base instance
2306 def gotContactTyping(self
, message
):
2308 called when we receive the special type of message notifying
2309 us that a contact is typing a message.
2311 @param message: the associated MSNMessage object
2317 def inviteUser(self
, userHandle
):
2319 used to invite a user to the current switchboard server.
2321 @param userHandle: the user handle (passport) of the desired user.
2323 @return: A Deferred, the callback for which will be called
2324 when the server notifies us that the user has indeed
2325 been invited. The callback argument will be a tuple
2326 with 1 element, the sessionID given to the invited user.
2327 I'm not sure if this is useful or not.
2330 id, d
= self
._createIDMapping
()
2331 self
.sendLine("CAL %s %s" % (id, userHandle
))
2334 def sendMessage(self
, message
):
2336 used to send a message.
2338 @param message: the corresponding MSNMessage object.
2340 @return: Depending on the value of message.ack.
2341 If set to MSNMessage.MESSAGE_ACK or
2342 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2343 the callback for which will be fired when an ACK or
2344 NACK is received - the callback argument will be
2345 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2346 the return value is None.
2349 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2350 else: id, d
= self
._createIDMapping
()
2351 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2352 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2353 # Apparently order matters with these
2354 orderMatters
= ("MIME-Version", "Content-Type", "Message-ID")
2355 for header
in orderMatters
:
2356 if message
.hasHeader(header
):
2357 self
.sendLine("%s: %s" % (header
, message
.getHeader(header
)))
2358 # send the rest of the headers
2359 for header
in [h
for h
in message
.headers
.items() if h
[0] not in orderMatters
]:
2360 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2361 self
.transport
.write("\r\n")
2362 self
.transport
.write(message
.message
)
2363 if MESSAGEDEBUG
: log
.msg(message
.message
)
2366 def sendAvatarRequest(self
, msnContact
):
2368 used to request an avatar from a user in this switchboard
2371 @param msnContact: the msnContact object to request an avatar for
2373 @return: A Deferred, the callback for which will be called
2374 when the avatar transfer succeeds.
2375 The callback argument will be a tuple with one element,
2376 the PNG avatar data.
2378 if not msnContact
.msnobj
: return
2380 def bufferClosed(data
):
2382 buffer = StringBuffer(bufferClosed
)
2383 buffer.error
= lambda: None
2384 slpLink
= SLPLink_AvatarReceive(remoteUser
=msnContact
.userHandle
, switchboard
=self
, consumer
=buffer, context
=msnContact
.msnobj
.text
)
2385 self
.slpLinks
[slpLink
.sessionID
] = slpLink
2388 def sendFile(self
, msnContact
, filename
, filesize
):
2390 used to send a file to a contact.
2392 @param msnContact: the MSNContact object to send a file to.
2393 @param filename: the name of the file to send.
2394 @param filesize: the size of the file to send.
2396 @return: (fileSend, d) A FileSend object and a Deferred.
2397 The Deferred will pass one argument in a tuple,
2398 whether or not the transfer is accepted. If you
2399 receive a True, then you can call write() on the
2400 fileSend object to send your file. Call close()
2401 when the file is done.
2402 NOTE: You MUST write() exactly as much as you
2403 declare in filesize.
2405 if not msnContact
.userHandle
: return
2406 # FIXME, check msnContact.caps to see if we should use old-style
2407 fileSend
= SLPLink_FileSend(remoteUser
=msnContact
.userHandle
, switchboard
=self
, filename
=filename
, filesize
=filesize
)
2408 self
.slpLinks
[fileSend
.sessionID
] = fileSend
2409 return fileSend
, fileSend
.acceptDeferred
2411 def sendTypingNotification(self
):
2413 Used to send a typing notification. Upon receiving this
2414 message the official client will display a 'user is typing'
2415 message to all other users in the chat session for 10 seconds.
2416 You should send one of these every 5 seconds as long as the
2420 m
.ack
= m
.MESSAGE_ACK_NONE
2421 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2422 m
.setHeader('TypingUser', self
.userHandle
)
2426 def sendFileInvitation(self
, fileName
, fileSize
):
2428 send an notification that we want to send a file.
2430 @param fileName: the file name
2431 @param fileSize: the file size
2433 @return: A Deferred, the callback of which will be fired
2434 when the user responds to this invitation with an
2435 appropriate message. The callback argument will be
2436 a tuple with 3 elements, the first being 1 or 0
2437 depending on whether they accepted the transfer
2438 (1=yes, 0=no), the second being an invitation cookie
2439 to identify your follow-up responses and the third being
2440 the message 'info' which is a dict of information they
2441 sent in their reply (this doesn't really need to be used).
2442 If you wish to proceed with the transfer see the
2443 sendTransferInfo method.
2445 cookie
= self
._newInvitationCookie
()
2448 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2449 m
.message
+= 'Application-Name: File Transfer\r\n'
2450 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2451 m
.message
+= 'Invitation-Command: INVITE\r\n'
2452 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2453 m
.message
+= 'Application-File: %s\r\n' % fileName
2454 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2455 m
.ack
= m
.MESSAGE_ACK_NONE
2457 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2460 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2462 send information relating to a file transfer session.
2464 @param accept: whether or not to go ahead with the transfer
2466 @param iCookie: the invitation cookie of previous replies
2467 relating to this transfer
2468 @param authCookie: the authentication cookie obtained from
2469 an FileSend instance
2471 @param port: the port on which an FileSend protocol is listening.
2474 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2475 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2476 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2477 m
.message
+= 'IP-Address: %s\r\n' % ip
2478 m
.message
+= 'Port: %s\r\n' % port
2479 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2481 m
.ack
= m
.MESSAGE_NACK
2486 def __init__(self
, filename
, filesize
, userHandle
):
2487 self
.consumer
= None
2488 self
.finished
= False
2491 self
.filename
, self
.filesize
, self
.userHandle
= filename
, filesize
, userHandle
2494 raise NotImplementedError
2496 def accept(self
, consumer
):
2497 if self
.consumer
: raise "AlreadyAccepted"
2498 self
.consumer
= consumer
2499 for data
in self
.buffer:
2500 self
.consumer
.write(data
)
2503 self
.consumer
.close()
2505 self
.consumer
.error()
2507 def write(self
, data
):
2508 if self
.error
or self
.finished
:
2509 raise IOError, "Attempt to write in an invalid state"
2511 self
.consumer
.write(data
)
2513 self
.buffer.append(data
)
2516 self
.finished
= True
2518 self
.consumer
.close()
2521 """ Represents the Context field for P2P file transfers """
2522 def __init__(self
, data
=""):
2530 if MSNP2PDEBUG
: log
.msg("FileContext packing:", self
.filename
, self
.filesize
)
2531 data
= struct
.pack("<LLQL", 638, 0x03, self
.filesize
, 0x01)
2532 data
= data
[:-1] # Uck, weird, but it works
2533 data
+= utf16net(self
.filename
)
2534 data
= ljust(data
, 570, '\0')
2535 data
+= struct
.pack("<L", 0xFFFFFFFFL
)
2536 data
= ljust(data
, 638, '\0')
2539 def parse(self
, packet
):
2540 self
.filesize
= struct
.unpack("<Q", packet
[8:16])[0]
2541 chunk
= packet
[19:540]
2542 chunk
= chunk
[:chunk
.find('\x00\x00')]
2543 self
.filename
= unicode((codecs
.BOM_UTF16_BE
+ chunk
).decode("utf-16"))
2544 if MSNP2PDEBUG
: log
.msg("FileContext parsed:", self
.filesize
, self
.filename
)
2548 """ Utility class for the binary header & footer in p2p messages """
2557 def __init__(self
, fields
=None, packet
=None):
2559 self
.fields
= fields
2561 self
.fields
= [0] * 10
2563 self
.unpackFields(packet
)
2565 def __getitem__(self
, key
):
2566 return self
.fields
[key
]
2568 def __setitem__(self
, key
, value
):
2569 self
.fields
[key
] = value
2571 def unpackFields(self
, packet
):
2572 self
.fields
= struct
.unpack("<LLQQLLLLQ", packet
[0:48])
2573 self
.fields
+= struct
.unpack(">L", packet
[len(packet
)-4:])
2575 out
= "Unpacked fields: "
2576 for i
in self
.fields
:
2580 def packHeaders(self
):
2581 f
= tuple(self
.fields
)
2583 out
= "Packed fields: "
2584 for i
in self
.fields
:
2587 return struct
.pack("<LLQQLLLLQ", f
[0], f
[1], f
[2], f
[3], f
[4], f
[5], f
[6], f
[7], f
[8])
2589 def packFooter(self
):
2590 return struct
.pack(">L", self
.fields
[9])
2593 class MSNSLPMessage
:
2594 """ Representation of a single MSNSLP message """
2595 def __init__(self
, packet
=None):
2602 self
.sessionGuid
= ""
2603 self
.sessionID
= None
2605 self
.data
= "\r\n" + chr(0)
2609 def create(self
, method
=None, status
=None, to
=None, fro
=None, branch
=None, cseq
=0, sessionGuid
=None, data
=None):
2610 self
.method
= method
2611 self
.status
= status
2614 self
.branch
= branch
2616 self
.sessionGuid
= sessionGuid
2617 if data
: self
.data
= data
2619 def setData(self
, ctype
, data
):
2622 order
= ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
2624 if key
== "Context" and data
.has_key(key
):
2625 s
.append("Context: %s\r\n" % b64enc(data
[key
]))
2626 elif data
.has_key(key
):
2627 s
.append("%s: %s\r\n" % (key
, str(data
[key
])))
2628 s
.append("\r\n"+chr(0))
2630 self
.data
= "".join(s
)
2634 if s
.find("MSNSLP/1.0") < 0: return
2636 lines
= s
.split("\r\n")
2638 # Get the MSNSLP method or status
2639 msnslp
= lines
[0].split(" ")
2640 if MSNP2PDEBUG
: log
.msg("Parsing MSNSLPMessage %s %s" % (len(s
), s
))
2641 if msnslp
[0] in ("INVITE", "BYE"):
2642 self
.method
= msnslp
[0].strip()
2644 self
.status
= msnslp
[1].strip()
2646 lines
.remove(lines
[0])
2649 line
= line
.split(":")
2650 if len(line
) < 1: continue
2652 if len(line
) > 2 and line
[0] == "To":
2653 self
.to
= line
[2][:line
[2].find('>')]
2654 elif len(line
) > 2 and line
[0] == "From":
2655 self
.fro
= line
[2][:line
[2].find('>')]
2656 elif line
[0] == "Call-ID":
2657 self
.sessionGuid
= line
[1].strip()
2658 elif line
[0] == "CSeq":
2659 self
.cseq
= int(line
[1].strip())
2660 elif line
[0] == "SessionID":
2661 self
.sessionID
= int(line
[1].strip())
2662 elif line
[0] == "EUF-GUID":
2663 self
.euf_guid
= line
[1].strip()
2664 elif line
[0] == "Content-Type":
2665 self
.ctype
= line
[1].strip()
2666 elif line
[0] == "Context":
2667 self
.context
= b64dec(line
[1])
2668 elif line
[0] == "Via":
2669 self
.branch
= line
[1].split(";")[1].split("=")[1].strip()
2672 log
.msg("Error parsing MSNSLP message.")
2678 s
.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self
.method
, self
.to
))
2680 if self
.status
== "200": status
= "200 OK"
2681 elif self
.status
== "603": status
= "603 Decline"
2682 s
.append("MSNSLP/1.0 %s\r\n" % status
)
2683 s
.append("To: <msnmsgr:%s>\r\n" % self
.to
)
2684 s
.append("From: <msnmsgr:%s>\r\n" % self
.fro
)
2685 s
.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % self
.branch
)
2686 s
.append("CSeq: %s \r\n" % str(self
.cseq
))
2687 s
.append("Call-ID: %s\r\n" % self
.sessionGuid
)
2688 s
.append("Max-Forwards: 0\r\n")
2689 s
.append("Content-Type: %s\r\n" % self
.ctype
)
2690 s
.append("Content-Length: %s\r\n\r\n" % len(self
.data
))
2695 """ Utility for handling the weird sequence IDs in p2p messages """
2696 def __init__(self
, baseID
=None):
2698 self
.baseID
= baseID
2700 self
.baseID
= random
.randint(1000, MSN_MAXINT
)
2704 return p2pseq(self
.pos
) + self
.baseID
2711 class StringBuffer(StringIO
.StringIO
):
2712 def __init__(self
, notifyFunc
=None):
2713 self
.notifyFunc
= notifyFunc
2714 StringIO
.StringIO
.__init
__(self
)
2718 self
.notifyFunc(self
.getvalue())
2719 self
.notifyFunc
= None
2720 StringIO
.StringIO
.close(self
)
2724 def __init__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
):
2727 sessionID
= random
.randint(1000, MSN_MAXINT
)
2729 sessionGuid
= random_guid()
2730 self
.remoteUser
= remoteUser
2731 self
.switchboard
= switchboard
2732 self
.sessionID
= sessionID
2733 self
.sessionGuid
= sessionGuid
2734 self
.seqID
= SeqID()
2737 if MSNP2PDEBUG
: log
.msg("killLink")
2739 if MSNP2PDEBUG
: log
.msg("killLink - kill()")
2740 if not self
.switchboard
: return
2741 del self
.switchboard
.slpLinks
[self
.sessionID
]
2742 self
.switchboard
= None
2743 # This is so that handleP2PMessage can still use the SLPLink
2744 # one last time, for ACKing BYEs and 601s.
2745 reactor
.callLater(0, kill
)
2747 def warn(self
, text
):
2748 log
.msg("Warning in transfer: %s %s" % (self
, text
))
2750 def sendP2PACK(self
, ackHeaders
):
2751 binaryFields
= BinaryFields()
2752 binaryFields
[0] = ackHeaders
[0]
2753 binaryFields
[1] = self
.seqID
.next()
2754 binaryFields
[3] = ackHeaders
[3]
2755 binaryFields
[5] = BinaryFields
.ACK
2756 binaryFields
[6] = ackHeaders
[1]
2757 binaryFields
[7] = ackHeaders
[6]
2758 binaryFields
[8] = ackHeaders
[3]
2759 self
.sendP2PMessage(binaryFields
, "")
2761 def sendSLPMessage(self
, cmd
, ctype
, data
, branch
=None):
2762 msg
= MSNSLPMessage()
2764 msg
.create(status
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=branch
, cseq
=1, sessionGuid
=self
.sessionGuid
)
2766 msg
.create(method
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=random_guid(), cseq
=0, sessionGuid
=self
.sessionGuid
)
2767 msg
.setData(ctype
, data
)
2769 binaryFields
= BinaryFields()
2770 binaryFields
[1] = self
.seqID
.next()
2771 binaryFields
[3] = len(msgStr
)
2772 binaryFields
[4] = binaryFields
[3]
2773 binaryFields
[6] = random
.randint(1000, MSN_MAXINT
)
2774 self
.sendP2PMessage(binaryFields
, msgStr
)
2776 def sendP2PMessage(self
, binaryFields
, msgStr
):
2777 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2779 message
= MSNMessage(message
=packet
)
2780 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2781 message
.setHeader("P2P-Dest", self
.remoteUser
)
2782 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2783 self
.switchboard
.sendMessage(message
)
2785 def handleSLPMessage(self
, slpMessage
):
2786 raise NotImplementedError
2792 class SLPLink_Send(SLPLink
):
2793 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2794 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2795 self
.handlePacket
= None
2797 self
.filesize
= filesize
2800 def send_dataprep(self
):
2801 if MSNP2PDEBUG
: log
.msg("send_dataprep")
2802 binaryFields
= BinaryFields()
2803 binaryFields
[0] = self
.sessionID
2804 binaryFields
[1] = self
.seqID
.next()
2807 binaryFields
[6] = random
.randint(1000, MSN_MAXINT
)
2809 self
.sendP2PMessage(binaryFields
, chr(0) * 4)
2811 def write(self
, data
):
2812 if MSNP2PDEBUG
: log
.msg("write")
2814 data
= self
.data
+ data
2818 if i
+ 1202 < length
:
2819 self
._writeChunk
(data
[i
:i
+1202])
2822 self
.data
= data
[i
:]
2825 def _writeChunk(self
, chunk
):
2826 if MSNP2PDEBUG
: log
.msg("writing chunk")
2827 binaryFields
= BinaryFields()
2828 binaryFields
[0] = self
.sessionID
2829 if self
.offset
== 0:
2830 binaryFields
[1] = self
.seqID
.next()
2832 binaryFields
[1] = self
.seqID
.get()
2833 binaryFields
[2] = self
.offset
2834 binaryFields
[3] = self
.filesize
2835 binaryFields
[4] = len(chunk
)
2836 binaryFields
[5] = self
.dataFlag
2837 binaryFields
[6] = random
.randint(1000, MSN_MAXINT
)
2839 self
.offset
+= len(chunk
)
2840 self
.sendP2PMessage(binaryFields
, chunk
)
2844 self
._writeChunk
(self
.data
)
2849 # FIXME, should send 601 or something
2851 class SLPLink_FileSend(SLPLink_Send
):
2852 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
):
2853 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
)
2854 self
.dataFlag
= BinaryFields
.DATAFT
2855 # Send invite & wait for 200OK before sending dataprep
2856 context
= FileContext()
2857 context
.filename
= filename
2858 context
.filesize
= filesize
2859 data
= {"EUF-GUID" : MSN_MSNFTP_GUID
,\
2860 "SessionID": self
.sessionID
,\
2862 "Context" : context
.pack() }
2863 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2864 self
.acceptDeferred
= Deferred()
2866 def handleSLPMessage(self
, slpMessage
):
2867 if slpMessage
.status
== "200":
2868 if slpMessage
.ctype
== "application/x-msnmsgr-sessionreqbody":
2869 data
= {"Bridges" : "TRUDPv1 TCPv1",\
2871 "Conn-Type" : "Firewall",\
2872 "UPnPNat" : "false",\
2874 #"Hashed-Nonce": random_guid()}
2875 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data
)
2876 elif slpMessage
.ctype
== "application/x-msnmsgr-transrespbody":
2877 self
.acceptDeferred
.callback((True,))
2878 self
.handlePacket
= self
.wait_data_ack
2880 if slpMessage
.status
== "603":
2881 self
.acceptDeferred
.callback((False,))
2882 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to decline, error or BYE")
2886 def wait_data_ack(self
, packet
):
2887 if MSNP2PDEBUG
: log
.msg("wait_data_ack")
2888 binaryFields
= BinaryFields()
2889 binaryFields
.unpackFields(packet
)
2891 if binaryFields
[5] != BinaryFields
.ACK
:
2892 self
.warn("field5," + str(binaryFields
[5]))
2895 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2896 self
.handlePacket
= None
2899 self
.handlePacket
= self
.wait_data_ack
2900 SLPLink_Send
.close(self
)
2903 class SLPLink_AvatarSend(SLPLink_Send
):
2904 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2905 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2906 self
.dataFlag
= BinaryFields
.DATA
2907 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2908 self
.send_dataprep()
2909 self
.handlePacket
= lambda packet
: None
2911 def handleSLPMessage(self
, slpMessage
):
2912 if MSNP2PDEBUG
: log
.msg("BYE or error")
2916 SLPLink_Send
.close(self
)
2917 # Keep the link open to wait for a BYE
2919 class SLPLink_Receive(SLPLink
):
2920 def __init__(self
, remoteUser
, switchboard
, consumer
, context
=None, sessionID
=None, sessionGuid
=None):
2921 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2922 self
.handlePacket
= None
2923 self
.consumer
= consumer
2926 def wait_dataprep(self
, packet
):
2927 if MSNP2PDEBUG
: log
.msg("wait_dataprep")
2928 binaryFields
= BinaryFields()
2929 binaryFields
.unpackFields(packet
)
2931 if binaryFields
[3] != 4:
2932 self
.warn("field3," + str(binaryFields
[3]))
2934 if binaryFields
[4] != 4:
2935 self
.warn("field4," + str(binaryFields
[4]))
2937 # Just ignore the footer
2938 #if binaryFields[9] != 1:
2939 # self.warn("field9," + str(binaryFields[9]))
2942 self
.sendP2PACK(binaryFields
)
2943 self
.handlePacket
= self
.wait_data
2945 def wait_data(self
, packet
):
2946 if MSNP2PDEBUG
: log
.msg("wait_data")
2947 binaryFields
= BinaryFields()
2948 binaryFields
.unpackFields(packet
)
2950 if binaryFields
[5] != self
.dataFlag
:
2951 self
.warn("field5," + str(binaryFields
[5]))
2953 # Just ignore the footer
2954 #if binaryFields[9] != 1:
2955 # self.warn("field9," + str(binaryFields[9]))
2957 offset
= binaryFields
[2]
2958 total
= binaryFields
[3]
2959 length
= binaryFields
[4]
2961 data
= packet
[48:-4]
2962 if offset
!= self
.pos
:
2963 self
.warn("Received packet out of order")
2964 self
.consumer
.error()
2966 if len(data
) != length
:
2967 self
.warn("Received bad length of slp")
2968 self
.consumer
.error()
2973 self
.consumer
.write(str(data
))
2975 if self
.pos
== total
:
2976 self
.sendP2PACK(binaryFields
)
2977 self
.consumer
.close()
2978 self
.handlePacket
= None
2981 def doFinished(self
):
2982 raise NotImplementedError
2985 class SLPLink_FileReceive(SLPLink_Receive
, FileReceive
):
2986 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
, sessionID
, sessionGuid
, branch
):
2987 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=self
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2988 self
.dataFlag
= BinaryFields
.DATAFT
2989 self
.initialBranch
= branch
2990 FileReceive
.__init
__(self
, filename
, filesize
, remoteUser
)
2993 # Send a 603 decline
2994 if not self
.switchboard
: return
2995 self
.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2998 def accept(self
, consumer
):
2999 FileReceive
.accept(self
, consumer
)
3000 if not self
.switchboard
: return
3001 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
3002 self
.handlePacket
= self
.wait_data
# Moved here because sometimes the second INVITE seems to be skipped
3004 def handleSLPMessage(self
, slpMessage
):
3005 if slpMessage
.method
== "INVITE": # The second invite
3006 data
= {"Bridge" : "TCPv1",\
3007 "Listening" : "false",\
3008 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
3009 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
3010 # self.handlePacket = self.wait_data # Moved up
3012 if MSNP2PDEBUG
: log
.msg("It's either a BYE or an error")
3014 # FIXME, do some error handling if it was an error
3016 def doFinished(self
):
3017 #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3019 # Wait for BYE? #FIXME
3022 class SLPLink_AvatarReceive(SLPLink_Receive
):
3023 def __init__(self
, remoteUser
, switchboard
, consumer
, context
):
3024 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=consumer
, context
=context
)
3025 self
.dataFlag
= BinaryFields
.DATA
3026 data
= {"EUF-GUID" : MSN_AVATAR_GUID
,\
3027 "SessionID": self
.sessionID
,\
3029 "Context" : context
}
3030 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
3031 self
.handlePacket
= self
.wait_dataprep
3033 def handleSLPMessage(self
, slpMessage
):
3034 if slpMessage
.method
== "INVITE": # The second invite
3035 data
= {"Bridge" : "TCPv1",\
3036 "Listening" : "false",\
3037 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
3038 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
3039 elif slpMessage
.status
== "200":
3041 #self.handlePacket = self.wait_dataprep # Moved upwards
3043 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to error or BYE")
3046 def doFinished(self
):
3047 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3049 # mapping of error codes to error messages
3052 200 : "Syntax error",
3053 201 : "Invalid parameter",
3054 205 : "Invalid user",
3055 206 : "Domain name missing",
3056 207 : "Already logged in",
3057 208 : "Invalid username",
3058 209 : "Invalid screen name",
3059 210 : "User list full",
3060 215 : "User already there",
3061 216 : "User already on list",
3062 217 : "User not online",
3063 218 : "Already in mode",
3064 219 : "User is in the opposite list",
3065 223 : "Too many groups",
3066 224 : "Invalid group",
3067 225 : "User not in group",
3068 229 : "Group name too long",
3069 230 : "Cannot remove group 0",
3070 231 : "Invalid group",
3071 280 : "Switchboard failed",
3072 281 : "Transfer to switchboard failed",
3074 300 : "Required field missing",
3075 301 : "Too many FND responses",
3076 302 : "Not logged in",
3078 400 : "Message not allowed",
3079 402 : "Error accessing contact list",
3080 403 : "Error accessing contact list",
3082 500 : "Internal server error",
3083 501 : "Database server error",
3084 502 : "Command disabled",
3085 510 : "File operation failed",
3086 520 : "Memory allocation failed",
3087 540 : "Wrong CHL value sent to server",
3089 600 : "Server is busy",
3090 601 : "Server is unavaliable",
3091 602 : "Peer nameserver is down",
3092 603 : "Database connection failed",
3093 604 : "Server is going down",
3094 605 : "Server unavailable",
3096 707 : "Could not create connection",
3097 710 : "Invalid CVR parameters",
3098 711 : "Write is blocking",
3099 712 : "Session is overloaded",
3100 713 : "Too many active users",
3101 714 : "Too many sessions",
3102 715 : "Not expected",
3103 717 : "Bad friend file",
3104 731 : "Not expected",
3106 800 : "Requests too rapid",
3108 910 : "Server too busy",
3109 911 : "Authentication failed",
3110 912 : "Server too busy",
3111 913 : "Not allowed when offline",
3112 914 : "Server too busy",
3113 915 : "Server too busy",
3114 916 : "Server too busy",
3115 917 : "Server too busy",
3116 918 : "Server too busy",
3117 919 : "Server too busy",
3118 920 : "Not accepting new users",
3119 921 : "Server too busy",
3120 922 : "Server too busy",
3121 923 : "No parent consent",
3122 924 : "Passport account not yet verified"
3126 # mapping of status codes to readable status format
3129 STATUS_ONLINE
: "Online",
3130 STATUS_OFFLINE
: "Offline",
3131 STATUS_HIDDEN
: "Appear Offline",
3132 STATUS_IDLE
: "Idle",
3133 STATUS_AWAY
: "Away",
3134 STATUS_BUSY
: "Busy",
3135 STATUS_BRB
: "Be Right Back",
3136 STATUS_PHONE
: "On the Phone",
3137 STATUS_LUNCH
: "Out to Lunch"
3141 # mapping of list ids to list codes
3144 FORWARD_LIST
: 'fl',
3147 REVERSE_LIST
: 'rl',
3152 # mapping of list codes to list ids
3154 for id,code
in listIDToCode
.items():
3155 listCodeToID
[code
] = id