1 # Twisted, the Framework of Your Internet
2 # Copyright (C) 2001-2002 Matthew W. Lefkowitz
3 # Copyright (C) 2004-2005 James C. Bunton
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of version 2.1 of the GNU Lesser General Public
7 # License as published by the Free Software Foundation.
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 MSNP11 Protocol (client only) - semi-experimental
24 This module provides support for clients using the MSN Protocol (MSNP11).
25 There are basically 3 servers involved in any MSN session:
29 The DispatchClient class handles connections to the
30 dispatch server, which basically delegates users to a
31 suitable notification server.
33 You will want to subclass this and handle the gotNotificationReferral
36 I{Notification Server}
38 The NotificationClient class handles connections to the
39 notification server, which acts as a session server
40 (state updates, message negotiation etc...)
44 The SwitchboardClient handles connections to switchboard
45 servers which are used to conduct conversations with other users.
47 There are also two classes (FileSend and FileReceive) used
50 Clients handle events in two ways.
52 - each client request requiring a response will return a Deferred,
53 the callback for same will be fired when the server sends the
55 - Events which are not in response to any client request have
56 respective methods which should be overridden and handled in
59 Most client request callbacks require more than one argument,
60 and since Deferreds can only pass the callback one result,
61 most of the time the callback argument will be a tuple of
62 values (documented in the respective request method).
63 To make reading/writing code easier, callbacks can be defined in
64 a number of ways to handle this 'cleanly'. One way would be to
65 define methods like: def callBack(self, (arg1, arg2, arg)): ...
66 another way would be to do something like:
67 d.addCallback(lambda result: myCallback(*result)).
69 If the server sends an error response to a client request,
70 the errback of the corresponding Deferred will be called,
71 the argument being the corresponding error code.
74 Due to the lack of an official spec for MSNP11, extra checking
75 than may be deemed necessary often takes place considering the
76 server is never 'wrong'. Thus, if gotBadLine (in any of the 3
77 main clients) is called, or an MSNProtocolError is raised, it's
78 probably a good idea to submit a bug report. ;)
79 Use of this module requires that PyOpenSSL is installed.
81 @author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
82 @author: U{James Bunton<mailto:james@delx.cjb.net>}
85 from __future__
import nested_scopes
88 from twisted
.protocols
.basic
import LineReceiver
90 from twisted
.web
.http
import HTTPClient
93 from twisted
.protocols
.http
import HTTPClient
95 print "Couldn't find a HTTPClient. If you're using Twisted 2.0 make sure you've installed twisted.web"
100 from twisted
.internet
import reactor
, task
101 from twisted
.internet
.defer
import Deferred
102 from twisted
.internet
.protocol
import ReconnectingClientFactory
, ClientFactory
104 from twisted
.internet
.ssl
import ClientContextFactory
106 print "You must install pycrypto and pyopenssl."
108 from twisted
.python
import failure
, log
111 from tlib
import xmlw
114 import types
, operator
, os
, sys
, base64
, random
, struct
, random
, sha
, base64
, StringIO
, array
, codecs
, binascii
115 from urllib
import quote
, unquote
118 MSN_PROTOCOL_VERSION
= "MSNP11 CVR0" # protocol version
119 MSN_PORT
= 1863 # default dispatch server port
120 MSN_MAX_MESSAGE
= 1664 # max message length
121 MSN_CVR_STR
= "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
122 MSN_AVATAR_GUID
= "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
123 MSN_MSNFTP_GUID
= "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
145 STATUS_ONLINE
= 'NLN'
146 STATUS_OFFLINE
= 'FLN'
147 STATUS_HIDDEN
= 'HDN'
162 P2PSEQ
= [-3, -2, 0, -1, 1, 2, 3, 4, 5, 6, 7, 8]
171 return inp
.split('=')[1]
183 userHandle
= getVal(p
)
185 screenName
= unquote(getVal(p
))
190 else: # Must be the groups
192 groups
= p
.split(',')
194 raise MSNProtocolError
, "Unknown LST/ADC response" + str(params
) # debug
196 return userHandle
, screenName
, userGuid
, lists
, groups
199 """ Needed for Python 2.3 compatibility """
200 return s
+ (n
-len(s
))*c
202 if sys
.byteorder
== "little":
204 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
205 a
= array
.array("h", s
.encode("utf-16")[2:])
210 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
211 return s
.encode("utf-16")[2:]
214 return base64
.encodestring(s
).replace("\n", "")
217 for pad
in ["", "=", "==", "A", "A=", "A=="]: # Stupid MSN client!
219 return base64
.decodestring(s
+ pad
)
222 raise ValueError("Got some very bad base64!")
225 format
= "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
228 data
.append(random
.random() * 0xAAFF + 0x1111)
233 def checkParamLen(num
, expected
, cmd
, error
=None):
234 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
235 if num
!= expected
: raise MSNProtocolError
, error
237 def _parseHeader(h
, v
):
239 Split a certin number of known
240 header values with the format:
241 field1=val,field2=val,field3=val into
242 a dict mapping fields to values.
243 @param h: the header's key
244 @param v: the header's value as a string
247 if h
in ('passporturls','authentication-info','www-authenticate'):
248 v
= v
.replace('Passport1.4','').lstrip()
250 for fieldPair
in v
.split(','):
252 field
,value
= fieldPair
.split('=',1)
253 fields
[field
.lower()] = value
255 fields
[field
.lower()] = ''
259 def _parsePrimitiveHost(host
):
261 h
,p
= host
.replace('https://','').split('/',1)
265 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
=''):
267 This function is used internally and should not ever be called
271 def _cb(server
, auth
):
272 loginFac
= ClientFactory()
273 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
274 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
277 _cb(nexusServer
, authData
)
279 fac
= ClientFactory()
281 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
282 d
.addErrback(lambda f
: cb
.errback(f
))
283 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
284 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
288 class PassportNexus(HTTPClient
):
291 Used to obtain the URL of a valid passport
294 This class is used internally and should
295 not be instantiated directly -- that is,
296 The passport logging in process is handled
297 transparantly by NotificationClient.
300 def __init__(self
, deferred
, host
):
301 self
.deferred
= deferred
302 self
.host
, self
.path
= _parsePrimitiveHost(host
)
304 def connectionMade(self
):
305 HTTPClient
.connectionMade(self
)
306 self
.sendCommand('GET', self
.path
)
307 self
.sendHeader('Host', self
.host
)
311 def handleHeader(self
, header
, value
):
313 self
.headers
[h
] = _parseHeader(h
, value
)
315 def handleEndHeaders(self
):
316 if self
.connected
: self
.transport
.loseConnection()
317 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
318 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
320 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
322 def handleResponse(self
, r
): pass
324 class PassportLogin(HTTPClient
):
326 This class is used internally to obtain
327 a login ticket from a passport HTTPS
328 server -- it should not be used directly.
333 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
334 self
.deferred
= deferred
335 self
.userHandle
= userHandle
337 self
.authData
= authData
338 self
.host
, self
.path
= _parsePrimitiveHost(host
)
340 def connectionMade(self
):
341 self
.sendCommand('GET', self
.path
)
342 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
343 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), quote(self
.passwd
), self
.authData
))
344 self
.sendHeader('Host', self
.host
)
348 def handleHeader(self
, header
, value
):
350 self
.headers
[h
] = _parseHeader(h
, value
)
352 def handleEndHeaders(self
):
353 if self
._finished
: return
354 self
._finished
= 1 # I think we need this because of HTTPClient
355 if self
.connected
: self
.transport
.loseConnection()
356 authHeader
= 'authentication-info'
357 _interHeader
= 'www-authenticate'
358 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
360 info
= self
.headers
[authHeader
]
361 status
= info
['da-status']
362 handler
= getattr(self
, 'login_%s' % (status
,), None)
365 else: raise Exception()
367 self
.deferred
.errback(failure
.Failure(e
))
369 def handleResponse(self
, r
): pass
371 def login_success(self
, info
):
372 ticket
= info
['from-pp']
373 ticket
= ticket
[1:len(ticket
)-1]
374 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
376 def login_failed(self
, info
):
377 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
379 def login_redir(self
, info
):
380 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
382 class MSNProtocolError(Exception):
384 This Exception is basically used for debugging
385 purposes, as the official MSN server should never
386 send anything _wrong_ and nobody in their right
387 mind would run their B{own} MSN server.
388 If it is raised by default command handlers
389 (handle_BLAH) the error will be logged.
396 I am the class used to represent an 'instant' message.
398 @ivar userHandle: The user handle (passport) of the sender
399 (this is only used when receiving a message)
400 @ivar screenName: The screen name of the sender (this is only used
401 when receiving a message)
402 @ivar message: The message
403 @ivar headers: The message headers
405 @ivar length: The message length (including headers and line endings)
406 @ivar ack: This variable is used to tell the server how to respond
407 once the message has been sent. If set to MESSAGE_ACK
408 (default) the server will respond with an ACK upon receiving
409 the message, if set to MESSAGE_NACK the server will respond
410 with a NACK upon failure to receive the message.
411 If set to MESSAGE_ACK_NONE the server will do nothing.
412 This is relevant for the return value of
413 SwitchboardClient.sendMessage (which will return
414 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
415 and will fire when the respective ACK or NACK is received).
416 If set to MESSAGE_ACK_NONE sendMessage will return None.
419 MESSAGE_ACK_FAT
= 'D'
421 MESSAGE_ACK_NONE
= 'U'
425 def __init__(self
, length
=0, userHandle
="", screenName
="", message
="", specialMessage
=False):
426 self
.userHandle
= userHandle
427 self
.screenName
= screenName
428 self
.specialMessage
= specialMessage
429 self
.message
= message
430 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain; charset=UTF-8'}
434 def _calcMessageLen(self
):
436 used to calculte the number to send
437 as the message length when sending a message.
439 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
441 def delHeader(self
, header
):
442 """ delete the desired header """
443 if self
.headers
.has_key(header
):
444 del self
.headers
[header
]
446 def setHeader(self
, header
, value
):
447 """ set the desired header """
448 self
.headers
[header
] = value
450 def getHeader(self
, header
):
452 get the desired header value
453 @raise KeyError: if no such header exists.
455 return self
.headers
[header
]
457 def hasHeader(self
, header
):
458 """ check to see if the desired header exists """
459 return self
.headers
.has_key(header
)
461 def getMessage(self
):
462 """ return the message - not including headers """
465 def setMessage(self
, message
):
466 """ set the message text """
467 self
.message
= message
472 Used to represent a MSNObject. This can be currently only be an avatar.
474 @ivar creator: The userHandle of the creator of this picture.
475 @ivar imageData: The PNG image data (only for our own avatar)
476 @ivar type: Always set to 3, for avatar.
477 @ivar size: The size of the image.
478 @ivar location: The filename of the image.
479 @ivar friendly: Unknown.
480 @ivar text: The textual representation of this MSNObject.
482 def __init__(self
, s
=""):
483 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
488 def setData(self
, creator
, imageData
):
489 """ Set the creator and imageData for this object """
490 self
.creator
= creator
491 self
.imageData
= imageData
492 self
.size
= len(imageData
)
494 self
.location
= "TMP" + str(random
.randint(1000,9999))
495 self
.friendly
= "AAA="
496 self
.sha1d
= b64enc(sha
.sha(imageData
).digest())
510 """ Makes a textual representation of this MSNObject. Stores it in self.text """
513 h
.append(self
.creator
)
515 h
.append(str(self
.size
))
517 h
.append(str(self
.type))
519 h
.append(self
.location
)
521 h
.append(self
.friendly
)
524 sha1c
= b64enc(sha
.sha("".join(h
)).digest())
525 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
)
528 e
= xmlw
.parseText(s
, True)
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")
541 This class represents a contact (user).
543 @ivar userGuid: The contact's user guid (unique string)
544 @ivar userHandle: The contact's user handle (passport).
545 @ivar screenName: The contact's screen name.
546 @ivar groups: A list of all the group IDs which this
548 @ivar lists: An integer representing the sum of all lists
549 that this contact belongs to.
550 @ivar caps: int, The capabilities of this client
551 @ivar msnobj: The MSNObject representing the contact's avatar
552 @ivar status: The contact's status code.
553 @type status: str if contact's status is known, None otherwise.
554 @ivar personal: The contact's personal message .
555 @type personal: str if contact's personal message is known, None otherwise.
557 @ivar homePhone: The contact's home phone number.
558 @type homePhone: str if known, otherwise None.
559 @ivar workPhone: The contact's work phone number.
560 @type workPhone: str if known, otherwise None.
561 @ivar mobilePhone: The contact's mobile phone number.
562 @type mobilePhone: str if known, otherwise None.
563 @ivar hasPager: Whether or not this user has a mobile pager
564 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
572 def __init__(self
, userGuid
="", userHandle
="", screenName
="", lists
=0, caps
=0, msnobj
=None, groups
={}, status
=None, personal
=""):
573 self
.userGuid
= userGuid
574 self
.userHandle
= userHandle
575 self
.screenName
= screenName
579 self
.msnobjGot
= True
580 self
.groups
= [] # if applicable
581 self
.status
= status
# current status
582 self
.personal
= personal
585 self
.homePhone
= None
586 self
.workPhone
= None
587 self
.mobilePhone
= None
591 def setPhone(self
, phoneType
, value
):
593 set phone numbers/values for this specific user.
594 for phoneType check the *_PHONE constants and HAS_PAGER
597 t
= phoneType
.upper()
598 if t
== HOME_PHONE
: self
.homePhone
= value
599 elif t
== WORK_PHONE
: self
.workPhone
= value
600 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
601 elif t
== HAS_PAGER
: self
.hasPager
= value
602 elif t
== HAS_BLOG
: self
.hasBlog
= value
603 #else: raise ValueError, "Invalid Phone Type: " + t
605 def addToList(self
, listType
):
607 Update the lists attribute to
608 reflect being part of the
611 self
.lists |
= listType
613 def removeFromList(self
, listType
):
615 Update the lists attribute to
616 reflect being removed from the
619 self
.lists ^
= listType
621 class MSNContactList
:
623 This class represents a basic MSN contact list.
625 @ivar contacts: All contacts on my various lists
626 @type contacts: dict (mapping user handles to MSNContact objects)
627 @ivar groups: a mapping of group ids to group names
628 (groups can only exist on the forward list)
632 This is used only for storage and doesn't effect the
633 server's contact list.
643 def _getContactsFromList(self
, listType
):
645 Obtain all contacts which belong
646 to the given list type.
648 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
650 def addContact(self
, contact
):
654 self
.contacts
[contact
.userHandle
] = contact
656 def remContact(self
, userHandle
):
661 del self
.contacts
[userHandle
]
662 except KeyError: pass
664 def getContact(self
, userHandle
):
666 Obtain the MSNContact object
667 associated with the given
669 @return: the MSNContact object if
670 the user exists, or None.
673 return self
.contacts
[userHandle
]
677 def getBlockedContacts(self
):
679 Obtain all the contacts on my block list
681 return self
._getContactsFromList
(BLOCK_LIST
)
683 def getAuthorizedContacts(self
):
685 Obtain all the contacts on my auth list.
686 (These are contacts which I have verified
687 can view my state changes).
689 return self
._getContactsFromList
(ALLOW_LIST
)
691 def getReverseContacts(self
):
693 Get all contacts on my reverse list.
694 (These are contacts which have added me
695 to their forward list).
697 return self
._getContactsFromList
(REVERSE_LIST
)
699 def getContacts(self
):
701 Get all contacts on my forward list.
702 (These are the contacts which I have added
705 return self
._getContactsFromList
(FORWARD_LIST
)
707 def setGroup(self
, id, name
):
709 Keep a mapping from the given id
712 self
.groups
[id] = name
714 def remGroup(self
, id):
716 Removed the stored group
717 mapping for the given id.
721 except KeyError: pass
722 for c
in self
.contacts
:
723 if id in c
.groups
: c
.groups
.remove(id)
726 class MSNEventBase(LineReceiver
):
728 This class provides support for handling / dispatching events and is the
729 base class of the three main client protocols (DispatchClient,
730 NotificationClient, SwitchboardClient)
734 self
.ids
= {} # mapping of ids to Deferreds
738 self
.currentMessage
= None
740 def connectionLost(self
, reason
):
744 def connectionMade(self
):
747 def _fireCallback(self
, id, *args
):
749 Fire the callback for the given id
750 if one exists and return 1, else return false
752 if self
.ids
.has_key(id):
753 self
.ids
[id][0].callback(args
)
758 def _nextTransactionID(self
):
759 """ return a usable transaction ID """
761 if self
.currentID
> 1000: self
.currentID
= 1
762 return self
.currentID
764 def _createIDMapping(self
, data
=None):
766 return a unique transaction ID that is mapped internally to a
767 deferred .. also store arbitrary data if it is needed
769 id = self
._nextTransactionID
()
771 self
.ids
[id] = (d
, data
)
774 def checkMessage(self
, message
):
776 process received messages to check for file invitations and
777 typing notifications and other control type messages
779 raise NotImplementedError
781 def sendLine(self
, line
):
782 if LINEDEBUG
: log
.msg("<< " + line
)
783 LineReceiver
.sendLine(self
, line
)
785 def lineReceived(self
, line
):
786 if LINEDEBUG
: log
.msg(">> " + line
)
787 if not self
.connected
: return
788 if self
.currentMessage
:
789 self
.currentMessage
.readPos
+= len(line
+"\r\n")
791 header
, value
= line
.split(':')
792 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
795 #raise MSNProtocolError, "Invalid Message Header"
797 if line
== "" or self
.currentMessage
.specialMessage
:
799 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
802 cmd
, params
= line
.split(' ', 1)
804 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
806 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
808 id = params
.split(' ')[0]
809 if id.isdigit() and self
.ids
.has_key(int(id)):
811 self
.ids
[id][0].errback(int(cmd
))
814 else: # we received an error which doesn't map to a sent command
815 self
.gotError(int(cmd
))
818 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
820 try: handler(params
.split(' '))
821 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
823 self
.handle_UNKNOWN(cmd
, params
.split(' '))
825 def rawDataReceived(self
, data
):
826 if not self
.connected
: return
828 self
.currentMessage
.readPos
+= len(data
)
829 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
831 self
.currentMessage
.message
+= data
[:-diff
]
834 self
.currentMessage
.message
+= data
836 self
.currentMessage
.message
+= data
838 del self
.currentMessage
.readPos
839 m
= self
.currentMessage
840 self
.currentMessage
= None
841 if MESSAGEDEBUG
: log
.msg(m
.message
)
843 if not self
.checkMessage(m
):
844 self
.setLineMode(extra
)
847 self
.setLineMode(extra
)
850 self
.setLineMode(extra
)
852 ### protocol command handlers - no need to override these.
854 def handle_MSG(self
, params
):
855 checkParamLen(len(params
), 3, 'MSG')
857 messageLen
= int(params
[2])
858 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
859 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
861 def handle_UNKNOWN(self
, cmd
, params
):
862 """ implement me in subclasses if you want to handle unknown events """
863 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
867 def gotBadLine(self
, line
, why
):
868 """ called when a handler notifies me that this line is broken """
869 log
.msg('Error in line: %s (%s)' % (line
, why
))
871 def gotError(self
, errorCode
):
873 called when the server sends an error which is not in
874 response to a sent command (ie. it has no matching transaction ID)
876 log
.msg('Error %s' % (errorCodes
[errorCode
]))
879 class DispatchClient(MSNEventBase
):
881 This class provides support for clients connecting to the dispatch server
882 @ivar userHandle: your user handle (passport) needed before connecting.
885 def connectionMade(self
):
886 MSNEventBase
.connectionMade(self
)
887 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
889 ### protocol command handlers ( there is no need to override these )
891 def handle_VER(self
, params
):
892 versions
= params
[1:]
893 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
894 self
.transport
.loseConnection()
895 raise MSNProtocolError
, "Invalid version response"
896 id = self
._nextTransactionID
()
897 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.factory
.userHandle
))
899 def handle_CVR(self
, params
):
900 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
902 def handle_XFR(self
, params
):
903 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
904 id, refType
, addr
= params
[:3]
905 # was addr a host:port pair?
907 host
, port
= addr
.split(':')
912 self
.gotNotificationReferral(host
, int(port
))
916 def gotNotificationReferral(self
, host
, port
):
918 called when we get a referral to the notification server.
920 @param host: the notification server's hostname
921 @param port: the port to connect to
926 class DispatchFactory(ClientFactory
):
928 This class keeps the state for the DispatchClient.
930 @ivar userHandle: the userHandle to request a notification
933 protocol
= DispatchClient
938 class NotificationClient(MSNEventBase
):
940 This class provides support for clients connecting
941 to the notification server.
944 factory
= None # sssh pychecker
946 def __init__(self
, currentID
=0):
947 MSNEventBase
.__init
__(self
)
948 self
.currentID
= currentID
949 self
._state
= ['DISCONNECTED', {}]
951 self
.pingCheckTask
= None
952 self
.msnobj
= MSNObject()
954 def _setState(self
, state
):
955 self
._state
[0] = state
958 return self
._state
[0]
960 def _getStateData(self
, key
):
961 return self
._state
[1][key
]
963 def _setStateData(self
, key
, value
):
964 self
._state
[1][key
] = value
966 def _remStateData(self
, *args
):
967 for key
in args
: del self
._state
[1][key
]
969 def connectionMade(self
):
970 MSNEventBase
.connectionMade(self
)
971 self
._setState
('CONNECTED')
972 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
973 self
.factory
.resetDelay()
975 def connectionLost(self
, reason
):
976 self
._setState
('DISCONNECTED')
978 if self
.pingCheckTask
:
979 self
.pingCheckTask
.stop()
980 self
.pingCheckTask
= None
981 MSNEventBase
.connectionLost(self
, reason
)
983 def _getEmailFields(self
, message
):
984 fields
= message
.getMessage().strip().split('\n')
988 if len(a
) != 2: continue
995 def _gotInitialEmailNotification(self
, message
):
996 values
= self
._getEmailFields
(message
)
998 inboxunread
= int(values
["Inbox-Unread"])
999 foldersunread
= int(values
["Folders-Unread"])
1002 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
1003 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
1005 def _gotEmailNotification(self
, message
):
1006 values
= self
._getEmailFields
(message
)
1008 mailfrom
= values
["From"]
1009 fromaddr
= values
["From-Addr"]
1010 subject
= values
["Subject"]
1011 junkbeginning
= "=?\"us-ascii\"?Q?"
1013 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
1015 # If any of the fields weren't found then it's not a big problem. We just ignore the message
1017 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
1019 def _gotMSNAlert(self
, message
):
1020 notification
= xmlw
.parseText(message
.message
, beExtremelyLenient
=True)
1021 siteurl
= notification
.getAttribute("siteurl")
1022 notid
= notification
.getAttribute("id")
1025 for e
in notification
.elements():
1031 msgid
= msg
.getAttribute("id")
1036 for e
in msg
.elements():
1037 if e
.name
== "ACTION":
1038 action
= e
.getAttribute("url")
1039 if e
.name
== "SUBSCR":
1040 subscr
= e
.getAttribute("url")
1041 if e
.name
== "BODY":
1042 for e2
in e
.elements():
1043 if e2
.name
== "TEXT":
1044 bodytext
= e2
.__str__()
1045 if not (action
and subscr
and bodytext
): return
1047 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
1048 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
1050 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
1052 def _gotUBX(self
, message
):
1053 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
1054 if not msnContact
: return
1055 lm
= message
.message
.lower()
1056 p1
= lm
.find("<psm>") + 5
1057 p2
= lm
.find("</psm>")
1058 if p1
>= 0 and p2
>= 0:
1059 personal
= xmlw
.unescapeFromXml(message
.message
[p1
:p2
])
1060 msnContact
.personal
= personal
1061 self
.contactPersonalChanged(message
.userHandle
, personal
)
1063 msnContact
.personal
= ''
1064 self
.contactPersonalChanged(message
.userHandle
, '')
1066 def checkMessage(self
, message
):
1067 """ hook used for detecting specific notification messages """
1068 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
1069 if 'text/x-msmsgsprofile' in cTypes
:
1070 self
.gotProfile(message
)
1072 elif "text/x-msmsgsinitialemailnotification" in cTypes
:
1073 self
._gotInitialEmailNotification
(message
)
1075 elif "text/x-msmsgsemailnotification" in cTypes
:
1076 self
._gotEmailNotification
(message
)
1078 elif "NOTIFICATION" == message
.userHandle
and message
.specialMessage
== True:
1079 self
._gotMSNAlert
(message
)
1081 elif "UBX" == message
.screenName
and message
.specialMessage
== True:
1082 self
._gotUBX
(message
)
1086 ### protocol command handlers - no need to override these
1088 def handle_VER(self
, params
):
1089 versions
= params
[1:]
1090 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
1091 self
.transport
.loseConnection()
1092 raise MSNProtocolError
, "Invalid version response"
1093 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
1095 def handle_CVR(self
, params
):
1096 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
1098 def handle_USR(self
, params
):
1099 if not (4 <= len(params
) <= 6):
1100 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
1102 mechanism
= params
[1]
1103 if mechanism
== "OK":
1104 self
.loggedIn(params
[2], int(params
[3]))
1105 elif params
[2].upper() == "S":
1106 # we need to obtain auth from a passport server
1108 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3])
1109 d
.addCallback(self
._passportLogin
)
1110 d
.addErrback(self
._passportError
)
1112 def _passportLogin(self
, result
):
1113 if result
[0] == LOGIN_REDIRECT
:
1114 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
1115 result
[1], cached
=1, authData
=result
[2])
1116 d
.addCallback(self
._passportLogin
)
1117 d
.addErrback(self
._passportError
)
1118 elif result
[0] == LOGIN_SUCCESS
:
1119 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
1120 elif result
[0] == LOGIN_FAILURE
:
1121 self
.loginFailure(result
[1])
1123 def _passportError(self
, failure
):
1124 self
.loginFailure("Exception while authenticating: %s" % failure
)
1126 def handle_CHG(self
, params
):
1128 if not self
._fireCallback
(id, params
[1]):
1129 if self
.factory
: self
.factory
.status
= params
[1]
1130 self
.statusChanged(params
[1])
1132 def handle_ILN(self
, params
):
1133 #checkParamLen(len(params), 6, 'ILN')
1134 msnContact
= self
.factory
.contacts
.getContact(params
[2])
1135 if not msnContact
: return
1136 msnContact
.status
= params
[1]
1137 msnContact
.screenName
= unquote(params
[3])
1138 if len(params
) > 4: msnContact
.caps
= int(params
[4])
1140 self
.handleAvatarHelper(msnContact
, params
[5])
1142 self
.handleAvatarGoneHelper(msnContact
)
1143 self
.gotContactStatus(params
[2], params
[1], unquote(params
[3]))
1145 def handleAvatarGoneHelper(self
, msnContact
):
1146 if msnContact
.msnobj
:
1147 msnContact
.msnobj
= None
1148 msnContact
.msnobjGot
= True
1149 self
.contactAvatarChanged(msnContact
.userHandle
, "")
1151 def handleAvatarHelper(self
, msnContact
, msnobjStr
):
1152 msnobj
= MSNObject(unquote(msnobjStr
))
1153 if not msnContact
.msnobj
or msnobj
.sha1d
!= msnContact
.msnobj
.sha1d
:
1154 if MSNP2PDEBUG
: log
.msg("Updated MSNObject received!" + msnobjStr
)
1155 msnContact
.msnobj
= msnobj
1156 msnContact
.msnobjGot
= False
1157 self
.contactAvatarChanged(msnContact
.userHandle
, binascii
.hexlify(b64dec(msnContact
.msnobj
.sha1d
)))
1159 def handle_CHL(self
, params
):
1160 checkParamLen(len(params
), 2, 'CHL')
1161 response
= msnp11chl
.doChallenge(params
[1])
1162 self
.sendLine("QRY %s %s %s" % (self
._nextTransactionID
(), msnp11chl
.MSNP11_PRODUCT_ID
, len(response
)))
1163 self
.transport
.write(response
)
1165 def handle_QRY(self
, params
):
1168 def handle_NLN(self
, params
):
1169 if not self
.factory
: return
1170 msnContact
= self
.factory
.contacts
.getContact(params
[1])
1171 if not msnContact
: return
1172 msnContact
.status
= params
[0]
1173 msnContact
.screenName
= unquote(params
[2])
1174 if len(params
) > 3: msnContact
.caps
= int(params
[3])
1176 self
.handleAvatarHelper(msnContact
, params
[4])
1178 self
.handleAvatarGoneHelper(msnContact
)
1179 self
.contactStatusChanged(params
[1], params
[0], unquote(params
[2]))
1181 def handle_FLN(self
, params
):
1182 checkParamLen(len(params
), 1, 'FLN')
1183 msnContact
= self
.factory
.contacts
.getContact(params
[0])
1185 msnContact
.status
= STATUS_OFFLINE
1186 self
.contactOffline(params
[0])
1188 def handle_LST(self
, params
):
1189 if self
._getState
() != 'SYNC': return
1191 userHandle
, screenName
, userGuid
, lists
, groups
= getVals(params
)
1193 if not userHandle
or lists
< 1:
1194 raise MSNProtocolError
, "Unknown LST " + str(params
) # debug
1195 contact
= MSNContact(userGuid
, userHandle
, screenName
, lists
)
1196 if contact
.lists
& FORWARD_LIST
:
1197 contact
.groups
.extend(map(str, groups
))
1198 self
._getStateData
('list').addContact(contact
)
1199 self
._setStateData
('last_contact', contact
)
1200 sofar
= self
._getStateData
('lst_sofar') + 1
1201 if sofar
== self
._getStateData
('lst_reply'):
1202 # this is the best place to determine that
1203 # a syn realy has finished - msn _may_ send
1204 # BPR information for the last contact
1205 # which is unfortunate because it means
1206 # that the real end of a syn is non-deterministic.
1207 # to handle this we'll keep 'last_contact' hanging
1208 # around in the state data and update it if we need
1210 self
._setState
('SESSION')
1211 contacts
= self
._getStateData
('list')
1212 phone
= self
._getStateData
('phone')
1213 id = self
._getStateData
('synid')
1214 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1215 self
._fireCallback
(id, contacts
, phone
)
1217 self
._setStateData
('lst_sofar',sofar
)
1219 def handle_BLP(self
, params
):
1220 # check to see if this is in response to a SYN
1221 if self
._getState
() == 'SYNC':
1222 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
1225 self
.factory
.contacts
.privacy
= listCodeToID
[params
[1].lower()]
1226 self
._fireCallback
(id, params
[1])
1228 def handle_GTC(self
, params
):
1229 # check to see if this is in response to a SYN
1230 if self
._getState
() == 'SYNC':
1231 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
1232 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
1233 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1236 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
1237 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
1238 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1240 def handle_SYN(self
, params
):
1242 self
._setStateData
('phone', []) # Always needs to be set
1243 if params
[3] == 0: # No LST will be received. New account?
1244 self
._setState
('SESSION')
1245 self
._fireCallback
(id, None, None)
1247 contacts
= MSNContactList()
1248 self
._setStateData
('list', contacts
)
1249 self
._setStateData
('lst_reply', int(params
[3]))
1250 self
._setStateData
('lsg_reply', int(params
[4]))
1251 self
._setStateData
('lst_sofar', 0)
1253 def handle_LSG(self
, params
):
1254 if self
._getState
() == 'SYNC':
1255 self
._getStateData
('list').groups
[params
[1]] = unquote(params
[0])
1257 def handle_PRP(self
, params
):
1258 if params
[1] == "MFN":
1259 self
._fireCallback
(int(params
[0]))
1260 elif self
._getState
() == 'SYNC':
1261 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
1263 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
1265 def handle_BPR(self
, params
):
1266 numParams
= len(params
)
1267 if numParams
== 2: # part of a syn
1268 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
1269 elif numParams
== 4:
1270 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_BPR called with no contact list" # debug
1271 self
.factory
.contacts
.version
= int(params
[0])
1272 userHandle
, phoneType
, number
= params
[1], params
[2], unquote(params
[3])
1273 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1274 self
.gotPhoneNumber(userHandle
, phoneType
, number
)
1277 def handle_ADG(self
, params
):
1278 checkParamLen(len(params
), 5, 'ADG')
1280 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
1281 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
1283 def handle_RMG(self
, params
):
1284 checkParamLen(len(params
), 3, 'RMG')
1286 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
1287 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
1289 def handle_REG(self
, params
):
1290 checkParamLen(len(params
), 5, 'REG')
1292 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
1293 raise MSNProtocolError
, "REG response does not match up to a request" # debug
1295 def handle_ADC(self
, params
):
1296 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_ADC called with no contact list"
1297 numParams
= len(params
)
1298 if numParams
< 3 or params
[1].upper() not in ('AL','BL','RL','FL','PL'):
1299 raise MSNProtocolError
, "Invalid Paramaters for ADC" # debug
1301 listType
= params
[1].lower()
1302 userHandle
, screenName
, userGuid
, ignored1
, groups
= getVals(params
[2:])
1304 if groups
and listType
.upper() != FORWARD_LIST
:
1305 raise MSNProtocolError
, "Only forward list can contain groups" # debug
1307 if not self
._fireCallback
(id, listCodeToID
[listType
], userGuid
, userHandle
, screenName
):
1308 c
= self
.factory
.contacts
.getContact(userHandle
)
1310 c
= MSNContact(userGuid
=userGuid
, userHandle
=userHandle
, screenName
=screenName
)
1311 self
.factory
.contacts
.addContact(c
)
1312 c
.addToList(PENDING_LIST
)
1313 self
.userAddedMe(userGuid
, userHandle
, screenName
)
1315 def handle_REM(self
, params
):
1316 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_REM called with no contact list available!"
1317 numParams
= len(params
)
1318 if numParams
< 3 or params
[1].upper() not in ('AL','BL','FL','RL','PL'):
1319 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1321 listType
= params
[1].lower()
1322 userHandle
= params
[2]
1325 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1326 groupID
= int(params
[3])
1327 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, groupID
):
1328 if listType
.upper() != "RL": return
1329 c
= self
.factory
.contacts
.getContact(userHandle
)
1331 c
.removeFromList(REVERSE_LIST
)
1332 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1333 self
.userRemovedMe(userHandle
)
1335 def handle_XFR(self
, params
):
1336 checkParamLen(len(params
), 5, 'XFR')
1338 # check to see if they sent a host/port pair
1340 host
, port
= params
[2].split(':')
1345 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1346 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1348 def handle_RNG(self
, params
):
1349 checkParamLen(len(params
), 6, 'RNG')
1350 # check for host:port pair
1352 host
, port
= params
[1].split(":")
1357 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1360 def handle_NOT(self
, params
):
1361 checkParamLen(len(params
), 1, 'NOT')
1363 messageLen
= int(params
[0])
1364 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1365 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", specialMessage
=True)
1368 def handle_UBX(self
, params
):
1369 checkParamLen(len(params
), 2, 'UBX')
1371 messageLen
= int(params
[1])
1372 except ValueError: raise MSNProtocolError
, "Invalid Parameter for UBX length argument"
1374 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
="UBX", specialMessage
=True)
1377 self
._gotUBX
(MSNMessage(userHandle
=params
[0]))
1379 def handle_UUX(self
, params
):
1380 checkParamLen(len(params
), 2, 'UUX')
1381 if params
[1] != '0': return
1383 self
._fireCallback
(id)
1385 def handle_OUT(self
, params
):
1386 checkParamLen(len(params
), 1, 'OUT')
1387 self
.factory
.stopTrying()
1388 if params
[0] == "OTH": self
.multipleLogin()
1389 elif params
[0] == "SSD": self
.serverGoingDown()
1390 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1392 def handle_QNG(self
, params
):
1393 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1397 def pingChecker(self
):
1398 if self
.pingCounter
> 5:
1399 # The server has ignored 5 pings, lets kill the connection
1400 self
.transport
.loseConnection()
1402 self
.sendLine("PNG")
1403 self
.pingCounter
+= 1
1405 def pingCheckerStart(self
, *args
):
1406 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1407 self
.pingCheckTask
.start(PINGSPEED
)
1409 def loggedIn(self
, userHandle
, verified
):
1411 Called when the client has logged in.
1412 The default behaviour of this method is to
1413 update the factory with our screenName and
1414 to sync the contact list (factory.contacts).
1415 When this is complete self.listSynchronized
1418 @param userHandle: our userHandle
1419 @param verified: 1 if our passport has been (verified), 0 if not.
1420 (i'm not sure of the significace of this)
1424 d
.addCallback(self
.listSynchronized
)
1425 d
.addCallback(self
.pingCheckerStart
)
1427 def loginFailure(self
, message
):
1429 Called when the client fails to login.
1431 @param message: a message indicating the problem that was encountered
1435 def gotProfile(self
, message
):
1437 Called after logging in when the server sends an initial
1438 message with MSN/passport specific profile information
1439 such as country, number of kids, etc.
1440 Check the message headers for the specific values.
1442 @param message: The profile message
1446 def listSynchronized(self
, *args
):
1448 Lists are now synchronized by default upon logging in, this
1449 method is called after the synchronization has finished
1450 and the factory now has the up-to-date contacts.
1454 def contactAvatarChanged(self
, userHandle
, hash):
1456 Called when we receive the first, or a new <msnobj/> from a
1459 @param userHandle: contact who's msnobj has been changed
1460 @param hash: sha1 hash of their avatar as hex string
1463 def statusChanged(self
, statusCode
):
1465 Called when our status changes and its not in response to a
1468 @param statusCode: 3-letter status code
1472 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
1474 Called when we receive a list of statuses upon login.
1476 @param userHandle: the contact's user handle (passport)
1477 @param statusCode: 3-letter status code
1478 @param screenName: the contact's screen name
1482 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
1484 Called when we're notified that a contact's status has changed.
1486 @param userHandle: the contact's user handle (passport)
1487 @param statusCode: 3-letter status code
1488 @param screenName: the contact's screen name
1492 def contactPersonalChanged(self
, userHandle
, personal
):
1494 Called when a contact's personal message changes.
1496 @param userHandle: the contact who changed their personal message
1497 @param personal : the new personal message
1501 def contactOffline(self
, userHandle
):
1503 Called when a contact goes offline.
1505 @param userHandle: the contact's user handle
1509 def gotMessage(self
, message
):
1511 Called when there is a message from the notification server
1512 that is not understood by default.
1514 @param message: the MSNMessage.
1518 def gotMSNAlert(self
, body
, action
, subscr
):
1520 Called when the server sends an MSN Alert (http://alerts.msn.com)
1522 @param body : the alert text
1523 @param action: a URL with more information for the user to view
1524 @param subscr: a URL the user can use to modify their alert subscription
1528 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1530 Called when the server sends you details about your hotmail
1531 inbox. This is only ever called once, on login.
1533 @param inboxunread : the number of unread items in your inbox
1534 @param foldersunread: the number of unread items in other folders
1538 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1540 Called when the server sends us realtime email
1541 notification. This means that you have received
1542 a new email in your hotmail inbox.
1544 @param mailfrom: the sender of the email
1545 @param fromaddr: the sender of the email (I don't know :P)
1546 @param subject : the email subject
1550 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1552 Called when the server sends us phone details about
1553 a specific user (for example after a user is added
1554 the server will send their status, phone details etc.
1556 @param userHandle: the contact's user handle (passport)
1557 @param phoneType: the specific phoneType
1558 (*_PHONE constants or HAS_PAGER)
1559 @param number: the value/phone number.
1563 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1565 Called when a user adds me to their list. (ie. they have been added to
1568 @param userHandle: the userHandle of the user
1569 @param screenName: the screen name of the user
1573 def userRemovedMe(self
, userHandle
):
1575 Called when a user removes us from their contact list
1576 (they are no longer on our reverseContacts list.
1578 @param userHandle: the contact's user handle (passport)
1582 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1583 key
, userHandle
, screenName
):
1585 Called when we get an invitation to a switchboard server.
1586 This happens when a user requests a chat session with us.
1588 @param sessionID: session ID number, must be remembered for logging in
1589 @param host: the hostname of the switchboard server
1590 @param port: the port to connect to
1591 @param key: used for authorization when connecting
1592 @param userHandle: the user handle of the person who invited us
1593 @param screenName: the screen name of the person who invited us
1597 def multipleLogin(self
):
1599 Called when the server says there has been another login
1600 under our account, the server should disconnect us right away.
1604 def serverGoingDown(self
):
1606 Called when the server has notified us that it is going down for
1613 def changeStatus(self
, status
):
1615 Change my current status. This method will add
1616 a default callback to the returned Deferred
1617 which will update the status attribute of the
1620 @param status: 3-letter status code (as defined by
1621 the STATUS_* constants)
1622 @return: A Deferred, the callback of which will be
1623 fired when the server confirms the change
1624 of status. The callback argument will be
1625 a tuple with the new status code as the
1629 id, d
= self
._createIDMapping
()
1630 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1632 self
.factory
.status
= r
[0]
1634 return d
.addCallback(_cb
)
1636 def setPrivacyMode(self
, privLevel
):
1638 Set my privacy mode on the server.
1641 This only keeps the current privacy setting on
1642 the server for later retrieval, it does not
1643 effect the way the server works at all.
1645 @param privLevel: This parameter can be true, in which
1646 case the server will keep the state as
1647 'al' which the official client interprets
1648 as -> allow messages from only users on
1649 the allow list. Alternatively it can be
1650 false, in which case the server will keep
1651 the state as 'bl' which the official client
1652 interprets as -> allow messages from all
1653 users except those on the block list.
1655 @return: A Deferred, the callback of which will be fired when
1656 the server replies with the new privacy setting.
1657 The callback argument will be a tuple, the only element
1658 of which being either 'al' or 'bl' (the new privacy setting).
1661 id, d
= self
._createIDMapping
()
1662 if privLevel
: self
.sendLine("BLP %s AL" % id)
1663 else: self
.sendLine("BLP %s BL" % id)
1668 Used for keeping an up-to-date contact list.
1669 A callback is added to the returned Deferred
1670 that updates the contact list on the factory
1671 and also sets my state to STATUS_ONLINE.
1674 This is called automatically upon signing
1675 in using the version attribute of
1676 factory.contacts, so you may want to persist
1677 this object accordingly. Because of this there
1678 is no real need to ever call this method
1681 @return: A Deferred, the callback of which will be
1682 fired when the server sends an adequate reply.
1683 The callback argument will be a tuple with two
1684 elements, the new list (MSNContactList) and
1685 your current state (a dictionary). If the version
1686 you sent _was_ the latest list version, both elements
1687 will be None. To just request the list send a version of 0.
1690 self
._setState
('SYNC')
1691 id, d
= self
._createIDMapping
(data
=None)
1692 self
._setStateData
('synid',id)
1693 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1695 self
.changeStatus(STATUS_ONLINE
)
1696 if r
[0] is not None:
1697 self
.factory
.contacts
= r
[0]
1699 return d
.addCallback(_cb
)
1701 def setPhoneDetails(self
, phoneType
, value
):
1703 Set/change my phone numbers stored on the server.
1705 @param phoneType: phoneType can be one of the following
1706 constants - HOME_PHONE, WORK_PHONE,
1707 MOBILE_PHONE, HAS_PAGER.
1708 These are pretty self-explanatory, except
1709 maybe HAS_PAGER which refers to whether or
1710 not you have a pager.
1711 @param value: for all of the *_PHONE constants the value is a
1712 phone number (str), for HAS_PAGER accepted values
1713 are 'Y' (for yes) and 'N' (for no).
1715 @return: A Deferred, the callback for which will be fired when
1716 the server confirms the change has been made. The
1717 callback argument will be a tuple with 2 elements, the
1718 first being the new list version (int) and the second
1719 being the new phone number value (str).
1721 raise "ProbablyDoesntWork"
1722 # XXX: Add a default callback which updates
1723 # factory.contacts.version and the relevant phone
1725 id, d
= self
._createIDMapping
()
1726 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1729 def addListGroup(self
, name
):
1731 Used to create a new list group.
1732 A default callback is added to the
1733 returned Deferred which updates the
1734 contacts attribute of the factory.
1736 @param name: The desired name of the new group.
1738 @return: A Deferred, the callbacck for which will be called
1739 when the server clarifies that the new group has been
1740 created. The callback argument will be a tuple with 3
1741 elements: the new list version (int), the new group name
1742 (str) and the new group ID (int).
1745 raise "ProbablyDoesntWork"
1746 id, d
= self
._createIDMapping
()
1747 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1749 if self
.factory
.contacts
:
1750 self
.factory
.contacts
.version
= r
[0]
1751 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1753 return d
.addCallback(_cb
)
1755 def remListGroup(self
, groupID
):
1757 Used to remove a list group.
1758 A default callback is added to the
1759 returned Deferred which updates the
1760 contacts attribute of the factory.
1762 @param groupID: the ID of the desired group to be removed.
1764 @return: A Deferred, the callback for which will be called when
1765 the server clarifies the deletion of the group.
1766 The callback argument will be a tuple with 2 elements:
1767 the new list version (int) and the group ID (int) of
1771 raise "ProbablyDoesntWork"
1772 id, d
= self
._createIDMapping
()
1773 self
.sendLine("RMG %s %s" % (id, groupID
))
1775 self
.factory
.contacts
.version
= r
[0]
1776 self
.factory
.contacts
.remGroup(r
[1])
1778 return d
.addCallback(_cb
)
1780 def renameListGroup(self
, groupID
, newName
):
1782 Used to rename an existing list group.
1783 A default callback is added to the returned
1784 Deferred which updates the contacts attribute
1787 @param groupID: the ID of the desired group to rename.
1788 @param newName: the desired new name for the group.
1790 @return: A Deferred, the callback for which will be called
1791 when the server clarifies the renaming.
1792 The callback argument will be a tuple of 3 elements,
1793 the new list version (int), the group id (int) and
1794 the new group name (str).
1797 raise "ProbablyDoesntWork"
1798 id, d
= self
._createIDMapping
()
1799 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1801 self
.factory
.contacts
.version
= r
[0]
1802 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1804 return d
.addCallback(_cb
)
1806 def addContact(self
, listType
, userHandle
):
1808 Used to add a contact to the desired list.
1809 A default callback is added to the returned
1810 Deferred which updates the contacts attribute of
1811 the factory with the new contact information.
1812 If you are adding a contact to the forward list
1813 and you want to associate this contact with multiple
1814 groups then you will need to call this method for each
1815 group you would like to add them to, changing the groupID
1816 parameter. The default callback will take care of updating
1817 the group information on the factory's contact list.
1819 @param listType: (as defined by the *_LIST constants)
1820 @param userHandle: the user handle (passport) of the contact
1823 @return: A Deferred, the callback for which will be called when
1824 the server has clarified that the user has been added.
1825 The callback argument will be a tuple with 4 elements:
1826 the list type, the contact's user handle, the new list
1827 version, and the group id (if relevant, otherwise it
1831 id, d
= self
._createIDMapping
()
1832 try: # Make sure the contact isn't actually on the list
1833 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1834 except AttributeError: pass
1835 listType
= listIDToCode
[listType
].upper()
1836 if listType
== "FL":
1837 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1839 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1842 if not self
.factory
: return
1843 c
= self
.factory
.contacts
.getContact(r
[2])
1845 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1846 self
.factory
.contacts
.addContact(c
)
1847 #if r[3]: c.groups.append(r[3])
1850 return d
.addCallback(_cb
)
1852 def remContact(self
, listType
, userHandle
):
1854 Used to remove a contact from the desired list.
1855 A default callback is added to the returned deferred
1856 which updates the contacts attribute of the factory
1857 to reflect the new contact information.
1859 @param listType: (as defined by the *_LIST constants)
1860 @param userHandle: the user handle (passport) of the
1861 contact being removed
1863 @return: A Deferred, the callback for which will be called when
1864 the server has clarified that the user has been removed.
1865 The callback argument will be a tuple of 3 elements:
1866 the list type, the contact's user handle and the group ID
1867 (if relevant, otherwise it will be None)
1870 id, d
= self
._createIDMapping
()
1871 try: # Make sure the contact is actually on this list
1872 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1873 except AttributeError: return
1874 listType
= listIDToCode
[listType
].upper()
1875 if listType
== "FL":
1877 c
= self
.factory
.contacts
.getContact(userHandle
)
1878 userGuid
= c
.userGuid
1879 except AttributeError: return
1880 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1882 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1885 if listType
== "FL":
1886 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1887 l
= self
.factory
.contacts
1888 c
= l
.getContact(r
[1])
1892 if group
: # they may not have been removed from the list
1893 c
.groups
.remove(group
)
1894 if c
.groups
: shouldRemove
= 0
1896 c
.removeFromList(r
[0])
1897 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1899 return d
.addCallback(_cb
)
1901 def changeScreenName(self
, newName
):
1903 Used to change your current screen name.
1904 A default callback is added to the returned
1905 Deferred which updates the screenName attribute
1906 of the factory and also updates the contact list
1909 @param newName: the new screen name
1911 @return: A Deferred, the callback for which will be called
1912 when the server acknowledges the change.
1913 The callback argument will be an empty tuple.
1916 id, d
= self
._createIDMapping
()
1917 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1919 self
.factory
.screenName
= newName
1921 return d
.addCallback(_cb
)
1923 def changePersonalMessage(self
, personal
):
1925 Used to change your personal message.
1927 @param personal: the new screen name
1929 @return: A Deferred, the callback for which will be called
1930 when the server acknowledges the change.
1931 The callback argument will be a tuple of 1 element,
1932 the personal message.
1935 id, d
= self
._createIDMapping
()
1938 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1939 self
.sendLine("UUX %s %s" % (id, len(data
)))
1940 self
.transport
.write(data
)
1942 self
.factory
.personal
= personal
1944 return d
.addCallback(_cb
)
1946 def changeAvatar(self
, imageData
, push
):
1948 Used to change the avatar that other users see.
1950 @param imageData: the PNG image data to set as the avatar
1951 @param push : whether to push the update to the server
1952 (it will otherwise be sent with the next
1955 @return: If push==True, a Deferred, the callback for which
1956 will be called when the server acknowledges the change.
1957 The callback argument will be the same as for changeStatus.
1960 if self
.msnobj
and imageData
== self
.msnobj
.imageData
: return
1962 self
.msnobj
.setData(self
.factory
.userHandle
, imageData
)
1964 self
.msnobj
.setNull()
1965 if push
: return self
.changeStatus(self
.factory
.status
) # Push to server
1968 def requestSwitchboardServer(self
):
1970 Used to request a switchboard server to use for conversations.
1972 @return: A Deferred, the callback for which will be called when
1973 the server responds with the switchboard information.
1974 The callback argument will be a tuple with 3 elements:
1975 the host of the switchboard server, the port and a key
1976 used for logging in.
1979 id, d
= self
._createIDMapping
()
1980 self
.sendLine("XFR %s SB" % id)
1985 Used to log out of the notification server.
1986 After running the method the server is expected
1987 to close the connection.
1990 if self
.pingCheckTask
:
1991 self
.pingCheckTask
.stop()
1992 self
.pingCheckTask
= None
1993 self
.factory
.stopTrying()
1994 self
.sendLine("OUT")
1995 self
.transport
.loseConnection()
1997 class NotificationFactory(ReconnectingClientFactory
):
1999 Factory for the NotificationClient protocol.
2000 This is basically responsible for keeping
2001 the state of the client and thus should be used
2002 in a 1:1 situation with clients.
2004 @ivar contacts: An MSNContactList instance reflecting
2005 the current contact list -- this is
2006 generally kept up to date by the default
2008 @ivar userHandle: The client's userHandle, this is expected
2009 to be set by the client and is used by the
2010 protocol (for logging in etc).
2011 @ivar screenName: The client's current screen-name -- this is
2012 generally kept up to date by the default
2014 @ivar password: The client's password -- this is (obviously)
2015 expected to be set by the client.
2016 @ivar passportServer: This must point to an msn passport server
2017 (the whole URL is required)
2018 @ivar status: The status of the client -- this is generally kept
2019 up to date by the default command handlers
2020 @ivar maxRetries: The number of times the factory will reconnect
2021 if the connection dies because of a network error.
2028 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
2030 protocol
= NotificationClient
2034 class SwitchboardClient(MSNEventBase
):
2036 This class provides support for clients connecting to a switchboard server.
2038 Switchboard servers are used for conversations with other people
2039 on the MSN network. This means that the number of conversations at
2040 any given time will be directly proportional to the number of
2041 connections to varioius switchboard servers.
2043 MSN makes no distinction between single and group conversations,
2044 so any number of users may be invited to join a specific conversation
2045 taking place on a switchboard server.
2047 @ivar key: authorization key, obtained when receiving
2048 invitation / requesting switchboard server.
2049 @ivar userHandle: your user handle (passport)
2050 @ivar sessionID: unique session ID, used if you are replying
2051 to a switchboard invitation
2052 @ivar reply: set this to 1 in connectionMade or before to signifiy
2053 that you are replying to a switchboard invitation.
2054 @ivar msnobj: the MSNObject for the user's avatar. So that the
2055 switchboard can distribute it to anyone who asks.
2067 MSNEventBase
.__init
__(self
)
2068 self
.pendingUsers
= {}
2069 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
2072 def connectionMade(self
):
2073 MSNEventBase
.connectionMade(self
)
2076 def connectionLost(self
, reason
):
2077 self
.cookies
['iCookies'] = {}
2078 MSNEventBase
.connectionLost(self
, reason
)
2080 def _sendInit(self
):
2082 send initial data based on whether we are replying to an invitation
2085 id = self
._nextTransactionID
()
2087 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
2089 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
2091 def _newInvitationCookie(self
):
2093 if self
._iCookie
> 1000: self
._iCookie
= 1
2094 return self
._iCookie
2096 def _checkTyping(self
, message
, cTypes
):
2097 """ helper method for checkMessage """
2098 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
2099 self
.gotContactTyping(message
)
2102 def _checkFileInvitation(self
, message
, info
):
2103 """ helper method for checkMessage """
2104 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
2106 cookie
= info
['Invitation-Cookie']
2107 filename
= info
['Application-File']
2108 filesize
= int(info
['Application-FileSize'])
2109 connectivity
= (info
.get('Connectivity', 'n').lower() == 'y')
2111 log
.msg('Received munged file transfer request ... ignoring.')
2113 raise NotImplementedError
2114 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
2117 def _handleP2PMessage(self
, message
):
2118 """ helper method for msnslp messages (file transfer & avatars) """
2119 if not message
.getHeader("P2P-Dest") == self
.userHandle
: return
2120 packet
= message
.message
2121 binaryFields
= BinaryFields(packet
=packet
)
2122 if binaryFields
[5] == BinaryFields
.BYEGOT
:
2123 pass # Ignore the ACKs to SLP messages
2124 elif binaryFields
[0] != 0:
2125 slpLink
= self
.slpLinks
.get(binaryFields
[0])
2127 # Link has been killed. Ignore
2129 if slpLink
.remoteUser
== message
.userHandle
:
2130 slpLink
.handlePacket(packet
)
2131 elif binaryFields
[5] == BinaryFields
.ACK
:
2132 pass # Ignore the ACKs to SLP messages
2134 slpMessage
= MSNSLPMessage(packet
)
2136 # Always try and give a slpMessage to a slpLink first.
2137 # If none can be found, and it was INVITE, then create
2138 # one to handle the session.
2139 for slpLink
in self
.slpLinks
.values():
2140 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
2141 slpLink
.handleSLPMessage(slpMessage
)
2144 slpLink
= None # Was not handled
2146 if not slpLink
and slpMessage
.method
== "INVITE":
2147 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
2148 context
= FileContext(slpMessage
.context
)
2149 slpLink
= SLPLink_FileReceive(remoteUser
=slpMessage
.fro
, switchboard
=self
, filename
=context
.filename
, filesize
=context
.filesize
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
, branch
=slpMessage
.branch
)
2150 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2151 self
.gotFileReceive(slpLink
)
2152 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
2153 # Check that we have an avatar to send
2155 slpLink
= SLPLink_AvatarSend(remoteUser
=slpMessage
.fro
, switchboard
=self
, filesize
=self
.msnobj
.size
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2156 slpLink
.write(self
.msnobj
.imageData
)
2159 # They shouldn't have sent a request if we have
2160 # no avatar. So we'll just ignore them.
2161 # FIXME We should really send an error
2164 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2166 # Always need to ACK these packets if we can
2167 slpLink
.sendP2PACK(binaryFields
)
2170 def checkMessage(self
, message
):
2172 hook for detecting any notification type messages
2173 (e.g. file transfer)
2175 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2176 if self
._checkTyping
(message
, cTypes
): return 0
2177 # if 'text/x-msmsgsinvite' in cTypes:
2178 # header like info is sent as part of the message body.
2180 # for line in message.message.split('\r\n'):
2182 # key, val = line.split(':')
2183 # info[key] = val.lstrip()
2184 # except ValueError: continue
2185 # if self._checkFileInvitation(message, info): return 0
2186 elif 'application/x-msnmsgrp2p' in cTypes
:
2187 self
._handleP
2PMessage
(message
)
2192 def handle_USR(self
, params
):
2193 checkParamLen(len(params
), 4, 'USR')
2194 if params
[1] == "OK":
2198 def handle_CAL(self
, params
):
2199 checkParamLen(len(params
), 3, 'CAL')
2201 if params
[1].upper() == "RINGING":
2202 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2205 def handle_JOI(self
, params
):
2206 checkParamLen(len(params
), 2, 'JOI')
2207 self
.userJoined(params
[0], unquote(params
[1]))
2209 # users participating in the current chat
2210 def handle_IRO(self
, params
):
2211 checkParamLen(len(params
), 5, 'IRO')
2212 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2213 if params
[1] == params
[2]:
2214 self
.gotChattingUsers(self
.pendingUsers
)
2215 self
.pendingUsers
= {}
2217 # finished listing users
2218 def handle_ANS(self
, params
):
2219 checkParamLen(len(params
), 2, 'ANS')
2220 if params
[1] == "OK":
2223 def handle_ACK(self
, params
):
2224 checkParamLen(len(params
), 1, 'ACK')
2225 self
._fireCallback
(int(params
[0]), None)
2227 def handle_NAK(self
, params
):
2228 checkParamLen(len(params
), 1, 'NAK')
2229 self
._fireCallback
(int(params
[0]), None)
2231 def handle_BYE(self
, params
):
2232 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2233 self
.userLeft(params
[0])
2239 called when all login details have been negotiated.
2240 Messages can now be sent, or new users invited.
2244 def gotChattingUsers(self
, users
):
2246 called after connecting to an existing chat session.
2248 @param users: A dict mapping user handles to screen names
2249 (current users taking part in the conversation)
2253 def userJoined(self
, userHandle
, screenName
):
2255 called when a user has joined the conversation.
2257 @param userHandle: the user handle (passport) of the user
2258 @param screenName: the screen name of the user
2262 def userLeft(self
, userHandle
):
2264 called when a user has left the conversation.
2266 @param userHandle: the user handle (passport) of the user.
2270 def gotMessage(self
, message
):
2272 called when we receive a message.
2274 @param message: the associated MSNMessage object
2278 def gotFileReceive(self
, fileReceive
):
2280 called when we receive a file send request from a contact.
2281 Default action is to reject the file.
2283 @param fileReceive: msnft.MSNFTReceive_Base instance
2285 fileReceive
.reject()
2288 def gotSendRequest(self
, fileReceive
):
2290 called when we receive a file send request from a contact
2292 @param fileReceive: msnft.MSNFTReceive_Base instance
2296 def gotContactTyping(self
, message
):
2298 called when we receive the special type of message notifying
2299 us that a contact is typing a message.
2301 @param message: the associated MSNMessage object
2307 def inviteUser(self
, userHandle
):
2309 used to invite a user to the current switchboard server.
2311 @param userHandle: the user handle (passport) of the desired user.
2313 @return: A Deferred, the callback for which will be called
2314 when the server notifies us that the user has indeed
2315 been invited. The callback argument will be a tuple
2316 with 1 element, the sessionID given to the invited user.
2317 I'm not sure if this is useful or not.
2320 id, d
= self
._createIDMapping
()
2321 self
.sendLine("CAL %s %s" % (id, userHandle
))
2324 def sendMessage(self
, message
):
2326 used to send a message.
2328 @param message: the corresponding MSNMessage object.
2330 @return: Depending on the value of message.ack.
2331 If set to MSNMessage.MESSAGE_ACK or
2332 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2333 the callback for which will be fired when an ACK or
2334 NACK is received - the callback argument will be
2335 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2336 the return value is None.
2339 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2340 else: id, d
= self
._createIDMapping
()
2341 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2342 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2343 # Apparently order matters with these
2344 orderMatters
= ("MIME-Version", "Content-Type", "Message-ID")
2345 for header
in orderMatters
:
2346 if message
.hasHeader(header
):
2347 self
.sendLine("%s: %s" % (header
, message
.getHeader(header
)))
2348 # send the rest of the headers
2349 for header
in [h
for h
in message
.headers
.items() if h
[0] not in orderMatters
]:
2350 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2351 self
.transport
.write("\r\n")
2352 self
.transport
.write(message
.message
)
2353 if MESSAGEDEBUG
: log
.msg(message
.message
)
2356 def sendAvatarRequest(self
, msnContact
):
2358 used to request an avatar from a user in this switchboard
2361 @param msnContact: the msnContact object to request an avatar for
2363 @return: A Deferred, the callback for which will be called
2364 when the avatar transfer succeeds.
2365 The callback argument will be a tuple with one element,
2366 the PNG avatar data.
2368 if not msnContact
.msnobj
: return
2370 def bufferClosed(data
):
2372 buffer = StringBuffer(bufferClosed
)
2373 buffer.error
= lambda: None
2374 slpLink
= SLPLink_AvatarReceive(remoteUser
=msnContact
.userHandle
, switchboard
=self
, consumer
=buffer, context
=msnContact
.msnobj
.text
)
2375 self
.slpLinks
[slpLink
.sessionID
] = slpLink
2378 def sendFile(self
, msnContact
, filename
, filesize
):
2380 used to send a file to a contact.
2382 @param msnContact: the MSNContact object to send a file to.
2383 @param filename: the name of the file to send.
2384 @param filesize: the size of the file to send.
2386 @return: (fileSend, d) A FileSend object and a Deferred.
2387 The Deferred will pass one argument in a tuple,
2388 whether or not the transfer is accepted. If you
2389 receive a True, then you can call write() on the
2390 fileSend object to send your file. Call close()
2391 when the file is done.
2392 NOTE: You MUST write() exactly as much as you
2393 declare in filesize.
2395 if not msnContact
.userHandle
: return
2396 # FIXME, check msnContact.caps to see if we should use old-style
2397 fileSend
= SLPLink_FileSend(remoteUser
=msnContact
.userHandle
, switchboard
=self
, filename
=filename
, filesize
=filesize
)
2398 self
.slpLinks
[fileSend
.sessionID
] = fileSend
2399 return fileSend
, fileSend
.acceptDeferred
2401 def sendTypingNotification(self
):
2403 Used to send a typing notification. Upon receiving this
2404 message the official client will display a 'user is typing'
2405 message to all other users in the chat session for 10 seconds.
2406 You should send one of these every 5 seconds as long as the
2410 m
.ack
= m
.MESSAGE_ACK_NONE
2411 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2412 m
.setHeader('TypingUser', self
.userHandle
)
2416 def sendFileInvitation(self
, fileName
, fileSize
):
2418 send an notification that we want to send a file.
2420 @param fileName: the file name
2421 @param fileSize: the file size
2423 @return: A Deferred, the callback of which will be fired
2424 when the user responds to this invitation with an
2425 appropriate message. The callback argument will be
2426 a tuple with 3 elements, the first being 1 or 0
2427 depending on whether they accepted the transfer
2428 (1=yes, 0=no), the second being an invitation cookie
2429 to identify your follow-up responses and the third being
2430 the message 'info' which is a dict of information they
2431 sent in their reply (this doesn't really need to be used).
2432 If you wish to proceed with the transfer see the
2433 sendTransferInfo method.
2435 cookie
= self
._newInvitationCookie
()
2438 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2439 m
.message
+= 'Application-Name: File Transfer\r\n'
2440 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2441 m
.message
+= 'Invitation-Command: INVITE\r\n'
2442 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2443 m
.message
+= 'Application-File: %s\r\n' % fileName
2444 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2445 m
.ack
= m
.MESSAGE_ACK_NONE
2447 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2450 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2452 send information relating to a file transfer session.
2454 @param accept: whether or not to go ahead with the transfer
2456 @param iCookie: the invitation cookie of previous replies
2457 relating to this transfer
2458 @param authCookie: the authentication cookie obtained from
2459 an FileSend instance
2461 @param port: the port on which an FileSend protocol is listening.
2464 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2465 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2466 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2467 m
.message
+= 'IP-Address: %s\r\n' % ip
2468 m
.message
+= 'Port: %s\r\n' % port
2469 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2471 m
.ack
= m
.MESSAGE_NACK
2476 def __init__(self
, filename
, filesize
, userHandle
):
2477 self
.consumer
= None
2478 self
.finished
= False
2481 self
.filename
, self
.filesize
, self
.userHandle
= filename
, filesize
, userHandle
2484 raise NotImplementedError
2486 def accept(self
, consumer
):
2487 if self
.consumer
: raise "AlreadyAccepted"
2488 self
.consumer
= consumer
2489 for data
in self
.buffer:
2490 self
.consumer
.write(data
)
2493 self
.consumer
.close()
2495 self
.consumer
.error()
2497 def write(self
, data
):
2498 if self
.error
or self
.finished
:
2499 raise IOError, "Attempt to write in an invalid state"
2501 self
.consumer
.write(data
)
2503 self
.buffer.append(data
)
2506 self
.finished
= True
2508 self
.consumer
.close()
2511 """ Represents the Context field for P2P file transfers """
2512 def __init__(self
, data
=""):
2520 if MSNP2PDEBUG
: log
.msg("FileContext packing:", self
.filename
, self
.filesize
)
2521 data
= struct
.pack("<LLQL", 638, 0x03, self
.filesize
, 0x01)
2522 data
= data
[:-1] # Uck, weird, but it works
2523 data
+= utf16net(self
.filename
)
2524 data
= ljust(data
, 570, '\0')
2525 data
+= struct
.pack("<L", 0xFFFFFFFFL
)
2526 data
= ljust(data
, 638, '\0')
2529 def parse(self
, packet
):
2530 self
.filesize
= struct
.unpack("<Q", packet
[8:16])[0]
2531 chunk
= packet
[19:540]
2532 chunk
= chunk
[:chunk
.find('\x00\x00')]
2533 self
.filename
= unicode((codecs
.BOM_UTF16_BE
+ chunk
).decode("utf-16"))
2534 if MSNP2PDEBUG
: log
.msg("FileContext parsed:", self
.filesize
, self
.filename
)
2538 """ Utility class for the binary header & footer in p2p messages """
2547 def __init__(self
, fields
=None, packet
=None):
2549 self
.fields
= fields
2551 self
.fields
= [0] * 10
2553 self
.unpackFields(packet
)
2555 def __getitem__(self
, key
):
2556 return self
.fields
[key
]
2558 def __setitem__(self
, key
, value
):
2559 self
.fields
[key
] = value
2561 def unpackFields(self
, packet
):
2562 self
.fields
= struct
.unpack("<LLQQLLLLQ", packet
[0:48])
2563 self
.fields
+= struct
.unpack(">L", packet
[len(packet
)-4:])
2565 out
= "Unpacked fields: "
2566 for i
in self
.fields
:
2570 def packHeaders(self
):
2571 f
= tuple(self
.fields
)
2573 out
= "Packed fields: "
2574 for i
in self
.fields
:
2577 return struct
.pack("<LLQQLLLLQ", f
[0], f
[1], f
[2], f
[3], f
[4], f
[5], f
[6], f
[7], f
[8])
2579 def packFooter(self
):
2580 return struct
.pack(">L", self
.fields
[9])
2583 class MSNSLPMessage
:
2584 """ Representation of a single MSNSLP message """
2585 def __init__(self
, packet
=None):
2592 self
.sessionGuid
= ""
2593 self
.sessionID
= None
2595 self
.data
= "\r\n" + chr(0)
2599 def create(self
, method
=None, status
=None, to
=None, fro
=None, branch
=None, cseq
=0, sessionGuid
=None, data
=None):
2600 self
.method
= method
2601 self
.status
= status
2604 self
.branch
= branch
2606 self
.sessionGuid
= sessionGuid
2607 if data
: self
.data
= data
2609 def setData(self
, ctype
, data
):
2612 order
= ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
2614 if key
== "Context" and data
.has_key(key
):
2615 s
.append("Context: %s\r\n" % b64enc(data
[key
]))
2616 elif data
.has_key(key
):
2617 s
.append("%s: %s\r\n" % (key
, str(data
[key
])))
2618 s
.append("\r\n"+chr(0))
2620 self
.data
= "".join(s
)
2624 if s
.find("MSNSLP/1.0") < 0: return
2626 lines
= s
.split("\r\n")
2628 # Get the MSNSLP method or status
2629 msnslp
= lines
[0].split(" ")
2630 if MSNP2PDEBUG
: log
.msg("Parsing MSNSLPMessage %s %s" % (len(s
), s
))
2631 if msnslp
[0] in ("INVITE", "BYE"):
2632 self
.method
= msnslp
[0].strip()
2634 self
.status
= msnslp
[1].strip()
2636 lines
.remove(lines
[0])
2639 line
= line
.split(":")
2640 if len(line
) < 1: continue
2642 if len(line
) > 2 and line
[0] == "To":
2643 self
.to
= line
[2][:line
[2].find('>')]
2644 elif len(line
) > 2 and line
[0] == "From":
2645 self
.fro
= line
[2][:line
[2].find('>')]
2646 elif line
[0] == "Call-ID":
2647 self
.sessionGuid
= line
[1].strip()
2648 elif line
[0] == "CSeq":
2649 self
.cseq
= int(line
[1].strip())
2650 elif line
[0] == "SessionID":
2651 self
.sessionID
= int(line
[1].strip())
2652 elif line
[0] == "EUF-GUID":
2653 self
.euf_guid
= line
[1].strip()
2654 elif line
[0] == "Content-Type":
2655 self
.ctype
= line
[1].strip()
2656 elif line
[0] == "Context":
2657 self
.context
= b64dec(line
[1])
2658 elif line
[0] == "Via":
2659 self
.branch
= line
[1].split(";")[1].split("=")[1].strip()
2662 log
.msg("Error parsing MSNSLP message.")
2668 s
.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self
.method
, self
.to
))
2670 if self
.status
== "200": status
= "200 OK"
2671 elif self
.status
== "603": status
= "603 Decline"
2672 s
.append("MSNSLP/1.0 %s\r\n" % status
)
2673 s
.append("To: <msnmsgr:%s>\r\n" % self
.to
)
2674 s
.append("From: <msnmsgr:%s>\r\n" % self
.fro
)
2675 s
.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % self
.branch
)
2676 s
.append("CSeq: %s \r\n" % str(self
.cseq
))
2677 s
.append("Call-ID: %s\r\n" % self
.sessionGuid
)
2678 s
.append("Max-Forwards: 0\r\n")
2679 s
.append("Content-Type: %s\r\n" % self
.ctype
)
2680 s
.append("Content-Length: %s\r\n\r\n" % len(self
.data
))
2685 """ Utility for handling the weird sequence IDs in p2p messages """
2686 def __init__(self
, baseID
=None):
2688 self
.baseID
= baseID
2690 self
.baseID
= random
.randint(1000, sys
.maxint
)
2694 return p2pseq(self
.pos
) + self
.baseID
2701 class StringBuffer(StringIO
.StringIO
):
2702 def __init__(self
, notifyFunc
=None):
2703 self
.notifyFunc
= notifyFunc
2704 StringIO
.StringIO
.__init
__(self
)
2708 self
.notifyFunc(self
.getvalue())
2709 self
.notifyFunc
= None
2710 StringIO
.StringIO
.close(self
)
2714 def __init__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
):
2717 sessionID
= random
.randint(1000, sys
.maxint
)
2719 sessionGuid
= random_guid()
2720 self
.remoteUser
= remoteUser
2721 self
.switchboard
= switchboard
2722 self
.sessionID
= sessionID
2723 self
.sessionGuid
= sessionGuid
2724 self
.seqID
= SeqID()
2727 if MSNP2PDEBUG
: log
.msg("killLink")
2729 if MSNP2PDEBUG
: log
.msg("killLink - kill()")
2730 if not self
.switchboard
: return
2731 del self
.switchboard
.slpLinks
[self
.sessionID
]
2732 self
.switchboard
= None
2733 # This is so that handleP2PMessage can still use the SLPLink
2734 # one last time, for ACKing BYEs and 601s.
2735 reactor
.callLater(0, kill
)
2737 def warn(self
, text
):
2738 log
.msg("Warning in transfer: %s %s" % (self
, text
))
2740 def sendP2PACK(self
, ackHeaders
):
2741 binaryFields
= BinaryFields()
2742 binaryFields
[0] = ackHeaders
[0]
2743 binaryFields
[1] = self
.seqID
.next()
2744 binaryFields
[3] = ackHeaders
[3]
2745 binaryFields
[5] = BinaryFields
.ACK
2746 binaryFields
[6] = ackHeaders
[1]
2747 binaryFields
[7] = ackHeaders
[6]
2748 binaryFields
[8] = ackHeaders
[3]
2749 self
.sendP2PMessage(binaryFields
, "")
2751 def sendSLPMessage(self
, cmd
, ctype
, data
, branch
=None):
2752 msg
= MSNSLPMessage()
2754 msg
.create(status
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=branch
, cseq
=1, sessionGuid
=self
.sessionGuid
)
2756 msg
.create(method
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=random_guid(), cseq
=0, sessionGuid
=self
.sessionGuid
)
2757 msg
.setData(ctype
, data
)
2759 binaryFields
= BinaryFields()
2760 binaryFields
[1] = self
.seqID
.next()
2761 binaryFields
[3] = len(msgStr
)
2762 binaryFields
[4] = binaryFields
[3]
2763 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2764 self
.sendP2PMessage(binaryFields
, msgStr
)
2766 def sendP2PMessage(self
, binaryFields
, msgStr
):
2767 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2769 message
= MSNMessage(message
=packet
)
2770 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2771 message
.setHeader("P2P-Dest", self
.remoteUser
)
2772 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2773 self
.switchboard
.sendMessage(message
)
2775 def handleSLPMessage(self
, slpMessage
):
2776 raise NotImplementedError
2782 class SLPLink_Send(SLPLink
):
2783 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2784 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2785 self
.handlePacket
= None
2787 self
.filesize
= filesize
2790 def send_dataprep(self
):
2791 if MSNP2PDEBUG
: log
.msg("send_dataprep")
2792 binaryFields
= BinaryFields()
2793 binaryFields
[0] = self
.sessionID
2794 binaryFields
[1] = self
.seqID
.next()
2797 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2799 self
.sendP2PMessage(binaryFields
, chr(0) * 4)
2801 def write(self
, data
):
2802 if MSNP2PDEBUG
: log
.msg("write")
2804 data
= self
.data
+ data
2808 if i
+ 1202 < length
:
2809 self
._writeChunk
(data
[i
:i
+1202])
2812 self
.data
= data
[i
:]
2815 def _writeChunk(self
, chunk
):
2816 if MSNP2PDEBUG
: log
.msg("writing chunk")
2817 binaryFields
= BinaryFields()
2818 binaryFields
[0] = self
.sessionID
2819 if self
.offset
== 0:
2820 binaryFields
[1] = self
.seqID
.next()
2822 binaryFields
[1] = self
.seqID
.get()
2823 binaryFields
[2] = self
.offset
2824 binaryFields
[3] = self
.filesize
2825 binaryFields
[4] = len(chunk
)
2826 binaryFields
[5] = self
.dataFlag
2827 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2829 self
.offset
+= len(chunk
)
2830 self
.sendP2PMessage(binaryFields
, chunk
)
2834 self
._writeChunk
(self
.data
)
2839 # FIXME, should send 601 or something
2841 class SLPLink_FileSend(SLPLink_Send
):
2842 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
):
2843 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
)
2844 self
.dataFlag
= BinaryFields
.DATAFT
2845 # Send invite & wait for 200OK before sending dataprep
2846 context
= FileContext()
2847 context
.filename
= filename
2848 context
.filesize
= filesize
2849 data
= {"EUF-GUID" : MSN_MSNFTP_GUID
,\
2850 "SessionID": self
.sessionID
,\
2852 "Context" : context
.pack() }
2853 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2854 self
.acceptDeferred
= Deferred()
2856 def handleSLPMessage(self
, slpMessage
):
2857 if slpMessage
.status
== "200":
2858 if slpMessage
.ctype
== "application/x-msnmsgr-sessionreqbody":
2859 data
= {"Bridges" : "TRUDPv1 TCPv1",\
2861 "Conn-Type" : "Firewall",\
2862 "UPnPNat" : "false",\
2864 #"Hashed-Nonce": random_guid()}
2865 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data
)
2866 elif slpMessage
.ctype
== "application/x-msnmsgr-transrespbody":
2867 self
.acceptDeferred
.callback((True,))
2868 self
.handlePacket
= self
.wait_data_ack
2870 if slpMessage
.status
== "603":
2871 self
.acceptDeferred
.callback((False,))
2872 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to decline, error or BYE")
2876 def wait_data_ack(self
, packet
):
2877 if MSNP2PDEBUG
: log
.msg("wait_data_ack")
2878 binaryFields
= BinaryFields()
2879 binaryFields
.unpackFields(packet
)
2881 if binaryFields
[5] != BinaryFields
.ACK
:
2882 self
.warn("field5," + str(binaryFields
[5]))
2885 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2886 self
.handlePacket
= None
2889 self
.handlePacket
= self
.wait_data_ack
2890 SLPLink_Send
.close(self
)
2893 class SLPLink_AvatarSend(SLPLink_Send
):
2894 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2895 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2896 self
.dataFlag
= BinaryFields
.DATA
2897 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2898 self
.send_dataprep()
2899 self
.handlePacket
= lambda packet
: None
2901 def handleSLPMessage(self
, slpMessage
):
2902 if MSNP2PDEBUG
: log
.msg("BYE or error")
2906 SLPLink_Send
.close(self
)
2907 # Keep the link open to wait for a BYE
2909 class SLPLink_Receive(SLPLink
):
2910 def __init__(self
, remoteUser
, switchboard
, consumer
, context
=None, sessionID
=None, sessionGuid
=None):
2911 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2912 self
.handlePacket
= None
2913 self
.consumer
= consumer
2916 def wait_dataprep(self
, packet
):
2917 if MSNP2PDEBUG
: log
.msg("wait_dataprep")
2918 binaryFields
= BinaryFields()
2919 binaryFields
.unpackFields(packet
)
2921 if binaryFields
[3] != 4:
2922 self
.warn("field3," + str(binaryFields
[3]))
2924 if binaryFields
[4] != 4:
2925 self
.warn("field4," + str(binaryFields
[4]))
2927 # Just ignore the footer
2928 #if binaryFields[9] != 1:
2929 # self.warn("field9," + str(binaryFields[9]))
2932 self
.sendP2PACK(binaryFields
)
2933 self
.handlePacket
= self
.wait_data
2935 def wait_data(self
, packet
):
2936 if MSNP2PDEBUG
: log
.msg("wait_data")
2937 binaryFields
= BinaryFields()
2938 binaryFields
.unpackFields(packet
)
2940 if binaryFields
[5] != self
.dataFlag
:
2941 self
.warn("field5," + str(binaryFields
[5]))
2943 # Just ignore the footer
2944 #if binaryFields[9] != 1:
2945 # self.warn("field9," + str(binaryFields[9]))
2947 offset
= binaryFields
[2]
2948 total
= binaryFields
[3]
2949 length
= binaryFields
[4]
2951 data
= packet
[48:-4]
2952 if offset
!= self
.pos
:
2953 self
.warn("Received packet out of order")
2954 self
.consumer
.error()
2956 if len(data
) != length
:
2957 self
.warn("Received bad length of slp")
2958 self
.consumer
.error()
2963 self
.consumer
.write(str(data
))
2965 if self
.pos
== total
:
2966 self
.sendP2PACK(binaryFields
)
2967 self
.consumer
.close()
2968 self
.handlePacket
= None
2971 def doFinished(self
):
2972 raise NotImplementedError
2975 class SLPLink_FileReceive(SLPLink_Receive
, FileReceive
):
2976 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
, sessionID
, sessionGuid
, branch
):
2977 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=self
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2978 self
.dataFlag
= BinaryFields
.DATAFT
2979 self
.initialBranch
= branch
2980 FileReceive
.__init
__(self
, filename
, filesize
, remoteUser
)
2983 # Send a 603 decline
2984 if not self
.switchboard
: return
2985 self
.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2988 def accept(self
, consumer
):
2989 FileReceive
.accept(self
, consumer
)
2990 if not self
.switchboard
: return
2991 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2992 self
.handlePacket
= self
.wait_data
# Moved here because sometimes the second INVITE seems to be skipped
2994 def handleSLPMessage(self
, slpMessage
):
2995 if slpMessage
.method
== "INVITE": # The second invite
2996 data
= {"Bridge" : "TCPv1",\
2997 "Listening" : "false",\
2998 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
2999 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
3000 # self.handlePacket = self.wait_data # Moved up
3002 if MSNP2PDEBUG
: log
.msg("It's either a BYE or an error")
3004 # FIXME, do some error handling if it was an error
3006 def doFinished(self
):
3007 #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3009 # Wait for BYE? #FIXME
3012 class SLPLink_AvatarReceive(SLPLink_Receive
):
3013 def __init__(self
, remoteUser
, switchboard
, consumer
, context
):
3014 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=consumer
, context
=context
)
3015 self
.dataFlag
= BinaryFields
.DATA
3016 data
= {"EUF-GUID" : MSN_AVATAR_GUID
,\
3017 "SessionID": self
.sessionID
,\
3019 "Context" : context
}
3020 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
3021 self
.handlePacket
= self
.wait_dataprep
3023 def handleSLPMessage(self
, slpMessage
):
3024 if slpMessage
.method
== "INVITE": # The second invite
3025 data
= {"Bridge" : "TCPv1",\
3026 "Listening" : "false",\
3027 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
3028 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
3029 elif slpMessage
.status
== "200":
3031 #self.handlePacket = self.wait_dataprep # Moved upwards
3033 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to error or BYE")
3036 def doFinished(self
):
3037 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3039 # mapping of error codes to error messages
3042 200 : "Syntax error",
3043 201 : "Invalid parameter",
3044 205 : "Invalid user",
3045 206 : "Domain name missing",
3046 207 : "Already logged in",
3047 208 : "Invalid username",
3048 209 : "Invalid screen name",
3049 210 : "User list full",
3050 215 : "User already there",
3051 216 : "User already on list",
3052 217 : "User not online",
3053 218 : "Already in mode",
3054 219 : "User is in the opposite list",
3055 223 : "Too many groups",
3056 224 : "Invalid group",
3057 225 : "User not in group",
3058 229 : "Group name too long",
3059 230 : "Cannot remove group 0",
3060 231 : "Invalid group",
3061 280 : "Switchboard failed",
3062 281 : "Transfer to switchboard failed",
3064 300 : "Required field missing",
3065 301 : "Too many FND responses",
3066 302 : "Not logged in",
3068 400 : "Message not allowed",
3069 402 : "Error accessing contact list",
3070 403 : "Error accessing contact list",
3072 500 : "Internal server error",
3073 501 : "Database server error",
3074 502 : "Command disabled",
3075 510 : "File operation failed",
3076 520 : "Memory allocation failed",
3077 540 : "Wrong CHL value sent to server",
3079 600 : "Server is busy",
3080 601 : "Server is unavaliable",
3081 602 : "Peer nameserver is down",
3082 603 : "Database connection failed",
3083 604 : "Server is going down",
3084 605 : "Server unavailable",
3086 707 : "Could not create connection",
3087 710 : "Invalid CVR parameters",
3088 711 : "Write is blocking",
3089 712 : "Session is overloaded",
3090 713 : "Too many active users",
3091 714 : "Too many sessions",
3092 715 : "Not expected",
3093 717 : "Bad friend file",
3094 731 : "Not expected",
3096 800 : "Requests too rapid",
3098 910 : "Server too busy",
3099 911 : "Authentication failed",
3100 912 : "Server too busy",
3101 913 : "Not allowed when offline",
3102 914 : "Server too busy",
3103 915 : "Server too busy",
3104 916 : "Server too busy",
3105 917 : "Server too busy",
3106 918 : "Server too busy",
3107 919 : "Server too busy",
3108 920 : "Not accepting new users",
3109 921 : "Server too busy",
3110 922 : "Server too busy",
3111 923 : "No parent consent",
3112 924 : "Passport account not yet verified"
3116 # mapping of status codes to readable status format
3119 STATUS_ONLINE
: "Online",
3120 STATUS_OFFLINE
: "Offline",
3121 STATUS_HIDDEN
: "Appear Offline",
3122 STATUS_IDLE
: "Idle",
3123 STATUS_AWAY
: "Away",
3124 STATUS_BUSY
: "Busy",
3125 STATUS_BRB
: "Be Right Back",
3126 STATUS_PHONE
: "On the Phone",
3127 STATUS_LUNCH
: "Out to Lunch"
3131 # mapping of list ids to list codes
3134 FORWARD_LIST
: 'fl',
3137 REVERSE_LIST
: 'rl',
3142 # mapping of list codes to list ids
3144 for id,code
in listIDToCode
.items():
3145 listCodeToID
[code
] = id