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
89 from twisted
.web
.http
import HTTPClient
93 from twisted
.internet
import reactor
, task
94 from twisted
.internet
.defer
import Deferred
95 from twisted
.internet
.protocol
import ReconnectingClientFactory
, ClientFactory
97 from twisted
.internet
.ssl
import ClientContextFactory
99 print "You must install pycrypto and pyopenssl."
101 from twisted
.python
import failure
, log
102 from twisted
.words
.xish
.domish
import parseText
, unescapeFromXml
106 import types
, operator
, os
, sys
, base64
, random
, struct
, random
, sha
, base64
, StringIO
, array
, codecs
, binascii
107 from urllib
import quote
, unquote
110 MSN_PROTOCOL_VERSION
= "MSNP11 CVR0" # protocol version
111 MSN_PORT
= 1863 # default dispatch server port
112 MSN_MAX_MESSAGE
= 1664 # max message length
113 MSN_CVR_STR
= "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
114 MSN_AVATAR_GUID
= "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
115 MSN_MSNFTP_GUID
= "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
137 STATUS_ONLINE
= 'NLN'
138 STATUS_OFFLINE
= 'FLN'
139 STATUS_HIDDEN
= 'HDN'
154 P2PSEQ
= [-3, -2, 0, -1, 1, 2, 3, 4, 5, 6, 7, 8]
163 return inp
.split('=')[1]
175 userHandle
= getVal(p
)
177 screenName
= unquote(getVal(p
))
182 else: # Must be the groups
184 groups
= p
.split(',')
186 raise MSNProtocolError
, "Unknown LST/ADC response" + str(params
) # debug
188 return userHandle
, screenName
, userGuid
, lists
, groups
191 """ Needed for Python 2.3 compatibility """
192 return s
+ (n
-len(s
))*c
194 if sys
.byteorder
== "little":
196 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
197 a
= array
.array("h", s
.encode("utf-16")[2:])
202 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
203 return s
.encode("utf-16")[2:]
206 return base64
.encodestring(s
).replace("\n", "")
209 for pad
in ["", "=", "==", "A", "A=", "A=="]: # Stupid MSN client!
211 return base64
.decodestring(s
+ pad
)
214 raise ValueError("Got some very bad base64!")
217 format
= "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
220 data
.append(random
.random() * 0xAAFF + 0x1111)
225 def checkParamLen(num
, expected
, cmd
, error
=None):
226 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
227 if num
!= expected
: raise MSNProtocolError
, error
229 def _parseHeader(h
, v
):
231 Split a certin number of known
232 header values with the format:
233 field1=val,field2=val,field3=val into
234 a dict mapping fields to values.
235 @param h: the header's key
236 @param v: the header's value as a string
239 if h
in ('passporturls','authentication-info','www-authenticate'):
240 v
= v
.replace('Passport1.4','').lstrip()
242 for fieldPair
in v
.split(','):
244 field
,value
= fieldPair
.split('=',1)
245 fields
[field
.lower()] = value
247 fields
[field
.lower()] = ''
251 def _parsePrimitiveHost(host
):
253 h
,p
= host
.replace('https://','').split('/',1)
257 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
=''):
259 This function is used internally and should not ever be called
263 def _cb(server
, auth
):
264 loginFac
= ClientFactory()
265 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
266 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
269 _cb(nexusServer
, authData
)
271 fac
= ClientFactory()
273 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
274 d
.addErrback(lambda f
: cb
.errback(f
))
275 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
276 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
280 class PassportNexus(HTTPClient
):
283 Used to obtain the URL of a valid passport
286 This class is used internally and should
287 not be instantiated directly -- that is,
288 The passport logging in process is handled
289 transparantly by NotificationClient.
292 def __init__(self
, deferred
, host
):
293 self
.deferred
= deferred
294 self
.host
, self
.path
= _parsePrimitiveHost(host
)
296 def connectionMade(self
):
297 HTTPClient
.connectionMade(self
)
298 self
.sendCommand('GET', self
.path
)
299 self
.sendHeader('Host', self
.host
)
303 def handleHeader(self
, header
, value
):
305 self
.headers
[h
] = _parseHeader(h
, value
)
307 def handleEndHeaders(self
):
308 if self
.connected
: self
.transport
.loseConnection()
309 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
310 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
312 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
314 def handleResponse(self
, r
): pass
316 class PassportLogin(HTTPClient
):
318 This class is used internally to obtain
319 a login ticket from a passport HTTPS
320 server -- it should not be used directly.
325 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
326 self
.deferred
= deferred
327 self
.userHandle
= userHandle
329 self
.authData
= authData
330 self
.host
, self
.path
= _parsePrimitiveHost(host
)
332 def connectionMade(self
):
333 self
.sendCommand('GET', self
.path
)
334 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
335 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), quote(self
.passwd
), self
.authData
))
336 self
.sendHeader('Host', self
.host
)
340 def handleHeader(self
, header
, value
):
342 self
.headers
[h
] = _parseHeader(h
, value
)
344 def handleEndHeaders(self
):
345 if self
._finished
: return
346 self
._finished
= 1 # I think we need this because of HTTPClient
347 if self
.connected
: self
.transport
.loseConnection()
348 authHeader
= 'authentication-info'
349 _interHeader
= 'www-authenticate'
350 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
352 info
= self
.headers
[authHeader
]
353 status
= info
['da-status']
354 handler
= getattr(self
, 'login_%s' % (status
,), None)
357 else: raise Exception()
359 self
.deferred
.errback(failure
.Failure(e
))
361 def handleResponse(self
, r
): pass
363 def login_success(self
, info
):
364 ticket
= info
['from-pp']
365 ticket
= ticket
[1:len(ticket
)-1]
366 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
368 def login_failed(self
, info
):
369 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
371 def login_redir(self
, info
):
372 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
374 class MSNProtocolError(Exception):
376 This Exception is basically used for debugging
377 purposes, as the official MSN server should never
378 send anything _wrong_ and nobody in their right
379 mind would run their B{own} MSN server.
380 If it is raised by default command handlers
381 (handle_BLAH) the error will be logged.
388 I am the class used to represent an 'instant' message.
390 @ivar userHandle: The user handle (passport) of the sender
391 (this is only used when receiving a message)
392 @ivar screenName: The screen name of the sender (this is only used
393 when receiving a message)
394 @ivar message: The message
395 @ivar headers: The message headers
397 @ivar length: The message length (including headers and line endings)
398 @ivar ack: This variable is used to tell the server how to respond
399 once the message has been sent. If set to MESSAGE_ACK
400 (default) the server will respond with an ACK upon receiving
401 the message, if set to MESSAGE_NACK the server will respond
402 with a NACK upon failure to receive the message.
403 If set to MESSAGE_ACK_NONE the server will do nothing.
404 This is relevant for the return value of
405 SwitchboardClient.sendMessage (which will return
406 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
407 and will fire when the respective ACK or NACK is received).
408 If set to MESSAGE_ACK_NONE sendMessage will return None.
411 MESSAGE_ACK_FAT
= 'D'
413 MESSAGE_ACK_NONE
= 'U'
417 def __init__(self
, length
=0, userHandle
="", screenName
="", message
="", specialMessage
=False):
418 self
.userHandle
= userHandle
419 self
.screenName
= screenName
420 self
.specialMessage
= specialMessage
421 self
.message
= message
422 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain; charset=UTF-8'}
426 def _calcMessageLen(self
):
428 used to calculte the number to send
429 as the message length when sending a message.
431 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
433 def delHeader(self
, header
):
434 """ delete the desired header """
435 if self
.headers
.has_key(header
):
436 del self
.headers
[header
]
438 def setHeader(self
, header
, value
):
439 """ set the desired header """
440 self
.headers
[header
] = value
442 def getHeader(self
, header
):
444 get the desired header value
445 @raise KeyError: if no such header exists.
447 return self
.headers
[header
]
449 def hasHeader(self
, header
):
450 """ check to see if the desired header exists """
451 return self
.headers
.has_key(header
)
453 def getMessage(self
):
454 """ return the message - not including headers """
457 def setMessage(self
, message
):
458 """ set the message text """
459 self
.message
= message
464 Used to represent a MSNObject. This can be currently only be an avatar.
466 @ivar creator: The userHandle of the creator of this picture.
467 @ivar imageData: The PNG image data (only for our own avatar)
468 @ivar type: Always set to 3, for avatar.
469 @ivar size: The size of the image.
470 @ivar location: The filename of the image.
471 @ivar friendly: Unknown.
472 @ivar text: The textual representation of this MSNObject.
474 def __init__(self
, s
=""):
475 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
480 def setData(self
, creator
, imageData
):
481 """ Set the creator and imageData for this object """
482 self
.creator
= creator
483 self
.imageData
= imageData
484 self
.size
= len(imageData
)
486 self
.location
= "TMP" + str(random
.randint(1000,9999))
487 self
.friendly
= "AAA="
488 self
.sha1d
= b64enc(sha
.sha(imageData
).digest())
502 """ Makes a textual representation of this MSNObject. Stores it in self.text """
505 h
.append(self
.creator
)
507 h
.append(str(self
.size
))
509 h
.append(str(self
.type))
511 h
.append(self
.location
)
513 h
.append(self
.friendly
)
516 sha1c
= b64enc(sha
.sha("".join(h
)).digest())
517 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
)
520 e
= parseText(s
, True)
522 return # Parse failed
523 self
.creator
= e
.getAttribute("Creator")
524 self
.size
= int(e
.getAttribute("Size"))
525 self
.type = int(e
.getAttribute("Type"))
526 self
.location
= e
.getAttribute("Location")
527 self
.friendly
= e
.getAttribute("Friendly")
528 self
.sha1d
= e
.getAttribute("SHA1D")
535 This class represents a contact (user).
537 @ivar userGuid: The contact's user guid (unique string)
538 @ivar userHandle: The contact's user handle (passport).
539 @ivar screenName: The contact's screen name.
540 @ivar groups: A list of all the group IDs which this
542 @ivar lists: An integer representing the sum of all lists
543 that this contact belongs to.
544 @ivar caps: int, The capabilities of this client
545 @ivar msnobj: The MSNObject representing the contact's avatar
546 @ivar status: The contact's status code.
547 @type status: str if contact's status is known, None otherwise.
548 @ivar personal: The contact's personal message .
549 @type personal: str if contact's personal message is known, None otherwise.
551 @ivar homePhone: The contact's home phone number.
552 @type homePhone: str if known, otherwise None.
553 @ivar workPhone: The contact's work phone number.
554 @type workPhone: str if known, otherwise None.
555 @ivar mobilePhone: The contact's mobile phone number.
556 @type mobilePhone: str if known, otherwise None.
557 @ivar hasPager: Whether or not this user has a mobile pager
558 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
566 def __init__(self
, userGuid
="", userHandle
="", screenName
="", lists
=0, caps
=0, msnobj
=None, groups
={}, status
=None, personal
=""):
567 self
.userGuid
= userGuid
568 self
.userHandle
= userHandle
569 self
.screenName
= screenName
573 self
.msnobjGot
= True
574 self
.groups
= [] # if applicable
575 self
.status
= status
# current status
576 self
.personal
= personal
579 self
.homePhone
= None
580 self
.workPhone
= None
581 self
.mobilePhone
= None
585 def setPhone(self
, phoneType
, value
):
587 set phone numbers/values for this specific user.
588 for phoneType check the *_PHONE constants and HAS_PAGER
591 t
= phoneType
.upper()
592 if t
== HOME_PHONE
: self
.homePhone
= value
593 elif t
== WORK_PHONE
: self
.workPhone
= value
594 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
595 elif t
== HAS_PAGER
: self
.hasPager
= value
596 elif t
== HAS_BLOG
: self
.hasBlog
= value
597 #else: raise ValueError, "Invalid Phone Type: " + t
599 def addToList(self
, listType
):
601 Update the lists attribute to
602 reflect being part of the
605 self
.lists |
= listType
607 def removeFromList(self
, listType
):
609 Update the lists attribute to
610 reflect being removed from the
613 self
.lists ^
= listType
615 class MSNContactList
:
617 This class represents a basic MSN contact list.
619 @ivar contacts: All contacts on my various lists
620 @type contacts: dict (mapping user handles to MSNContact objects)
621 @ivar groups: a mapping of group ids to group names
622 (groups can only exist on the forward list)
626 This is used only for storage and doesn't effect the
627 server's contact list.
637 def _getContactsFromList(self
, listType
):
639 Obtain all contacts which belong
640 to the given list type.
642 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
644 def addContact(self
, contact
):
648 self
.contacts
[contact
.userHandle
] = contact
650 def remContact(self
, userHandle
):
655 del self
.contacts
[userHandle
]
656 except KeyError: pass
658 def getContact(self
, userHandle
):
660 Obtain the MSNContact object
661 associated with the given
663 @return: the MSNContact object if
664 the user exists, or None.
667 return self
.contacts
[userHandle
]
671 def getBlockedContacts(self
):
673 Obtain all the contacts on my block list
675 return self
._getContactsFromList
(BLOCK_LIST
)
677 def getAuthorizedContacts(self
):
679 Obtain all the contacts on my auth list.
680 (These are contacts which I have verified
681 can view my state changes).
683 return self
._getContactsFromList
(ALLOW_LIST
)
685 def getReverseContacts(self
):
687 Get all contacts on my reverse list.
688 (These are contacts which have added me
689 to their forward list).
691 return self
._getContactsFromList
(REVERSE_LIST
)
693 def getContacts(self
):
695 Get all contacts on my forward list.
696 (These are the contacts which I have added
699 return self
._getContactsFromList
(FORWARD_LIST
)
701 def setGroup(self
, id, name
):
703 Keep a mapping from the given id
706 self
.groups
[id] = name
708 def remGroup(self
, id):
710 Removed the stored group
711 mapping for the given id.
715 except KeyError: pass
716 for c
in self
.contacts
:
717 if id in c
.groups
: c
.groups
.remove(id)
720 class MSNEventBase(LineReceiver
):
722 This class provides support for handling / dispatching events and is the
723 base class of the three main client protocols (DispatchClient,
724 NotificationClient, SwitchboardClient)
728 self
.ids
= {} # mapping of ids to Deferreds
732 self
.currentMessage
= None
734 def connectionLost(self
, reason
):
738 def connectionMade(self
):
741 def _fireCallback(self
, id, *args
):
743 Fire the callback for the given id
744 if one exists and return 1, else return false
746 if self
.ids
.has_key(id):
747 self
.ids
[id][0].callback(args
)
752 def _nextTransactionID(self
):
753 """ return a usable transaction ID """
755 if self
.currentID
> 1000: self
.currentID
= 1
756 return self
.currentID
758 def _createIDMapping(self
, data
=None):
760 return a unique transaction ID that is mapped internally to a
761 deferred .. also store arbitrary data if it is needed
763 id = self
._nextTransactionID
()
765 self
.ids
[id] = (d
, data
)
768 def checkMessage(self
, message
):
770 process received messages to check for file invitations and
771 typing notifications and other control type messages
773 raise NotImplementedError
775 def sendLine(self
, line
):
776 if LINEDEBUG
: log
.msg("<< " + line
)
777 LineReceiver
.sendLine(self
, line
)
779 def lineReceived(self
, line
):
780 if LINEDEBUG
: log
.msg(">> " + line
)
781 if not self
.connected
: return
782 if self
.currentMessage
:
783 self
.currentMessage
.readPos
+= len(line
+"\r\n")
785 header
, value
= line
.split(':')
786 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
789 #raise MSNProtocolError, "Invalid Message Header"
791 if line
== "" or self
.currentMessage
.specialMessage
:
793 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
796 cmd
, params
= line
.split(' ', 1)
798 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
800 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
802 id = params
.split(' ')[0]
803 if id.isdigit() and self
.ids
.has_key(int(id)):
805 self
.ids
[id][0].errback(int(cmd
))
808 else: # we received an error which doesn't map to a sent command
809 self
.gotError(int(cmd
))
812 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
814 try: handler(params
.split(' '))
815 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
817 self
.handle_UNKNOWN(cmd
, params
.split(' '))
819 def rawDataReceived(self
, data
):
820 if not self
.connected
: return
822 self
.currentMessage
.readPos
+= len(data
)
823 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
825 self
.currentMessage
.message
+= data
[:-diff
]
828 self
.currentMessage
.message
+= data
830 self
.currentMessage
.message
+= data
832 del self
.currentMessage
.readPos
833 m
= self
.currentMessage
834 self
.currentMessage
= None
835 if MESSAGEDEBUG
: log
.msg(m
.message
)
837 if not self
.checkMessage(m
):
838 self
.setLineMode(extra
)
841 self
.setLineMode(extra
)
844 self
.setLineMode(extra
)
846 ### protocol command handlers - no need to override these.
848 def handle_MSG(self
, params
):
849 checkParamLen(len(params
), 3, 'MSG')
851 messageLen
= int(params
[2])
852 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
853 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
855 def handle_UNKNOWN(self
, cmd
, params
):
856 """ implement me in subclasses if you want to handle unknown events """
857 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
861 def gotBadLine(self
, line
, why
):
862 """ called when a handler notifies me that this line is broken """
863 log
.msg('Error in line: %s (%s)' % (line
, why
))
865 def gotError(self
, errorCode
):
867 called when the server sends an error which is not in
868 response to a sent command (ie. it has no matching transaction ID)
870 log
.msg('Error %s' % (errorCodes
[errorCode
]))
873 class DispatchClient(MSNEventBase
):
875 This class provides support for clients connecting to the dispatch server
876 @ivar userHandle: your user handle (passport) needed before connecting.
879 def connectionMade(self
):
880 MSNEventBase
.connectionMade(self
)
881 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
883 ### protocol command handlers ( there is no need to override these )
885 def handle_VER(self
, params
):
886 versions
= params
[1:]
887 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
888 self
.transport
.loseConnection()
889 raise MSNProtocolError
, "Invalid version response"
890 id = self
._nextTransactionID
()
891 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.factory
.userHandle
))
893 def handle_CVR(self
, params
):
894 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
896 def handle_XFR(self
, params
):
897 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
898 id, refType
, addr
= params
[:3]
899 # was addr a host:port pair?
901 host
, port
= addr
.split(':')
906 self
.gotNotificationReferral(host
, int(port
))
910 def gotNotificationReferral(self
, host
, port
):
912 called when we get a referral to the notification server.
914 @param host: the notification server's hostname
915 @param port: the port to connect to
920 class DispatchFactory(ClientFactory
):
922 This class keeps the state for the DispatchClient.
924 @ivar userHandle: the userHandle to request a notification
927 protocol
= DispatchClient
932 class NotificationClient(MSNEventBase
):
934 This class provides support for clients connecting
935 to the notification server.
938 factory
= None # sssh pychecker
940 def __init__(self
, currentID
=0):
941 MSNEventBase
.__init
__(self
)
942 self
.currentID
= currentID
943 self
._state
= ['DISCONNECTED', {}]
945 self
.pingCheckTask
= None
946 self
.msnobj
= MSNObject()
948 def _setState(self
, state
):
949 self
._state
[0] = state
952 return self
._state
[0]
954 def _getStateData(self
, key
):
955 return self
._state
[1][key
]
957 def _setStateData(self
, key
, value
):
958 self
._state
[1][key
] = value
960 def _remStateData(self
, *args
):
961 for key
in args
: del self
._state
[1][key
]
963 def connectionMade(self
):
964 MSNEventBase
.connectionMade(self
)
965 self
._setState
('CONNECTED')
966 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
967 self
.factory
.resetDelay()
969 def connectionLost(self
, reason
):
970 self
._setState
('DISCONNECTED')
972 if self
.pingCheckTask
:
973 self
.pingCheckTask
.stop()
974 self
.pingCheckTask
= None
975 MSNEventBase
.connectionLost(self
, reason
)
977 def _getEmailFields(self
, message
):
978 fields
= message
.getMessage().strip().split('\n')
982 if len(a
) != 2: continue
989 def _gotInitialEmailNotification(self
, message
):
990 values
= self
._getEmailFields
(message
)
992 inboxunread
= int(values
["Inbox-Unread"])
993 foldersunread
= int(values
["Folders-Unread"])
996 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
997 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
999 def _gotEmailNotification(self
, message
):
1000 values
= self
._getEmailFields
(message
)
1002 mailfrom
= values
["From"]
1003 fromaddr
= values
["From-Addr"]
1004 subject
= values
["Subject"]
1005 junkbeginning
= "=?\"us-ascii\"?Q?"
1007 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
1009 # If any of the fields weren't found then it's not a big problem. We just ignore the message
1011 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
1013 def _gotMSNAlert(self
, message
):
1014 notification
= parseText(message
.message
, beExtremelyLenient
=True)
1015 siteurl
= notification
.getAttribute("siteurl")
1016 notid
= notification
.getAttribute("id")
1019 for e
in notification
.elements():
1025 msgid
= msg
.getAttribute("id")
1030 for e
in msg
.elements():
1031 if e
.name
== "ACTION":
1032 action
= e
.getAttribute("url")
1033 if e
.name
== "SUBSCR":
1034 subscr
= e
.getAttribute("url")
1035 if e
.name
== "BODY":
1036 for e2
in e
.elements():
1037 if e2
.name
== "TEXT":
1038 bodytext
= e2
.__str__()
1039 if not (action
and subscr
and bodytext
): return
1041 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
1042 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
1044 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
1046 def _gotUBX(self
, message
):
1047 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
1048 if not msnContact
: return
1049 lm
= message
.message
.lower()
1050 p1
= lm
.find("<psm>") + 5
1051 p2
= lm
.find("</psm>")
1052 if p1
>= 0 and p2
>= 0:
1053 personal
= unescapeFromXml(message
.message
[p1
:p2
])
1054 msnContact
.personal
= personal
1055 self
.contactPersonalChanged(message
.userHandle
, personal
)
1057 msnContact
.personal
= ''
1058 self
.contactPersonalChanged(message
.userHandle
, '')
1060 def checkMessage(self
, message
):
1061 """ hook used for detecting specific notification messages """
1062 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
1063 if 'text/x-msmsgsprofile' in cTypes
:
1064 self
.gotProfile(message
)
1066 elif "text/x-msmsgsinitialemailnotification" in cTypes
:
1067 self
._gotInitialEmailNotification
(message
)
1069 elif "text/x-msmsgsemailnotification" in cTypes
:
1070 self
._gotEmailNotification
(message
)
1072 elif "NOTIFICATION" == message
.userHandle
and message
.specialMessage
== True:
1073 self
._gotMSNAlert
(message
)
1075 elif "UBX" == message
.screenName
and message
.specialMessage
== True:
1076 self
._gotUBX
(message
)
1080 ### protocol command handlers - no need to override these
1082 def handle_VER(self
, params
):
1083 versions
= params
[1:]
1084 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
1085 self
.transport
.loseConnection()
1086 raise MSNProtocolError
, "Invalid version response"
1087 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
1089 def handle_CVR(self
, params
):
1090 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
1092 def handle_USR(self
, params
):
1093 if not (4 <= len(params
) <= 6):
1094 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
1096 mechanism
= params
[1]
1097 if mechanism
== "OK":
1098 self
.loggedIn(params
[2], int(params
[3]))
1099 elif params
[2].upper() == "S":
1100 # we need to obtain auth from a passport server
1102 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3])
1103 d
.addCallback(self
._passportLogin
)
1104 d
.addErrback(self
._passportError
)
1106 def _passportLogin(self
, result
):
1107 if result
[0] == LOGIN_REDIRECT
:
1108 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
1109 result
[1], cached
=1, authData
=result
[2])
1110 d
.addCallback(self
._passportLogin
)
1111 d
.addErrback(self
._passportError
)
1112 elif result
[0] == LOGIN_SUCCESS
:
1113 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
1114 elif result
[0] == LOGIN_FAILURE
:
1115 self
.loginFailure(result
[1])
1117 def _passportError(self
, failure
):
1118 self
.loginFailure("Exception while authenticating: %s" % failure
)
1120 def handle_CHG(self
, params
):
1122 if not self
._fireCallback
(id, params
[1]):
1123 if self
.factory
: self
.factory
.status
= params
[1]
1124 self
.statusChanged(params
[1])
1126 def handle_ILN(self
, params
):
1127 #checkParamLen(len(params), 6, 'ILN')
1128 msnContact
= self
.factory
.contacts
.getContact(params
[2])
1129 if not msnContact
: return
1130 msnContact
.status
= params
[1]
1131 msnContact
.screenName
= unquote(params
[3])
1132 if len(params
) > 4: msnContact
.caps
= int(params
[4])
1133 if len(params
) > 5 and params
[5] != "0":
1134 self
.handleAvatarHelper(msnContact
, params
[5])
1136 self
.handleAvatarGoneHelper(msnContact
)
1137 self
.gotContactStatus(params
[2], params
[1], unquote(params
[3]))
1139 def handleAvatarGoneHelper(self
, msnContact
):
1140 if msnContact
.msnobj
:
1141 msnContact
.msnobj
= None
1142 msnContact
.msnobjGot
= True
1143 self
.contactAvatarChanged(msnContact
.userHandle
, "")
1145 def handleAvatarHelper(self
, msnContact
, msnobjStr
):
1146 msnobj
= MSNObject(unquote(msnobjStr
))
1147 if not msnContact
.msnobj
or msnobj
.sha1d
!= msnContact
.msnobj
.sha1d
:
1148 if MSNP2PDEBUG
: log
.msg("Updated MSNObject received!" + msnobjStr
)
1149 msnContact
.msnobj
= msnobj
1150 msnContact
.msnobjGot
= False
1151 self
.contactAvatarChanged(msnContact
.userHandle
, binascii
.hexlify(b64dec(msnContact
.msnobj
.sha1d
)))
1153 def handle_CHL(self
, params
):
1154 checkParamLen(len(params
), 2, 'CHL')
1155 response
= msnp11chl
.doChallenge(params
[1])
1156 self
.sendLine("QRY %s %s %s" % (self
._nextTransactionID
(), msnp11chl
.MSNP11_PRODUCT_ID
, len(response
)))
1157 self
.transport
.write(response
)
1159 def handle_QRY(self
, params
):
1162 def handle_NLN(self
, params
):
1163 if not self
.factory
: return
1164 msnContact
= self
.factory
.contacts
.getContact(params
[1])
1165 if not msnContact
: return
1166 msnContact
.status
= params
[0]
1167 msnContact
.screenName
= unquote(params
[2])
1168 if len(params
) > 3: msnContact
.caps
= int(params
[3])
1169 if len(params
) > 4 and params
[4] != "0":
1170 self
.handleAvatarHelper(msnContact
, params
[4])
1172 self
.handleAvatarGoneHelper(msnContact
)
1173 self
.contactStatusChanged(params
[1], params
[0], unquote(params
[2]))
1175 def handle_FLN(self
, params
):
1176 checkParamLen(len(params
), 1, 'FLN')
1177 msnContact
= self
.factory
.contacts
.getContact(params
[0])
1179 msnContact
.status
= STATUS_OFFLINE
1180 self
.contactOffline(params
[0])
1182 def handle_LST(self
, params
):
1183 if self
._getState
() != 'SYNC': return
1185 userHandle
, screenName
, userGuid
, lists
, groups
= getVals(params
)
1187 if not userHandle
or lists
< 1:
1188 raise MSNProtocolError
, "Unknown LST " + str(params
) # debug
1189 contact
= MSNContact(userGuid
, userHandle
, screenName
, lists
)
1190 if contact
.lists
& FORWARD_LIST
:
1191 contact
.groups
.extend(map(str, groups
))
1192 self
._getStateData
('list').addContact(contact
)
1193 self
._setStateData
('last_contact', contact
)
1194 sofar
= self
._getStateData
('lst_sofar') + 1
1195 if sofar
== self
._getStateData
('lst_reply'):
1196 # this is the best place to determine that
1197 # a syn realy has finished - msn _may_ send
1198 # BPR information for the last contact
1199 # which is unfortunate because it means
1200 # that the real end of a syn is non-deterministic.
1201 # to handle this we'll keep 'last_contact' hanging
1202 # around in the state data and update it if we need
1204 self
._setState
('SESSION')
1205 contacts
= self
._getStateData
('list')
1206 phone
= self
._getStateData
('phone')
1207 id = self
._getStateData
('synid')
1208 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1209 self
._fireCallback
(id, contacts
, phone
)
1211 self
._setStateData
('lst_sofar',sofar
)
1213 def handle_BLP(self
, params
):
1214 # check to see if this is in response to a SYN
1215 if self
._getState
() == 'SYNC':
1216 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
1219 self
.factory
.contacts
.privacy
= listCodeToID
[params
[1].lower()]
1220 self
._fireCallback
(id, params
[1])
1222 def handle_GTC(self
, params
):
1223 # check to see if this is in response to a SYN
1224 if self
._getState
() == 'SYNC':
1225 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
1226 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
1227 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1230 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
1231 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
1232 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1234 def handle_SYN(self
, params
):
1236 self
._setStateData
('phone', []) # Always needs to be set
1237 if params
[3] == 0: # No LST will be received. New account?
1238 self
._setState
('SESSION')
1239 self
._fireCallback
(id, None, None)
1241 contacts
= MSNContactList()
1242 self
._setStateData
('list', contacts
)
1243 self
._setStateData
('lst_reply', int(params
[3]))
1244 self
._setStateData
('lsg_reply', int(params
[4]))
1245 self
._setStateData
('lst_sofar', 0)
1247 def handle_LSG(self
, params
):
1248 if self
._getState
() == 'SYNC':
1249 self
._getStateData
('list').groups
[params
[1]] = unquote(params
[0])
1251 def handle_PRP(self
, params
):
1252 if params
[1] == "MFN":
1253 self
._fireCallback
(int(params
[0]))
1254 elif self
._getState
() == 'SYNC':
1255 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
1257 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
1259 def handle_BPR(self
, params
):
1260 numParams
= len(params
)
1261 if numParams
== 2: # part of a syn
1262 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
1263 elif numParams
== 4:
1264 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_BPR called with no contact list" # debug
1265 self
.factory
.contacts
.version
= int(params
[0])
1266 userHandle
, phoneType
, number
= params
[1], params
[2], unquote(params
[3])
1267 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1268 self
.gotPhoneNumber(userHandle
, phoneType
, number
)
1271 def handle_ADG(self
, params
):
1272 checkParamLen(len(params
), 5, 'ADG')
1274 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
1275 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
1277 def handle_RMG(self
, params
):
1278 checkParamLen(len(params
), 3, 'RMG')
1280 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
1281 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
1283 def handle_REG(self
, params
):
1284 checkParamLen(len(params
), 5, 'REG')
1286 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
1287 raise MSNProtocolError
, "REG response does not match up to a request" # debug
1289 def handle_ADC(self
, params
):
1290 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_ADC called with no contact list"
1291 numParams
= len(params
)
1292 if numParams
< 3 or params
[1].upper() not in ('AL','BL','RL','FL','PL'):
1293 raise MSNProtocolError
, "Invalid Paramaters for ADC" # debug
1295 listType
= params
[1].lower()
1296 userHandle
, screenName
, userGuid
, ignored1
, groups
= getVals(params
[2:])
1298 if groups
and listType
.upper() != FORWARD_LIST
:
1299 raise MSNProtocolError
, "Only forward list can contain groups" # debug
1301 if not self
._fireCallback
(id, listCodeToID
[listType
], userGuid
, userHandle
, screenName
):
1302 c
= self
.factory
.contacts
.getContact(userHandle
)
1304 c
= MSNContact(userGuid
=userGuid
, userHandle
=userHandle
, screenName
=screenName
)
1305 self
.factory
.contacts
.addContact(c
)
1306 c
.addToList(PENDING_LIST
)
1307 self
.userAddedMe(userGuid
, userHandle
, screenName
)
1309 def handle_REM(self
, params
):
1310 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_REM called with no contact list available!"
1311 numParams
= len(params
)
1312 if numParams
< 3 or params
[1].upper() not in ('AL','BL','FL','RL','PL'):
1313 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1315 listType
= params
[1].lower()
1316 userHandle
= params
[2]
1319 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1320 groupID
= int(params
[3])
1321 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, groupID
):
1322 if listType
.upper() != "RL": return
1323 c
= self
.factory
.contacts
.getContact(userHandle
)
1325 c
.removeFromList(REVERSE_LIST
)
1326 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1327 self
.userRemovedMe(userHandle
)
1329 def handle_XFR(self
, params
):
1330 checkParamLen(len(params
), 5, 'XFR')
1332 # check to see if they sent a host/port pair
1334 host
, port
= params
[2].split(':')
1339 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1340 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1342 def handle_RNG(self
, params
):
1343 checkParamLen(len(params
), 6, 'RNG')
1344 # check for host:port pair
1346 host
, port
= params
[1].split(":")
1351 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1354 def handle_NOT(self
, params
):
1355 checkParamLen(len(params
), 1, 'NOT')
1357 messageLen
= int(params
[0])
1358 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1359 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", specialMessage
=True)
1362 def handle_UBX(self
, params
):
1363 checkParamLen(len(params
), 2, 'UBX')
1365 messageLen
= int(params
[1])
1366 except ValueError: raise MSNProtocolError
, "Invalid Parameter for UBX length argument"
1368 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
="UBX", specialMessage
=True)
1371 self
._gotUBX
(MSNMessage(userHandle
=params
[0]))
1373 def handle_UUX(self
, params
):
1374 checkParamLen(len(params
), 2, 'UUX')
1375 if params
[1] != '0': return
1377 self
._fireCallback
(id)
1379 def handle_OUT(self
, params
):
1380 checkParamLen(len(params
), 1, 'OUT')
1381 self
.factory
.stopTrying()
1382 if params
[0] == "OTH": self
.multipleLogin()
1383 elif params
[0] == "SSD": self
.serverGoingDown()
1384 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1386 def handle_QNG(self
, params
):
1387 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1391 def pingChecker(self
):
1392 if self
.pingCounter
> 5:
1393 # The server has ignored 5 pings, lets kill the connection
1394 self
.transport
.loseConnection()
1396 self
.sendLine("PNG")
1397 self
.pingCounter
+= 1
1399 def pingCheckerStart(self
, *args
):
1400 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1401 self
.pingCheckTask
.start(PINGSPEED
)
1403 def loggedIn(self
, userHandle
, verified
):
1405 Called when the client has logged in.
1406 The default behaviour of this method is to
1407 update the factory with our screenName and
1408 to sync the contact list (factory.contacts).
1409 When this is complete self.listSynchronized
1412 @param userHandle: our userHandle
1413 @param verified: 1 if our passport has been (verified), 0 if not.
1414 (i'm not sure of the significace of this)
1418 d
.addCallback(self
.listSynchronized
)
1419 d
.addCallback(self
.pingCheckerStart
)
1421 def loginFailure(self
, message
):
1423 Called when the client fails to login.
1425 @param message: a message indicating the problem that was encountered
1429 def gotProfile(self
, message
):
1431 Called after logging in when the server sends an initial
1432 message with MSN/passport specific profile information
1433 such as country, number of kids, etc.
1434 Check the message headers for the specific values.
1436 @param message: The profile message
1440 def listSynchronized(self
, *args
):
1442 Lists are now synchronized by default upon logging in, this
1443 method is called after the synchronization has finished
1444 and the factory now has the up-to-date contacts.
1448 def contactAvatarChanged(self
, userHandle
, hash):
1450 Called when we receive the first, or a new <msnobj/> from a
1453 @param userHandle: contact who's msnobj has been changed
1454 @param hash: sha1 hash of their avatar as hex string
1457 def statusChanged(self
, statusCode
):
1459 Called when our status changes and its not in response to a
1462 @param statusCode: 3-letter status code
1466 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
1468 Called when we receive a list of statuses upon login.
1470 @param userHandle: the contact's user handle (passport)
1471 @param statusCode: 3-letter status code
1472 @param screenName: the contact's screen name
1476 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
1478 Called when we're notified that a contact's status has changed.
1480 @param userHandle: the contact's user handle (passport)
1481 @param statusCode: 3-letter status code
1482 @param screenName: the contact's screen name
1486 def contactPersonalChanged(self
, userHandle
, personal
):
1488 Called when a contact's personal message changes.
1490 @param userHandle: the contact who changed their personal message
1491 @param personal : the new personal message
1495 def contactOffline(self
, userHandle
):
1497 Called when a contact goes offline.
1499 @param userHandle: the contact's user handle
1503 def gotMessage(self
, message
):
1505 Called when there is a message from the notification server
1506 that is not understood by default.
1508 @param message: the MSNMessage.
1512 def gotMSNAlert(self
, body
, action
, subscr
):
1514 Called when the server sends an MSN Alert (http://alerts.msn.com)
1516 @param body : the alert text
1517 @param action: a URL with more information for the user to view
1518 @param subscr: a URL the user can use to modify their alert subscription
1522 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1524 Called when the server sends you details about your hotmail
1525 inbox. This is only ever called once, on login.
1527 @param inboxunread : the number of unread items in your inbox
1528 @param foldersunread: the number of unread items in other folders
1532 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1534 Called when the server sends us realtime email
1535 notification. This means that you have received
1536 a new email in your hotmail inbox.
1538 @param mailfrom: the sender of the email
1539 @param fromaddr: the sender of the email (I don't know :P)
1540 @param subject : the email subject
1544 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1546 Called when the server sends us phone details about
1547 a specific user (for example after a user is added
1548 the server will send their status, phone details etc.
1550 @param userHandle: the contact's user handle (passport)
1551 @param phoneType: the specific phoneType
1552 (*_PHONE constants or HAS_PAGER)
1553 @param number: the value/phone number.
1557 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1559 Called when a user adds me to their list. (ie. they have been added to
1562 @param userHandle: the userHandle of the user
1563 @param screenName: the screen name of the user
1567 def userRemovedMe(self
, userHandle
):
1569 Called when a user removes us from their contact list
1570 (they are no longer on our reverseContacts list.
1572 @param userHandle: the contact's user handle (passport)
1576 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1577 key
, userHandle
, screenName
):
1579 Called when we get an invitation to a switchboard server.
1580 This happens when a user requests a chat session with us.
1582 @param sessionID: session ID number, must be remembered for logging in
1583 @param host: the hostname of the switchboard server
1584 @param port: the port to connect to
1585 @param key: used for authorization when connecting
1586 @param userHandle: the user handle of the person who invited us
1587 @param screenName: the screen name of the person who invited us
1591 def multipleLogin(self
):
1593 Called when the server says there has been another login
1594 under our account, the server should disconnect us right away.
1598 def serverGoingDown(self
):
1600 Called when the server has notified us that it is going down for
1607 def changeStatus(self
, status
):
1609 Change my current status. This method will add
1610 a default callback to the returned Deferred
1611 which will update the status attribute of the
1614 @param status: 3-letter status code (as defined by
1615 the STATUS_* constants)
1616 @return: A Deferred, the callback of which will be
1617 fired when the server confirms the change
1618 of status. The callback argument will be
1619 a tuple with the new status code as the
1623 id, d
= self
._createIDMapping
()
1624 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1626 self
.factory
.status
= r
[0]
1628 return d
.addCallback(_cb
)
1630 def setPrivacyMode(self
, privLevel
):
1632 Set my privacy mode on the server.
1635 This only keeps the current privacy setting on
1636 the server for later retrieval, it does not
1637 effect the way the server works at all.
1639 @param privLevel: This parameter can be true, in which
1640 case the server will keep the state as
1641 'al' which the official client interprets
1642 as -> allow messages from only users on
1643 the allow list. Alternatively it can be
1644 false, in which case the server will keep
1645 the state as 'bl' which the official client
1646 interprets as -> allow messages from all
1647 users except those on the block list.
1649 @return: A Deferred, the callback of which will be fired when
1650 the server replies with the new privacy setting.
1651 The callback argument will be a tuple, the only element
1652 of which being either 'al' or 'bl' (the new privacy setting).
1655 id, d
= self
._createIDMapping
()
1656 if privLevel
: self
.sendLine("BLP %s AL" % id)
1657 else: self
.sendLine("BLP %s BL" % id)
1662 Used for keeping an up-to-date contact list.
1663 A callback is added to the returned Deferred
1664 that updates the contact list on the factory
1665 and also sets my state to STATUS_ONLINE.
1668 This is called automatically upon signing
1669 in using the version attribute of
1670 factory.contacts, so you may want to persist
1671 this object accordingly. Because of this there
1672 is no real need to ever call this method
1675 @return: A Deferred, the callback of which will be
1676 fired when the server sends an adequate reply.
1677 The callback argument will be a tuple with two
1678 elements, the new list (MSNContactList) and
1679 your current state (a dictionary). If the version
1680 you sent _was_ the latest list version, both elements
1681 will be None. To just request the list send a version of 0.
1684 self
._setState
('SYNC')
1685 id, d
= self
._createIDMapping
(data
=None)
1686 self
._setStateData
('synid',id)
1687 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1689 self
.changeStatus(STATUS_ONLINE
)
1690 if r
[0] is not None:
1691 self
.factory
.contacts
= r
[0]
1693 return d
.addCallback(_cb
)
1695 def setPhoneDetails(self
, phoneType
, value
):
1697 Set/change my phone numbers stored on the server.
1699 @param phoneType: phoneType can be one of the following
1700 constants - HOME_PHONE, WORK_PHONE,
1701 MOBILE_PHONE, HAS_PAGER.
1702 These are pretty self-explanatory, except
1703 maybe HAS_PAGER which refers to whether or
1704 not you have a pager.
1705 @param value: for all of the *_PHONE constants the value is a
1706 phone number (str), for HAS_PAGER accepted values
1707 are 'Y' (for yes) and 'N' (for no).
1709 @return: A Deferred, the callback for which will be fired when
1710 the server confirms the change has been made. The
1711 callback argument will be a tuple with 2 elements, the
1712 first being the new list version (int) and the second
1713 being the new phone number value (str).
1715 raise "ProbablyDoesntWork"
1716 # XXX: Add a default callback which updates
1717 # factory.contacts.version and the relevant phone
1719 id, d
= self
._createIDMapping
()
1720 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1723 def addListGroup(self
, name
):
1725 Used to create a new list group.
1726 A default callback is added to the
1727 returned Deferred which updates the
1728 contacts attribute of the factory.
1730 @param name: The desired name of the new group.
1732 @return: A Deferred, the callbacck for which will be called
1733 when the server clarifies that the new group has been
1734 created. The callback argument will be a tuple with 3
1735 elements: the new list version (int), the new group name
1736 (str) and the new group ID (int).
1739 raise "ProbablyDoesntWork"
1740 id, d
= self
._createIDMapping
()
1741 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1743 if self
.factory
.contacts
:
1744 self
.factory
.contacts
.version
= r
[0]
1745 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1747 return d
.addCallback(_cb
)
1749 def remListGroup(self
, groupID
):
1751 Used to remove a list group.
1752 A default callback is added to the
1753 returned Deferred which updates the
1754 contacts attribute of the factory.
1756 @param groupID: the ID of the desired group to be removed.
1758 @return: A Deferred, the callback for which will be called when
1759 the server clarifies the deletion of the group.
1760 The callback argument will be a tuple with 2 elements:
1761 the new list version (int) and the group ID (int) of
1765 raise "ProbablyDoesntWork"
1766 id, d
= self
._createIDMapping
()
1767 self
.sendLine("RMG %s %s" % (id, groupID
))
1769 self
.factory
.contacts
.version
= r
[0]
1770 self
.factory
.contacts
.remGroup(r
[1])
1772 return d
.addCallback(_cb
)
1774 def renameListGroup(self
, groupID
, newName
):
1776 Used to rename an existing list group.
1777 A default callback is added to the returned
1778 Deferred which updates the contacts attribute
1781 @param groupID: the ID of the desired group to rename.
1782 @param newName: the desired new name for the group.
1784 @return: A Deferred, the callback for which will be called
1785 when the server clarifies the renaming.
1786 The callback argument will be a tuple of 3 elements,
1787 the new list version (int), the group id (int) and
1788 the new group name (str).
1791 raise "ProbablyDoesntWork"
1792 id, d
= self
._createIDMapping
()
1793 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1795 self
.factory
.contacts
.version
= r
[0]
1796 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1798 return d
.addCallback(_cb
)
1800 def addContact(self
, listType
, userHandle
):
1802 Used to add a contact to the desired list.
1803 A default callback is added to the returned
1804 Deferred which updates the contacts attribute of
1805 the factory with the new contact information.
1806 If you are adding a contact to the forward list
1807 and you want to associate this contact with multiple
1808 groups then you will need to call this method for each
1809 group you would like to add them to, changing the groupID
1810 parameter. The default callback will take care of updating
1811 the group information on the factory's contact list.
1813 @param listType: (as defined by the *_LIST constants)
1814 @param userHandle: the user handle (passport) of the contact
1817 @return: A Deferred, the callback for which will be called when
1818 the server has clarified that the user has been added.
1819 The callback argument will be a tuple with 4 elements:
1820 the list type, the contact's user handle, the new list
1821 version, and the group id (if relevant, otherwise it
1825 id, d
= self
._createIDMapping
()
1826 try: # Make sure the contact isn't actually on the list
1827 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1828 except AttributeError: pass
1829 listType
= listIDToCode
[listType
].upper()
1830 if listType
== "FL":
1831 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1833 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1836 if not self
.factory
: return
1837 c
= self
.factory
.contacts
.getContact(r
[2])
1839 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1840 self
.factory
.contacts
.addContact(c
)
1841 #if r[3]: c.groups.append(r[3])
1844 return d
.addCallback(_cb
)
1846 def remContact(self
, listType
, userHandle
):
1848 Used to remove a contact from the desired list.
1849 A default callback is added to the returned deferred
1850 which updates the contacts attribute of the factory
1851 to reflect the new contact information.
1853 @param listType: (as defined by the *_LIST constants)
1854 @param userHandle: the user handle (passport) of the
1855 contact being removed
1857 @return: A Deferred, the callback for which will be called when
1858 the server has clarified that the user has been removed.
1859 The callback argument will be a tuple of 3 elements:
1860 the list type, the contact's user handle and the group ID
1861 (if relevant, otherwise it will be None)
1864 id, d
= self
._createIDMapping
()
1865 try: # Make sure the contact is actually on this list
1866 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1867 except AttributeError: return
1868 listType
= listIDToCode
[listType
].upper()
1869 if listType
== "FL":
1871 c
= self
.factory
.contacts
.getContact(userHandle
)
1872 userGuid
= c
.userGuid
1873 except AttributeError: return
1874 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1876 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1879 if listType
== "FL":
1880 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1881 l
= self
.factory
.contacts
1882 c
= l
.getContact(r
[1])
1886 if group
: # they may not have been removed from the list
1887 c
.groups
.remove(group
)
1888 if c
.groups
: shouldRemove
= 0
1890 c
.removeFromList(r
[0])
1891 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1893 return d
.addCallback(_cb
)
1895 def changeScreenName(self
, newName
):
1897 Used to change your current screen name.
1898 A default callback is added to the returned
1899 Deferred which updates the screenName attribute
1900 of the factory and also updates the contact list
1903 @param newName: the new screen name
1905 @return: A Deferred, the callback for which will be called
1906 when the server acknowledges the change.
1907 The callback argument will be an empty tuple.
1910 id, d
= self
._createIDMapping
()
1911 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1913 self
.factory
.screenName
= newName
1915 return d
.addCallback(_cb
)
1917 def changePersonalMessage(self
, personal
):
1919 Used to change your personal message.
1921 @param personal: the new screen name
1923 @return: A Deferred, the callback for which will be called
1924 when the server acknowledges the change.
1925 The callback argument will be a tuple of 1 element,
1926 the personal message.
1929 id, d
= self
._createIDMapping
()
1932 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1933 self
.sendLine("UUX %s %s" % (id, len(data
)))
1934 self
.transport
.write(data
)
1936 self
.factory
.personal
= personal
1938 return d
.addCallback(_cb
)
1940 def changeAvatar(self
, imageData
, push
):
1942 Used to change the avatar that other users see.
1944 @param imageData: the PNG image data to set as the avatar
1945 @param push : whether to push the update to the server
1946 (it will otherwise be sent with the next
1949 @return: If push==True, a Deferred, the callback for which
1950 will be called when the server acknowledges the change.
1951 The callback argument will be the same as for changeStatus.
1954 if self
.msnobj
and imageData
== self
.msnobj
.imageData
: return
1956 self
.msnobj
.setData(self
.factory
.userHandle
, imageData
)
1958 self
.msnobj
.setNull()
1959 if push
: return self
.changeStatus(self
.factory
.status
) # Push to server
1962 def requestSwitchboardServer(self
):
1964 Used to request a switchboard server to use for conversations.
1966 @return: A Deferred, the callback for which will be called when
1967 the server responds with the switchboard information.
1968 The callback argument will be a tuple with 3 elements:
1969 the host of the switchboard server, the port and a key
1970 used for logging in.
1973 id, d
= self
._createIDMapping
()
1974 self
.sendLine("XFR %s SB" % id)
1979 Used to log out of the notification server.
1980 After running the method the server is expected
1981 to close the connection.
1984 if self
.pingCheckTask
:
1985 self
.pingCheckTask
.stop()
1986 self
.pingCheckTask
= None
1987 self
.factory
.stopTrying()
1988 self
.sendLine("OUT")
1989 self
.transport
.loseConnection()
1991 class NotificationFactory(ReconnectingClientFactory
):
1993 Factory for the NotificationClient protocol.
1994 This is basically responsible for keeping
1995 the state of the client and thus should be used
1996 in a 1:1 situation with clients.
1998 @ivar contacts: An MSNContactList instance reflecting
1999 the current contact list -- this is
2000 generally kept up to date by the default
2002 @ivar userHandle: The client's userHandle, this is expected
2003 to be set by the client and is used by the
2004 protocol (for logging in etc).
2005 @ivar screenName: The client's current screen-name -- this is
2006 generally kept up to date by the default
2008 @ivar password: The client's password -- this is (obviously)
2009 expected to be set by the client.
2010 @ivar passportServer: This must point to an msn passport server
2011 (the whole URL is required)
2012 @ivar status: The status of the client -- this is generally kept
2013 up to date by the default command handlers
2014 @ivar maxRetries: The number of times the factory will reconnect
2015 if the connection dies because of a network error.
2022 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
2024 protocol
= NotificationClient
2028 class SwitchboardClient(MSNEventBase
):
2030 This class provides support for clients connecting to a switchboard server.
2032 Switchboard servers are used for conversations with other people
2033 on the MSN network. This means that the number of conversations at
2034 any given time will be directly proportional to the number of
2035 connections to varioius switchboard servers.
2037 MSN makes no distinction between single and group conversations,
2038 so any number of users may be invited to join a specific conversation
2039 taking place on a switchboard server.
2041 @ivar key: authorization key, obtained when receiving
2042 invitation / requesting switchboard server.
2043 @ivar userHandle: your user handle (passport)
2044 @ivar sessionID: unique session ID, used if you are replying
2045 to a switchboard invitation
2046 @ivar reply: set this to 1 in connectionMade or before to signifiy
2047 that you are replying to a switchboard invitation.
2048 @ivar msnobj: the MSNObject for the user's avatar. So that the
2049 switchboard can distribute it to anyone who asks.
2061 MSNEventBase
.__init
__(self
)
2062 self
.pendingUsers
= {}
2063 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
2066 def connectionMade(self
):
2067 MSNEventBase
.connectionMade(self
)
2070 def connectionLost(self
, reason
):
2071 self
.cookies
['iCookies'] = {}
2072 MSNEventBase
.connectionLost(self
, reason
)
2074 def _sendInit(self
):
2076 send initial data based on whether we are replying to an invitation
2079 id = self
._nextTransactionID
()
2081 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
2083 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
2085 def _newInvitationCookie(self
):
2087 if self
._iCookie
> 1000: self
._iCookie
= 1
2088 return self
._iCookie
2090 def _checkTyping(self
, message
, cTypes
):
2091 """ helper method for checkMessage """
2092 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
2093 self
.gotContactTyping(message
)
2096 def _checkFileInvitation(self
, message
, info
):
2097 """ helper method for checkMessage """
2098 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
2100 cookie
= info
['Invitation-Cookie']
2101 filename
= info
['Application-File']
2102 filesize
= int(info
['Application-FileSize'])
2103 connectivity
= (info
.get('Connectivity', 'n').lower() == 'y')
2105 log
.msg('Received munged file transfer request ... ignoring.')
2107 raise NotImplementedError
2108 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
2111 def _handleP2PMessage(self
, message
):
2112 """ helper method for msnslp messages (file transfer & avatars) """
2113 if not message
.getHeader("P2P-Dest") == self
.userHandle
: return
2114 packet
= message
.message
2115 binaryFields
= BinaryFields(packet
=packet
)
2116 if binaryFields
[5] == BinaryFields
.BYEGOT
:
2117 pass # Ignore the ACKs to SLP messages
2118 elif binaryFields
[0] != 0:
2119 slpLink
= self
.slpLinks
.get(binaryFields
[0])
2121 # Link has been killed. Ignore
2123 if slpLink
.remoteUser
== message
.userHandle
:
2124 slpLink
.handlePacket(packet
)
2125 elif binaryFields
[5] == BinaryFields
.ACK
:
2126 pass # Ignore the ACKs to SLP messages
2128 slpMessage
= MSNSLPMessage(packet
)
2130 # Always try and give a slpMessage to a slpLink first.
2131 # If none can be found, and it was INVITE, then create
2132 # one to handle the session.
2133 for slpLink
in self
.slpLinks
.values():
2134 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
2135 slpLink
.handleSLPMessage(slpMessage
)
2138 slpLink
= None # Was not handled
2140 if not slpLink
and slpMessage
.method
== "INVITE":
2141 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
2142 context
= FileContext(slpMessage
.context
)
2143 slpLink
= SLPLink_FileReceive(remoteUser
=slpMessage
.fro
, switchboard
=self
, filename
=context
.filename
, filesize
=context
.filesize
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
, branch
=slpMessage
.branch
)
2144 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2145 self
.gotFileReceive(slpLink
)
2146 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
2147 # Check that we have an avatar to send
2149 slpLink
= SLPLink_AvatarSend(remoteUser
=slpMessage
.fro
, switchboard
=self
, filesize
=self
.msnobj
.size
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2150 slpLink
.write(self
.msnobj
.imageData
)
2153 # They shouldn't have sent a request if we have
2154 # no avatar. So we'll just ignore them.
2155 # FIXME We should really send an error
2158 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2160 # Always need to ACK these packets if we can
2161 slpLink
.sendP2PACK(binaryFields
)
2164 def checkMessage(self
, message
):
2166 hook for detecting any notification type messages
2167 (e.g. file transfer)
2169 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2170 if self
._checkTyping
(message
, cTypes
): return 0
2171 # if 'text/x-msmsgsinvite' in cTypes:
2172 # header like info is sent as part of the message body.
2174 # for line in message.message.split('\r\n'):
2176 # key, val = line.split(':')
2177 # info[key] = val.lstrip()
2178 # except ValueError: continue
2179 # if self._checkFileInvitation(message, info): return 0
2180 elif 'application/x-msnmsgrp2p' in cTypes
:
2181 self
._handleP
2PMessage
(message
)
2186 def handle_USR(self
, params
):
2187 checkParamLen(len(params
), 4, 'USR')
2188 if params
[1] == "OK":
2192 def handle_CAL(self
, params
):
2193 checkParamLen(len(params
), 3, 'CAL')
2195 if params
[1].upper() == "RINGING":
2196 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2199 def handle_JOI(self
, params
):
2200 checkParamLen(len(params
), 2, 'JOI')
2201 self
.userJoined(params
[0], unquote(params
[1]))
2203 # users participating in the current chat
2204 def handle_IRO(self
, params
):
2205 checkParamLen(len(params
), 5, 'IRO')
2206 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2207 if params
[1] == params
[2]:
2208 self
.gotChattingUsers(self
.pendingUsers
)
2209 self
.pendingUsers
= {}
2211 # finished listing users
2212 def handle_ANS(self
, params
):
2213 checkParamLen(len(params
), 2, 'ANS')
2214 if params
[1] == "OK":
2217 def handle_ACK(self
, params
):
2218 checkParamLen(len(params
), 1, 'ACK')
2219 self
._fireCallback
(int(params
[0]), None)
2221 def handle_NAK(self
, params
):
2222 checkParamLen(len(params
), 1, 'NAK')
2223 self
._fireCallback
(int(params
[0]), None)
2225 def handle_BYE(self
, params
):
2226 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2227 self
.userLeft(params
[0])
2233 called when all login details have been negotiated.
2234 Messages can now be sent, or new users invited.
2238 def gotChattingUsers(self
, users
):
2240 called after connecting to an existing chat session.
2242 @param users: A dict mapping user handles to screen names
2243 (current users taking part in the conversation)
2247 def userJoined(self
, userHandle
, screenName
):
2249 called when a user has joined the conversation.
2251 @param userHandle: the user handle (passport) of the user
2252 @param screenName: the screen name of the user
2256 def userLeft(self
, userHandle
):
2258 called when a user has left the conversation.
2260 @param userHandle: the user handle (passport) of the user.
2264 def gotMessage(self
, message
):
2266 called when we receive a message.
2268 @param message: the associated MSNMessage object
2272 def gotFileReceive(self
, fileReceive
):
2274 called when we receive a file send request from a contact.
2275 Default action is to reject the file.
2277 @param fileReceive: msnft.MSNFTReceive_Base instance
2279 fileReceive
.reject()
2282 def gotSendRequest(self
, fileReceive
):
2284 called when we receive a file send request from a contact
2286 @param fileReceive: msnft.MSNFTReceive_Base instance
2290 def gotContactTyping(self
, message
):
2292 called when we receive the special type of message notifying
2293 us that a contact is typing a message.
2295 @param message: the associated MSNMessage object
2301 def inviteUser(self
, userHandle
):
2303 used to invite a user to the current switchboard server.
2305 @param userHandle: the user handle (passport) of the desired user.
2307 @return: A Deferred, the callback for which will be called
2308 when the server notifies us that the user has indeed
2309 been invited. The callback argument will be a tuple
2310 with 1 element, the sessionID given to the invited user.
2311 I'm not sure if this is useful or not.
2314 id, d
= self
._createIDMapping
()
2315 self
.sendLine("CAL %s %s" % (id, userHandle
))
2318 def sendMessage(self
, message
):
2320 used to send a message.
2322 @param message: the corresponding MSNMessage object.
2324 @return: Depending on the value of message.ack.
2325 If set to MSNMessage.MESSAGE_ACK or
2326 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2327 the callback for which will be fired when an ACK or
2328 NACK is received - the callback argument will be
2329 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2330 the return value is None.
2333 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2334 else: id, d
= self
._createIDMapping
()
2335 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2336 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2337 # Apparently order matters with these
2338 orderMatters
= ("MIME-Version", "Content-Type", "Message-ID")
2339 for header
in orderMatters
:
2340 if message
.hasHeader(header
):
2341 self
.sendLine("%s: %s" % (header
, message
.getHeader(header
)))
2342 # send the rest of the headers
2343 for header
in [h
for h
in message
.headers
.items() if h
[0] not in orderMatters
]:
2344 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2345 self
.transport
.write("\r\n")
2346 self
.transport
.write(message
.message
)
2347 if MESSAGEDEBUG
: log
.msg(message
.message
)
2350 def sendAvatarRequest(self
, msnContact
):
2352 used to request an avatar from a user in this switchboard
2355 @param msnContact: the msnContact object to request an avatar for
2357 @return: A Deferred, the callback for which will be called
2358 when the avatar transfer succeeds.
2359 The callback argument will be a tuple with one element,
2360 the PNG avatar data.
2362 if not msnContact
.msnobj
: return
2364 def bufferClosed(data
):
2366 buffer = StringBuffer(bufferClosed
)
2367 buffer.error
= lambda: None
2368 slpLink
= SLPLink_AvatarReceive(remoteUser
=msnContact
.userHandle
, switchboard
=self
, consumer
=buffer, context
=msnContact
.msnobj
.text
)
2369 self
.slpLinks
[slpLink
.sessionID
] = slpLink
2372 def sendFile(self
, msnContact
, filename
, filesize
):
2374 used to send a file to a contact.
2376 @param msnContact: the MSNContact object to send a file to.
2377 @param filename: the name of the file to send.
2378 @param filesize: the size of the file to send.
2380 @return: (fileSend, d) A FileSend object and a Deferred.
2381 The Deferred will pass one argument in a tuple,
2382 whether or not the transfer is accepted. If you
2383 receive a True, then you can call write() on the
2384 fileSend object to send your file. Call close()
2385 when the file is done.
2386 NOTE: You MUST write() exactly as much as you
2387 declare in filesize.
2389 if not msnContact
.userHandle
: return
2390 # FIXME, check msnContact.caps to see if we should use old-style
2391 fileSend
= SLPLink_FileSend(remoteUser
=msnContact
.userHandle
, switchboard
=self
, filename
=filename
, filesize
=filesize
)
2392 self
.slpLinks
[fileSend
.sessionID
] = fileSend
2393 return fileSend
, fileSend
.acceptDeferred
2395 def sendTypingNotification(self
):
2397 Used to send a typing notification. Upon receiving this
2398 message the official client will display a 'user is typing'
2399 message to all other users in the chat session for 10 seconds.
2400 You should send one of these every 5 seconds as long as the
2404 m
.ack
= m
.MESSAGE_ACK_NONE
2405 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2406 m
.setHeader('TypingUser', self
.userHandle
)
2410 def sendFileInvitation(self
, fileName
, fileSize
):
2412 send an notification that we want to send a file.
2414 @param fileName: the file name
2415 @param fileSize: the file size
2417 @return: A Deferred, the callback of which will be fired
2418 when the user responds to this invitation with an
2419 appropriate message. The callback argument will be
2420 a tuple with 3 elements, the first being 1 or 0
2421 depending on whether they accepted the transfer
2422 (1=yes, 0=no), the second being an invitation cookie
2423 to identify your follow-up responses and the third being
2424 the message 'info' which is a dict of information they
2425 sent in their reply (this doesn't really need to be used).
2426 If you wish to proceed with the transfer see the
2427 sendTransferInfo method.
2429 cookie
= self
._newInvitationCookie
()
2432 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2433 m
.message
+= 'Application-Name: File Transfer\r\n'
2434 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2435 m
.message
+= 'Invitation-Command: INVITE\r\n'
2436 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2437 m
.message
+= 'Application-File: %s\r\n' % fileName
2438 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2439 m
.ack
= m
.MESSAGE_ACK_NONE
2441 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2444 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2446 send information relating to a file transfer session.
2448 @param accept: whether or not to go ahead with the transfer
2450 @param iCookie: the invitation cookie of previous replies
2451 relating to this transfer
2452 @param authCookie: the authentication cookie obtained from
2453 an FileSend instance
2455 @param port: the port on which an FileSend protocol is listening.
2458 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2459 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2460 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2461 m
.message
+= 'IP-Address: %s\r\n' % ip
2462 m
.message
+= 'Port: %s\r\n' % port
2463 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2465 m
.ack
= m
.MESSAGE_NACK
2470 def __init__(self
, filename
, filesize
, userHandle
):
2471 self
.consumer
= None
2472 self
.finished
= False
2475 self
.filename
, self
.filesize
, self
.userHandle
= filename
, filesize
, userHandle
2478 raise NotImplementedError
2480 def accept(self
, consumer
):
2481 if self
.consumer
: raise "AlreadyAccepted"
2482 self
.consumer
= consumer
2483 for data
in self
.buffer:
2484 self
.consumer
.write(data
)
2487 self
.consumer
.close()
2489 self
.consumer
.error()
2491 def write(self
, data
):
2492 if self
.error
or self
.finished
:
2493 raise IOError, "Attempt to write in an invalid state"
2495 self
.consumer
.write(data
)
2497 self
.buffer.append(data
)
2500 self
.finished
= True
2502 self
.consumer
.close()
2505 """ Represents the Context field for P2P file transfers """
2506 def __init__(self
, data
=""):
2514 if MSNP2PDEBUG
: log
.msg("FileContext packing:", self
.filename
, self
.filesize
)
2515 data
= struct
.pack("<LLQL", 638, 0x03, self
.filesize
, 0x01)
2516 data
= data
[:-1] # Uck, weird, but it works
2517 data
+= utf16net(self
.filename
)
2518 data
= ljust(data
, 570, '\0')
2519 data
+= struct
.pack("<L", 0xFFFFFFFFL
)
2520 data
= ljust(data
, 638, '\0')
2523 def parse(self
, packet
):
2524 self
.filesize
= struct
.unpack("<Q", packet
[8:16])[0]
2525 chunk
= packet
[19:540]
2526 chunk
= chunk
[:chunk
.find('\x00\x00')]
2527 self
.filename
= unicode((codecs
.BOM_UTF16_BE
+ chunk
).decode("utf-16"))
2528 if MSNP2PDEBUG
: log
.msg("FileContext parsed:", self
.filesize
, self
.filename
)
2532 """ Utility class for the binary header & footer in p2p messages """
2541 def __init__(self
, fields
=None, packet
=None):
2543 self
.fields
= fields
2545 self
.fields
= [0] * 10
2547 self
.unpackFields(packet
)
2549 def __getitem__(self
, key
):
2550 return self
.fields
[key
]
2552 def __setitem__(self
, key
, value
):
2553 self
.fields
[key
] = value
2555 def unpackFields(self
, packet
):
2556 self
.fields
= struct
.unpack("<LLQQLLLLQ", packet
[0:48])
2557 self
.fields
+= struct
.unpack(">L", packet
[len(packet
)-4:])
2559 out
= "Unpacked fields: "
2560 for i
in self
.fields
:
2564 def packHeaders(self
):
2565 f
= tuple(self
.fields
)
2567 out
= "Packed fields: "
2568 for i
in self
.fields
:
2571 return struct
.pack("<LLQQLLLLQ", f
[0], f
[1], f
[2], f
[3], f
[4], f
[5], f
[6], f
[7], f
[8])
2573 def packFooter(self
):
2574 return struct
.pack(">L", self
.fields
[9])
2577 class MSNSLPMessage
:
2578 """ Representation of a single MSNSLP message """
2579 def __init__(self
, packet
=None):
2586 self
.sessionGuid
= ""
2587 self
.sessionID
= None
2589 self
.data
= "\r\n" + chr(0)
2593 def create(self
, method
=None, status
=None, to
=None, fro
=None, branch
=None, cseq
=0, sessionGuid
=None, data
=None):
2594 self
.method
= method
2595 self
.status
= status
2598 self
.branch
= branch
2600 self
.sessionGuid
= sessionGuid
2601 if data
: self
.data
= data
2603 def setData(self
, ctype
, data
):
2606 order
= ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
2608 if key
== "Context" and data
.has_key(key
):
2609 s
.append("Context: %s\r\n" % b64enc(data
[key
]))
2610 elif data
.has_key(key
):
2611 s
.append("%s: %s\r\n" % (key
, str(data
[key
])))
2612 s
.append("\r\n"+chr(0))
2614 self
.data
= "".join(s
)
2618 if s
.find("MSNSLP/1.0") < 0: return
2620 lines
= s
.split("\r\n")
2622 # Get the MSNSLP method or status
2623 msnslp
= lines
[0].split(" ")
2624 if MSNP2PDEBUG
: log
.msg("Parsing MSNSLPMessage %s %s" % (len(s
), s
))
2625 if msnslp
[0] in ("INVITE", "BYE"):
2626 self
.method
= msnslp
[0].strip()
2628 self
.status
= msnslp
[1].strip()
2630 lines
.remove(lines
[0])
2633 line
= line
.split(":")
2634 if len(line
) < 1: continue
2636 if len(line
) > 2 and line
[0] == "To":
2637 self
.to
= line
[2][:line
[2].find('>')]
2638 elif len(line
) > 2 and line
[0] == "From":
2639 self
.fro
= line
[2][:line
[2].find('>')]
2640 elif line
[0] == "Call-ID":
2641 self
.sessionGuid
= line
[1].strip()
2642 elif line
[0] == "CSeq":
2643 self
.cseq
= int(line
[1].strip())
2644 elif line
[0] == "SessionID":
2645 self
.sessionID
= int(line
[1].strip())
2646 elif line
[0] == "EUF-GUID":
2647 self
.euf_guid
= line
[1].strip()
2648 elif line
[0] == "Content-Type":
2649 self
.ctype
= line
[1].strip()
2650 elif line
[0] == "Context":
2651 self
.context
= b64dec(line
[1])
2652 elif line
[0] == "Via":
2653 self
.branch
= line
[1].split(";")[1].split("=")[1].strip()
2656 log
.msg("Error parsing MSNSLP message.")
2662 s
.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self
.method
, self
.to
))
2664 if self
.status
== "200": status
= "200 OK"
2665 elif self
.status
== "603": status
= "603 Decline"
2666 s
.append("MSNSLP/1.0 %s\r\n" % status
)
2667 s
.append("To: <msnmsgr:%s>\r\n" % self
.to
)
2668 s
.append("From: <msnmsgr:%s>\r\n" % self
.fro
)
2669 s
.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % self
.branch
)
2670 s
.append("CSeq: %s \r\n" % str(self
.cseq
))
2671 s
.append("Call-ID: %s\r\n" % self
.sessionGuid
)
2672 s
.append("Max-Forwards: 0\r\n")
2673 s
.append("Content-Type: %s\r\n" % self
.ctype
)
2674 s
.append("Content-Length: %s\r\n\r\n" % len(self
.data
))
2679 """ Utility for handling the weird sequence IDs in p2p messages """
2680 def __init__(self
, baseID
=None):
2682 self
.baseID
= baseID
2684 self
.baseID
= random
.randint(1000, sys
.maxint
)
2688 return p2pseq(self
.pos
) + self
.baseID
2695 class StringBuffer(StringIO
.StringIO
):
2696 def __init__(self
, notifyFunc
=None):
2697 self
.notifyFunc
= notifyFunc
2698 StringIO
.StringIO
.__init
__(self
)
2702 self
.notifyFunc(self
.getvalue())
2703 self
.notifyFunc
= None
2704 StringIO
.StringIO
.close(self
)
2708 def __init__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
):
2711 sessionID
= random
.randint(1000, sys
.maxint
)
2713 sessionGuid
= random_guid()
2714 self
.remoteUser
= remoteUser
2715 self
.switchboard
= switchboard
2716 self
.sessionID
= sessionID
2717 self
.sessionGuid
= sessionGuid
2718 self
.seqID
= SeqID()
2721 if MSNP2PDEBUG
: log
.msg("killLink")
2723 if MSNP2PDEBUG
: log
.msg("killLink - kill()")
2724 if not self
.switchboard
: return
2725 del self
.switchboard
.slpLinks
[self
.sessionID
]
2726 self
.switchboard
= None
2727 # This is so that handleP2PMessage can still use the SLPLink
2728 # one last time, for ACKing BYEs and 601s.
2729 reactor
.callLater(0, kill
)
2731 def warn(self
, text
):
2732 log
.msg("Warning in transfer: %s %s" % (self
, text
))
2734 def sendP2PACK(self
, ackHeaders
):
2735 binaryFields
= BinaryFields()
2736 binaryFields
[0] = ackHeaders
[0]
2737 binaryFields
[1] = self
.seqID
.next()
2738 binaryFields
[3] = ackHeaders
[3]
2739 binaryFields
[5] = BinaryFields
.ACK
2740 binaryFields
[6] = ackHeaders
[1]
2741 binaryFields
[7] = ackHeaders
[6]
2742 binaryFields
[8] = ackHeaders
[3]
2743 self
.sendP2PMessage(binaryFields
, "")
2745 def sendSLPMessage(self
, cmd
, ctype
, data
, branch
=None):
2746 msg
= MSNSLPMessage()
2748 msg
.create(status
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=branch
, cseq
=1, sessionGuid
=self
.sessionGuid
)
2750 msg
.create(method
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=random_guid(), cseq
=0, sessionGuid
=self
.sessionGuid
)
2751 msg
.setData(ctype
, data
)
2753 binaryFields
= BinaryFields()
2754 binaryFields
[1] = self
.seqID
.next()
2755 binaryFields
[3] = len(msgStr
)
2756 binaryFields
[4] = binaryFields
[3]
2757 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2758 self
.sendP2PMessage(binaryFields
, msgStr
)
2760 def sendP2PMessage(self
, binaryFields
, msgStr
):
2761 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2763 message
= MSNMessage(message
=packet
)
2764 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2765 message
.setHeader("P2P-Dest", self
.remoteUser
)
2766 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2767 self
.switchboard
.sendMessage(message
)
2769 def handleSLPMessage(self
, slpMessage
):
2770 raise NotImplementedError
2776 class SLPLink_Send(SLPLink
):
2777 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2778 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2779 self
.handlePacket
= None
2781 self
.filesize
= filesize
2784 def send_dataprep(self
):
2785 if MSNP2PDEBUG
: log
.msg("send_dataprep")
2786 binaryFields
= BinaryFields()
2787 binaryFields
[0] = self
.sessionID
2788 binaryFields
[1] = self
.seqID
.next()
2791 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2793 self
.sendP2PMessage(binaryFields
, chr(0) * 4)
2795 def write(self
, data
):
2796 if MSNP2PDEBUG
: log
.msg("write")
2798 data
= self
.data
+ data
2802 if i
+ 1202 < length
:
2803 self
._writeChunk
(data
[i
:i
+1202])
2806 self
.data
= data
[i
:]
2809 def _writeChunk(self
, chunk
):
2810 if MSNP2PDEBUG
: log
.msg("writing chunk")
2811 binaryFields
= BinaryFields()
2812 binaryFields
[0] = self
.sessionID
2813 if self
.offset
== 0:
2814 binaryFields
[1] = self
.seqID
.next()
2816 binaryFields
[1] = self
.seqID
.get()
2817 binaryFields
[2] = self
.offset
2818 binaryFields
[3] = self
.filesize
2819 binaryFields
[4] = len(chunk
)
2820 binaryFields
[5] = self
.dataFlag
2821 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2823 self
.offset
+= len(chunk
)
2824 self
.sendP2PMessage(binaryFields
, chunk
)
2828 self
._writeChunk
(self
.data
)
2833 # FIXME, should send 601 or something
2835 class SLPLink_FileSend(SLPLink_Send
):
2836 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
):
2837 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
)
2838 self
.dataFlag
= BinaryFields
.DATAFT
2839 # Send invite & wait for 200OK before sending dataprep
2840 context
= FileContext()
2841 context
.filename
= filename
2842 context
.filesize
= filesize
2843 data
= {"EUF-GUID" : MSN_MSNFTP_GUID
,\
2844 "SessionID": self
.sessionID
,\
2846 "Context" : context
.pack() }
2847 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2848 self
.acceptDeferred
= Deferred()
2850 def handleSLPMessage(self
, slpMessage
):
2851 if slpMessage
.status
== "200":
2852 if slpMessage
.ctype
== "application/x-msnmsgr-sessionreqbody":
2853 data
= {"Bridges" : "TRUDPv1 TCPv1",\
2855 "Conn-Type" : "Firewall",\
2856 "UPnPNat" : "false",\
2858 #"Hashed-Nonce": random_guid()}
2859 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data
)
2860 elif slpMessage
.ctype
== "application/x-msnmsgr-transrespbody":
2861 self
.acceptDeferred
.callback((True,))
2862 self
.handlePacket
= self
.wait_data_ack
2864 if slpMessage
.status
== "603":
2865 self
.acceptDeferred
.callback((False,))
2866 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to decline, error or BYE")
2870 def wait_data_ack(self
, packet
):
2871 if MSNP2PDEBUG
: log
.msg("wait_data_ack")
2872 binaryFields
= BinaryFields()
2873 binaryFields
.unpackFields(packet
)
2875 if binaryFields
[5] != BinaryFields
.ACK
:
2876 self
.warn("field5," + str(binaryFields
[5]))
2879 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2880 self
.handlePacket
= None
2883 self
.handlePacket
= self
.wait_data_ack
2884 SLPLink_Send
.close(self
)
2887 class SLPLink_AvatarSend(SLPLink_Send
):
2888 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2889 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2890 self
.dataFlag
= BinaryFields
.DATA
2891 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2892 self
.send_dataprep()
2893 self
.handlePacket
= lambda packet
: None
2895 def handleSLPMessage(self
, slpMessage
):
2896 if MSNP2PDEBUG
: log
.msg("BYE or error")
2900 SLPLink_Send
.close(self
)
2901 # Keep the link open to wait for a BYE
2903 class SLPLink_Receive(SLPLink
):
2904 def __init__(self
, remoteUser
, switchboard
, consumer
, context
=None, sessionID
=None, sessionGuid
=None):
2905 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2906 self
.handlePacket
= None
2907 self
.consumer
= consumer
2910 def wait_dataprep(self
, packet
):
2911 if MSNP2PDEBUG
: log
.msg("wait_dataprep")
2912 binaryFields
= BinaryFields()
2913 binaryFields
.unpackFields(packet
)
2915 if binaryFields
[3] != 4:
2916 self
.warn("field3," + str(binaryFields
[3]))
2918 if binaryFields
[4] != 4:
2919 self
.warn("field4," + str(binaryFields
[4]))
2921 # Just ignore the footer
2922 #if binaryFields[9] != 1:
2923 # self.warn("field9," + str(binaryFields[9]))
2926 self
.sendP2PACK(binaryFields
)
2927 self
.handlePacket
= self
.wait_data
2929 def wait_data(self
, packet
):
2930 if MSNP2PDEBUG
: log
.msg("wait_data")
2931 binaryFields
= BinaryFields()
2932 binaryFields
.unpackFields(packet
)
2934 if binaryFields
[5] != self
.dataFlag
:
2935 self
.warn("field5," + str(binaryFields
[5]))
2937 # Just ignore the footer
2938 #if binaryFields[9] != 1:
2939 # self.warn("field9," + str(binaryFields[9]))
2941 offset
= binaryFields
[2]
2942 total
= binaryFields
[3]
2943 length
= binaryFields
[4]
2945 data
= packet
[48:-4]
2946 if offset
!= self
.pos
:
2947 self
.warn("Received packet out of order")
2948 self
.consumer
.error()
2950 if len(data
) != length
:
2951 self
.warn("Received bad length of slp")
2952 self
.consumer
.error()
2957 self
.consumer
.write(str(data
))
2959 if self
.pos
== total
:
2960 self
.sendP2PACK(binaryFields
)
2961 self
.consumer
.close()
2962 self
.handlePacket
= None
2965 def doFinished(self
):
2966 raise NotImplementedError
2969 class SLPLink_FileReceive(SLPLink_Receive
, FileReceive
):
2970 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
, sessionID
, sessionGuid
, branch
):
2971 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=self
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2972 self
.dataFlag
= BinaryFields
.DATAFT
2973 self
.initialBranch
= branch
2974 FileReceive
.__init
__(self
, filename
, filesize
, remoteUser
)
2977 # Send a 603 decline
2978 if not self
.switchboard
: return
2979 self
.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2982 def accept(self
, consumer
):
2983 FileReceive
.accept(self
, consumer
)
2984 if not self
.switchboard
: return
2985 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2986 self
.handlePacket
= self
.wait_data
# Moved here because sometimes the second INVITE seems to be skipped
2988 def handleSLPMessage(self
, slpMessage
):
2989 if slpMessage
.method
== "INVITE": # The second invite
2990 data
= {"Bridge" : "TCPv1",\
2991 "Listening" : "false",\
2992 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
2993 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
2994 # self.handlePacket = self.wait_data # Moved up
2996 if MSNP2PDEBUG
: log
.msg("It's either a BYE or an error")
2998 # FIXME, do some error handling if it was an error
3000 def doFinished(self
):
3001 #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3003 # Wait for BYE? #FIXME
3006 class SLPLink_AvatarReceive(SLPLink_Receive
):
3007 def __init__(self
, remoteUser
, switchboard
, consumer
, context
):
3008 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=consumer
, context
=context
)
3009 self
.dataFlag
= BinaryFields
.DATA
3010 data
= {"EUF-GUID" : MSN_AVATAR_GUID
,\
3011 "SessionID": self
.sessionID
,\
3013 "Context" : context
}
3014 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
3015 self
.handlePacket
= self
.wait_dataprep
3017 def handleSLPMessage(self
, slpMessage
):
3018 if slpMessage
.method
== "INVITE": # The second invite
3019 data
= {"Bridge" : "TCPv1",\
3020 "Listening" : "false",\
3021 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
3022 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
3023 elif slpMessage
.status
== "200":
3025 #self.handlePacket = self.wait_dataprep # Moved upwards
3027 if MSNP2PDEBUG
: log
.msg("SLPLink is over due to error or BYE")
3030 def doFinished(self
):
3031 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3033 # mapping of error codes to error messages
3036 200 : "Syntax error",
3037 201 : "Invalid parameter",
3038 205 : "Invalid user",
3039 206 : "Domain name missing",
3040 207 : "Already logged in",
3041 208 : "Invalid username",
3042 209 : "Invalid screen name",
3043 210 : "User list full",
3044 215 : "User already there",
3045 216 : "User already on list",
3046 217 : "User not online",
3047 218 : "Already in mode",
3048 219 : "User is in the opposite list",
3049 223 : "Too many groups",
3050 224 : "Invalid group",
3051 225 : "User not in group",
3052 229 : "Group name too long",
3053 230 : "Cannot remove group 0",
3054 231 : "Invalid group",
3055 280 : "Switchboard failed",
3056 281 : "Transfer to switchboard failed",
3058 300 : "Required field missing",
3059 301 : "Too many FND responses",
3060 302 : "Not logged in",
3062 400 : "Message not allowed",
3063 402 : "Error accessing contact list",
3064 403 : "Error accessing contact list",
3066 500 : "Internal server error",
3067 501 : "Database server error",
3068 502 : "Command disabled",
3069 510 : "File operation failed",
3070 520 : "Memory allocation failed",
3071 540 : "Wrong CHL value sent to server",
3073 600 : "Server is busy",
3074 601 : "Server is unavaliable",
3075 602 : "Peer nameserver is down",
3076 603 : "Database connection failed",
3077 604 : "Server is going down",
3078 605 : "Server unavailable",
3080 707 : "Could not create connection",
3081 710 : "Invalid CVR parameters",
3082 711 : "Write is blocking",
3083 712 : "Session is overloaded",
3084 713 : "Too many active users",
3085 714 : "Too many sessions",
3086 715 : "Not expected",
3087 717 : "Bad friend file",
3088 731 : "Not expected",
3090 800 : "Requests too rapid",
3092 910 : "Server too busy",
3093 911 : "Authentication failed",
3094 912 : "Server too busy",
3095 913 : "Not allowed when offline",
3096 914 : "Server too busy",
3097 915 : "Server too busy",
3098 916 : "Server too busy",
3099 917 : "Server too busy",
3100 918 : "Server too busy",
3101 919 : "Server too busy",
3102 920 : "Not accepting new users",
3103 921 : "Server too busy",
3104 922 : "Server too busy",
3105 923 : "No parent consent",
3106 924 : "Passport account not yet verified"
3110 # mapping of status codes to readable status format
3113 STATUS_ONLINE
: "Online",
3114 STATUS_OFFLINE
: "Offline",
3115 STATUS_HIDDEN
: "Appear Offline",
3116 STATUS_IDLE
: "Idle",
3117 STATUS_AWAY
: "Away",
3118 STATUS_BUSY
: "Busy",
3119 STATUS_BRB
: "Be Right Back",
3120 STATUS_PHONE
: "On the Phone",
3121 STATUS_LUNCH
: "Out to Lunch"
3125 # mapping of list ids to list codes
3128 FORWARD_LIST
: 'fl',
3131 REVERSE_LIST
: 'rl',
3136 # mapping of list codes to list ids
3138 for id,code
in listIDToCode
.items():
3139 listCodeToID
[code
] = id