1 # Twisted, the Framework of Your Internet
2 # Copyright (C) 2001-2002 Matthew W. Lefkowitz
3 # Copyright (C) 2004-2005 James C. Bunton
5 # This library is free software; you can redistribute it and/or
6 # modify it under the terms of version 2.1 of the GNU Lesser General Public
7 # License as published by the Free Software Foundation.
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 MSNP11 Protocol (client only) - semi-experimental
24 This module provides support for clients using the MSN Protocol (MSNP11).
25 There are basically 3 servers involved in any MSN session:
29 The DispatchClient class handles connections to the
30 dispatch server, which basically delegates users to a
31 suitable notification server.
33 You will want to subclass this and handle the gotNotificationReferral
36 I{Notification Server}
38 The NotificationClient class handles connections to the
39 notification server, which acts as a session server
40 (state updates, message negotiation etc...)
44 The SwitchboardClient handles connections to switchboard
45 servers which are used to conduct conversations with other users.
47 There are also two classes (FileSend and FileReceive) used
50 Clients handle events in two ways.
52 - each client request requiring a response will return a Deferred,
53 the callback for same will be fired when the server sends the
55 - Events which are not in response to any client request have
56 respective methods which should be overridden and handled in
59 Most client request callbacks require more than one argument,
60 and since Deferreds can only pass the callback one result,
61 most of the time the callback argument will be a tuple of
62 values (documented in the respective request method).
63 To make reading/writing code easier, callbacks can be defined in
64 a number of ways to handle this 'cleanly'. One way would be to
65 define methods like: def callBack(self, (arg1, arg2, arg)): ...
66 another way would be to do something like:
67 d.addCallback(lambda result: myCallback(*result)).
69 If the server sends an error response to a client request,
70 the errback of the corresponding Deferred will be called,
71 the argument being the corresponding error code.
74 Due to the lack of an official spec for MSNP11, extra checking
75 than may be deemed necessary often takes place considering the
76 server is never 'wrong'. Thus, if gotBadLine (in any of the 3
77 main clients) is called, or an MSNProtocolError is raised, it's
78 probably a good idea to submit a bug report. ;)
79 Use of this module requires that PyOpenSSL is installed.
81 @author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
82 @author: U{James Bunton<mailto:james@delx.cjb.net>}
85 from __future__
import nested_scopes
88 from twisted
.protocols
.basic
import LineReceiver
90 from twisted
.web
.http
import HTTPClient
93 from twisted
.protocols
.http
import HTTPClient
95 print "Couldn't find a HTTPClient. If you're using Twisted 2.0 make sure you've installed twisted.web"
100 from twisted
.internet
import reactor
, task
101 from twisted
.internet
.defer
import Deferred
102 from twisted
.internet
.protocol
import ClientFactory
103 from twisted
.internet
.ssl
import ClientContextFactory
104 from twisted
.python
import failure
, log
105 from twisted
.xish
.domish
import unescapeFromXml
108 from tlib
import xmlw
111 import types
, operator
, os
, sys
, base64
, random
, struct
, random
, sha
, base64
, StringIO
112 from urllib
import quote
, unquote
115 MSN_PROTOCOL_VERSION
= "MSNP11 CVR0" # protocol version
116 MSN_PORT
= 1863 # default dispatch server port
117 MSN_MAX_MESSAGE
= 1664 # max message length
118 MSN_CVR_STR
= "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
119 MSN_AVATAR_GUID
= "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
120 MSN_MSNFTP_GUID
= "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
142 STATUS_ONLINE
= 'NLN'
143 STATUS_OFFLINE
= 'FLN'
144 STATUS_HIDDEN
= 'HDN'
165 return inp
.split('=')[1]
177 userHandle
= getVal(p
)
179 screenName
= unquote(getVal(p
))
184 else: # Must be the groups
186 groups
= p
.split(',')
188 raise MSNProtocolError
, "Unknown LST/ADC response" + str(params
) # debug
190 return userHandle
, screenName
, userGuid
, lists
, groups
193 return base64
.encodestring(s
).replace("\n", "")
196 format
= "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
199 data
.append(random
.random() * 0xAAFF + 0x1111)
204 def checkParamLen(num
, expected
, cmd
, error
=None):
205 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
206 if num
!= expected
: raise MSNProtocolError
, error
208 def _parseHeader(h
, v
):
210 Split a certin number of known
211 header values with the format:
212 field1=val,field2=val,field3=val into
213 a dict mapping fields to values.
214 @param h: the header's key
215 @param v: the header's value as a string
218 if h
in ('passporturls','authentication-info','www-authenticate'):
219 v
= v
.replace('Passport1.4','').lstrip()
221 for fieldPair
in v
.split(','):
223 field
,value
= fieldPair
.split('=',1)
224 fields
[field
.lower()] = value
226 fields
[field
.lower()] = ''
230 def _parsePrimitiveHost(host
):
232 h
,p
= host
.replace('https://','').split('/',1)
236 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
=''):
238 This function is used internally and should not ever be called
242 def _cb(server
, auth
):
243 loginFac
= ClientFactory()
244 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
245 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
248 _cb(nexusServer
, authData
)
250 fac
= ClientFactory()
252 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
253 d
.addErrback(lambda f
: cb
.errback(f
))
254 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
255 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
259 class PassportNexus(HTTPClient
):
262 Used to obtain the URL of a valid passport
265 This class is used internally and should
266 not be instantiated directly -- that is,
267 The passport logging in process is handled
268 transparantly by NotificationClient.
271 def __init__(self
, deferred
, host
):
272 self
.deferred
= deferred
273 self
.host
, self
.path
= _parsePrimitiveHost(host
)
275 def connectionMade(self
):
276 HTTPClient
.connectionMade(self
)
277 self
.sendCommand('GET', self
.path
)
278 self
.sendHeader('Host', self
.host
)
282 def handleHeader(self
, header
, value
):
284 self
.headers
[h
] = _parseHeader(h
, value
)
286 def handleEndHeaders(self
):
287 if self
.connected
: self
.transport
.loseConnection()
288 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
289 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
291 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
293 def handleResponse(self
, r
): pass
295 class PassportLogin(HTTPClient
):
297 This class is used internally to obtain
298 a login ticket from a passport HTTPS
299 server -- it should not be used directly.
304 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
305 self
.deferred
= deferred
306 self
.userHandle
= userHandle
308 self
.authData
= authData
309 self
.host
, self
.path
= _parsePrimitiveHost(host
)
311 def connectionMade(self
):
312 self
.sendCommand('GET', self
.path
)
313 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
314 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), self
.passwd
,self
.authData
))
315 self
.sendHeader('Host', self
.host
)
319 def handleHeader(self
, header
, value
):
321 self
.headers
[h
] = _parseHeader(h
, value
)
323 def handleEndHeaders(self
):
324 if self
._finished
: return
325 self
._finished
= 1 # I think we need this because of HTTPClient
326 if self
.connected
: self
.transport
.loseConnection()
327 authHeader
= 'authentication-info'
328 _interHeader
= 'www-authenticate'
329 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
331 info
= self
.headers
[authHeader
]
332 status
= info
['da-status']
333 handler
= getattr(self
, 'login_%s' % (status
,), None)
336 else: raise Exception()
338 self
.deferred
.errback(failure
.Failure(e
))
340 def handleResponse(self
, r
): pass
342 def login_success(self
, info
):
343 ticket
= info
['from-pp']
344 ticket
= ticket
[1:len(ticket
)-1]
345 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
347 def login_failed(self
, info
):
348 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
350 def login_redir(self
, info
):
351 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
353 class MSNProtocolError(Exception):
355 This Exception is basically used for debugging
356 purposes, as the official MSN server should never
357 send anything _wrong_ and nobody in their right
358 mind would run their B{own} MSN server.
359 If it is raised by default command handlers
360 (handle_BLAH) the error will be logged.
367 I am the class used to represent an 'instant' message.
369 @ivar userHandle: The user handle (passport) of the sender
370 (this is only used when receiving a message)
371 @ivar screenName: The screen name of the sender (this is only used
372 when receiving a message)
373 @ivar message: The message
374 @ivar headers: The message headers
376 @ivar length: The message length (including headers and line endings)
377 @ivar ack: This variable is used to tell the server how to respond
378 once the message has been sent. If set to MESSAGE_ACK
379 (default) the server will respond with an ACK upon receiving
380 the message, if set to MESSAGE_NACK the server will respond
381 with a NACK upon failure to receive the message.
382 If set to MESSAGE_ACK_NONE the server will do nothing.
383 This is relevant for the return value of
384 SwitchboardClient.sendMessage (which will return
385 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
386 and will fire when the respective ACK or NACK is received).
387 If set to MESSAGE_ACK_NONE sendMessage will return None.
390 MESSAGE_ACK_FAT
= 'D'
392 MESSAGE_ACK_NONE
= 'U'
396 def __init__(self
, length
=0, userHandle
="", screenName
="", message
="", specialMessage
=False):
397 self
.userHandle
= userHandle
398 self
.screenName
= screenName
399 self
.specialMessage
= specialMessage
400 self
.message
= message
401 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
405 def _calcMessageLen(self
):
407 used to calculte the number to send
408 as the message length when sending a message.
410 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
412 def setHeader(self
, header
, value
):
413 """ set the desired header """
414 self
.headers
[header
] = value
416 def getHeader(self
, header
):
418 get the desired header value
419 @raise KeyError: if no such header exists.
421 return self
.headers
[header
]
423 def hasHeader(self
, header
):
424 """ check to see if the desired header exists """
425 return self
.headers
.has_key(header
)
427 def getMessage(self
):
428 """ return the message - not including headers """
431 def setMessage(self
, message
):
432 """ set the message text """
433 self
.message
= message
438 Used to represent a MSNObject. This can be currently only be an avatar.
440 @ivar creator: The userHandle of the creator of this picture.
441 @ivar imageData: The PNG image data (only for our own avatar)
442 @ivar type: Always set to 3, for avatar.
443 @ivar size: The size of the image.
444 @ivar location: The filename of the image.
445 @ivar friendly: Unknown.
446 @ivar text: The textual representation of this MSNObject.
448 def __init__(self
, s
=""):
449 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
455 def setData(self
, creator
, imageData
):
456 """ Set the creator and imageData for this object """
457 self
.creator
= creator
458 self
.imageData
= imageData
459 self
.size
= len(imageData
)
461 self
.location
= "TMP" + str(random
.randint(1000,9999))
462 self
.friendly
= "AAA="
463 self
.sha1d
= b64enc(sha
.sha(imageData
).digest())
477 """ Makes a textual representation of this MSNObject. Stores it in self.text """
480 h
.append(self
.creator
)
482 h
.append(str(self
.size
))
484 h
.append(str(self
.type))
486 h
.append(self
.location
)
488 h
.append(self
.friendly
)
491 sha1c
= b64enc(sha
.sha("".join(h
)).digest())
492 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
)
495 e
= xmlw
.parseText(s
, True)
496 self
.creator
= e
.getAttribute("Creator")
497 self
.size
= int(e
.getAttribute("Size"))
498 self
.type = int(e
.getAttribute("Type"))
499 self
.location
= e
.getAttribute("Location")
500 self
.friendly
= e
.getAttribute("Friendly")
501 self
.sha1d
= e
.getAttribute("SHA1D")
508 This class represents a contact (user).
510 @ivar userGuid: The contact's user guid (unique string)
511 @ivar userHandle: The contact's user handle (passport).
512 @ivar screenName: The contact's screen name.
513 @ivar groups: A list of all the group IDs which this
515 @ivar lists: An integer representing the sum of all lists
516 that this contact belongs to.
517 @ivar caps: int, The capabilities of this client
518 @ivar msnobj: The MSNObject representing the contact's avatar
519 @ivar status: The contact's status code.
520 @type status: str if contact's status is known, None otherwise.
521 @ivar personal: The contact's personal message .
522 @type personal: str if contact's personal message is known, None otherwise.
524 @ivar homePhone: The contact's home phone number.
525 @type homePhone: str if known, otherwise None.
526 @ivar workPhone: The contact's work phone number.
527 @type workPhone: str if known, otherwise None.
528 @ivar mobilePhone: The contact's mobile phone number.
529 @type mobilePhone: str if known, otherwise None.
530 @ivar hasPager: Whether or not this user has a mobile pager
531 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
539 def __init__(self
, userGuid
="", userHandle
="", screenName
="", lists
=0, caps
=0, msnobj
=None, groups
={}, status
=None, personal
=""):
540 self
.userGuid
= userGuid
541 self
.userHandle
= userHandle
542 self
.screenName
= screenName
546 self
.msnobjGot
= True
547 self
.groups
= [] # if applicable
548 self
.status
= status
# current status
549 self
.personal
= personal
552 self
.homePhone
= None
553 self
.workPhone
= None
554 self
.mobilePhone
= None
558 def setPhone(self
, phoneType
, value
):
560 set phone numbers/values for this specific user.
561 for phoneType check the *_PHONE constants and HAS_PAGER
564 t
= phoneType
.upper()
565 if t
== HOME_PHONE
: self
.homePhone
= value
566 elif t
== WORK_PHONE
: self
.workPhone
= value
567 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
568 elif t
== HAS_PAGER
: self
.hasPager
= value
569 elif t
== HAS_BLOG
: self
.hasBlog
= value
570 #else: raise ValueError, "Invalid Phone Type: " + t
572 def addToList(self
, listType
):
574 Update the lists attribute to
575 reflect being part of the
578 self
.lists |
= listType
580 def removeFromList(self
, listType
):
582 Update the lists attribute to
583 reflect being removed from the
586 self
.lists ^
= listType
588 class MSNContactList
:
590 This class represents a basic MSN contact list.
592 @ivar contacts: All contacts on my various lists
593 @type contacts: dict (mapping user handles to MSNContact objects)
594 @ivar groups: a mapping of group ids to group names
595 (groups can only exist on the forward list)
599 This is used only for storage and doesn't effect the
600 server's contact list.
610 def _getContactsFromList(self
, listType
):
612 Obtain all contacts which belong
613 to the given list type.
615 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
617 def addContact(self
, contact
):
621 self
.contacts
[contact
.userHandle
] = contact
623 def remContact(self
, userHandle
):
628 del self
.contacts
[userHandle
]
629 except KeyError: pass
631 def getContact(self
, userHandle
):
633 Obtain the MSNContact object
634 associated with the given
636 @return: the MSNContact object if
637 the user exists, or None.
640 return self
.contacts
[userHandle
]
644 def getBlockedContacts(self
):
646 Obtain all the contacts on my block list
648 return self
._getContactsFromList
(BLOCK_LIST
)
650 def getAuthorizedContacts(self
):
652 Obtain all the contacts on my auth list.
653 (These are contacts which I have verified
654 can view my state changes).
656 return self
._getContactsFromList
(ALLOW_LIST
)
658 def getReverseContacts(self
):
660 Get all contacts on my reverse list.
661 (These are contacts which have added me
662 to their forward list).
664 return self
._getContactsFromList
(REVERSE_LIST
)
666 def getContacts(self
):
668 Get all contacts on my forward list.
669 (These are the contacts which I have added
672 return self
._getContactsFromList
(FORWARD_LIST
)
674 def setGroup(self
, id, name
):
676 Keep a mapping from the given id
679 self
.groups
[id] = name
681 def remGroup(self
, id):
683 Removed the stored group
684 mapping for the given id.
688 except KeyError: pass
689 for c
in self
.contacts
:
690 if id in c
.groups
: c
.groups
.remove(id)
693 class MSNEventBase(LineReceiver
):
695 This class provides support for handling / dispatching events and is the
696 base class of the three main client protocols (DispatchClient,
697 NotificationClient, SwitchboardClient)
701 self
.ids
= {} # mapping of ids to Deferreds
705 self
.currentMessage
= None
707 def connectionLost(self
, reason
):
711 def connectionMade(self
):
714 def _fireCallback(self
, id, *args
):
716 Fire the callback for the given id
717 if one exists and return 1, else return false
719 if self
.ids
.has_key(id):
720 self
.ids
[id][0].callback(args
)
725 def _nextTransactionID(self
):
726 """ return a usable transaction ID """
728 if self
.currentID
> 1000: self
.currentID
= 1
729 return self
.currentID
731 def _createIDMapping(self
, data
=None):
733 return a unique transaction ID that is mapped internally to a
734 deferred .. also store arbitrary data if it is needed
736 id = self
._nextTransactionID
()
738 self
.ids
[id] = (d
, data
)
741 def checkMessage(self
, message
):
743 process received messages to check for file invitations and
744 typing notifications and other control type messages
746 raise NotImplementedError
748 def sendLine(self
, line
):
749 if LINEDEBUG
: log
.msg(">> " + line
)
750 LineReceiver
.sendLine(self
, line
)
752 def lineReceived(self
, line
):
753 if LINEDEBUG
: log
.msg("<< " + line
)
754 if self
.currentMessage
:
755 self
.currentMessage
.readPos
+= len(line
+"\r\n")
757 header
, value
= line
.split(':')
758 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
761 #raise MSNProtocolError, "Invalid Message Header"
763 if line
== "" or self
.currentMessage
.specialMessage
:
765 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
768 cmd
, params
= line
.split(' ', 1)
770 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
772 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
774 if self
.ids
.has_key(params
.split(' ')[0]):
775 self
.ids
[id].errback(int(cmd
))
778 else: # we received an error which doesn't map to a sent command
779 self
.gotError(int(cmd
))
782 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
784 try: handler(params
.split(' '))
785 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
787 self
.handle_UNKNOWN(cmd
, params
.split(' '))
789 def rawDataReceived(self
, data
):
791 self
.currentMessage
.readPos
+= len(data
)
792 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
794 self
.currentMessage
.message
+= data
[:-diff
]
797 self
.currentMessage
.message
+= data
799 self
.currentMessage
.message
+= data
801 del self
.currentMessage
.readPos
802 m
= self
.currentMessage
803 self
.currentMessage
= None
804 if MESSAGEDEBUG
: log
.msg(m
.message
)
805 if not self
.checkMessage(m
):
806 self
.setLineMode(extra
)
809 self
.setLineMode(extra
)
811 ### protocol command handlers - no need to override these.
813 def handle_MSG(self
, params
):
814 checkParamLen(len(params
), 3, 'MSG')
816 messageLen
= int(params
[2])
817 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
818 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
820 def handle_UNKNOWN(self
, cmd
, params
):
821 """ implement me in subclasses if you want to handle unknown events """
822 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
826 def gotBadLine(self
, line
, why
):
827 """ called when a handler notifies me that this line is broken """
828 log
.msg('Error in line: %s (%s)' % (line
, why
))
830 def gotError(self
, errorCode
):
832 called when the server sends an error which is not in
833 response to a sent command (ie. it has no matching transaction ID)
835 log
.msg('Error %s' % (errorCodes
[errorCode
]))
837 class DispatchClient(MSNEventBase
):
839 This class provides support for clients connecting to the dispatch server
840 @ivar userHandle: your user handle (passport) needed before connecting.
843 # eventually this may become an attribute of the
847 def connectionMade(self
):
848 MSNEventBase
.connectionMade(self
)
849 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
851 ### protocol command handlers ( there is no need to override these )
853 def handle_VER(self
, params
):
854 versions
= params
[1:]
855 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
856 self
.transport
.loseConnection()
857 raise MSNProtocolError
, "Invalid version response"
858 id = self
._nextTransactionID
()
859 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.userHandle
))
861 def handle_CVR(self
, params
):
862 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.userHandle
))
864 def handle_XFR(self
, params
):
865 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
866 id, refType
, addr
= params
[:3]
867 # was addr a host:port pair?
869 host
, port
= addr
.split(':')
874 self
.gotNotificationReferral(host
, int(port
))
878 def gotNotificationReferral(self
, host
, port
):
880 called when we get a referral to the notification server.
882 @param host: the notification server's hostname
883 @param port: the port to connect to
888 class NotificationClient(MSNEventBase
):
890 This class provides support for clients connecting
891 to the notification server.
894 factory
= None # sssh pychecker
896 def __init__(self
, currentID
=0):
897 MSNEventBase
.__init
__(self
)
898 self
.currentID
= currentID
899 self
._state
= ['DISCONNECTED', {}]
901 self
.pingCheckTask
= None
902 self
.msnobj
= MSNObject()
904 def _setState(self
, state
):
905 self
._state
[0] = state
908 return self
._state
[0]
910 def _getStateData(self
, key
):
911 return self
._state
[1][key
]
913 def _setStateData(self
, key
, value
):
914 self
._state
[1][key
] = value
916 def _remStateData(self
, *args
):
917 for key
in args
: del self
._state
[1][key
]
919 def connectionMade(self
):
920 MSNEventBase
.connectionMade(self
)
921 self
._setState
('CONNECTED')
922 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
924 def connectionLost(self
, reason
):
925 self
._setState
('DISCONNECTED')
927 if self
.pingCheckTask
:
928 self
.pingCheckTask
.stop()
929 self
.pingCheckTask
= None
930 MSNEventBase
.connectionLost(self
, reason
)
932 def _getEmailFields(self
, message
):
933 fields
= message
.getMessage().strip().split('\n')
937 if len(a
) != 2: continue
944 def _gotInitialEmailNotification(self
, message
):
945 values
= self
._getEmailFields
(message
)
947 inboxunread
= int(values
["Inbox-Unread"])
948 foldersunread
= int(values
["Folders-Unread"])
951 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
952 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
954 def _gotEmailNotification(self
, message
):
955 values
= self
._getEmailFields
(message
)
957 mailfrom
= values
["From"]
958 fromaddr
= values
["From-Addr"]
959 subject
= values
["Subject"]
960 junkbeginning
= "=?\"us-ascii\"?Q?"
962 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
964 # If any of the fields weren't found then it's not a big problem. We just ignore the message
966 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
968 def _gotMSNAlert(self
, message
):
969 notification
= xmlw
.parseText(message
.message
, beExtremelyLenient
=True)
970 siteurl
= notification
.getAttribute("siteurl")
971 notid
= notification
.getAttribute("id")
974 for e
in notification
.elements():
980 msgid
= msg
.getAttribute("id")
985 for e
in msg
.elements():
986 if e
.name
== "ACTION":
987 action
= e
.getAttribute("url")
988 if e
.name
== "SUBSCR":
989 subscr
= e
.getAttribute("url")
991 for e2
in e
.elements():
992 if e2
.name
== "TEXT":
993 bodytext
= e2
.__str__()
994 if not (action
and subscr
and bodytext
): return
996 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
997 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
999 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
1001 def _gotUBX(self
, message
):
1002 lm
= message
.message
.lower()
1003 p1
= lm
.find("<psm>") + 5
1004 p2
= lm
.find("</psm>")
1005 if p1
>= 0 and p2
>= 0:
1006 personal
= unescapeFromXml(message
.message
[p1
:p2
])
1007 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
1008 if not msnContact
: return
1009 msnContact
.personal
= personal
1010 self
.contactPersonalChanged(message
.userHandle
, personal
)
1012 self
.contactPersonalChanged(message
.userHandle
, '')
1014 def checkMessage(self
, message
):
1015 """ hook used for detecting specific notification messages """
1016 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
1017 if 'text/x-msmsgsprofile' in cTypes
:
1018 self
.gotProfile(message
)
1020 elif "text/x-msmsgsinitialemailnotification" in cTypes
:
1021 self
._gotInitialEmailNotification
(message
)
1023 elif "text/x-msmsgsemailnotification" in cTypes
:
1024 self
._gotEmailNotification
(message
)
1026 elif "NOTIFICATION" == message
.userHandle
and message
.specialMessage
== True:
1027 self
._gotMSNAlert
(message
)
1029 elif "UBX" == message
.screenName
and message
.specialMessage
== True:
1030 self
._gotUBX
(message
)
1034 ### protocol command handlers - no need to override these
1036 def handle_VER(self
, params
):
1037 versions
= params
[1:]
1038 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
1039 self
.transport
.loseConnection()
1040 raise MSNProtocolError
, "Invalid version response"
1041 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
1043 def handle_CVR(self
, params
):
1044 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
1046 def handle_USR(self
, params
):
1047 if not (4 <= len(params
) <= 6):
1048 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
1050 mechanism
= params
[1]
1051 if mechanism
== "OK":
1052 self
.loggedIn(params
[2], int(params
[3]))
1053 elif params
[2].upper() == "S":
1054 # we need to obtain auth from a passport server
1056 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3])
1057 d
.addCallback(self
._passportLogin
)
1058 d
.addErrback(self
._passportError
)
1060 def _passportLogin(self
, result
):
1061 if result
[0] == LOGIN_REDIRECT
:
1062 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
1063 result
[1], cached
=1, authData
=result
[2])
1064 d
.addCallback(self
._passportLogin
)
1065 d
.addErrback(self
._passportError
)
1066 elif result
[0] == LOGIN_SUCCESS
:
1067 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
1068 elif result
[0] == LOGIN_FAILURE
:
1069 self
.loginFailure(result
[1])
1071 def _passportError(self
, failure
):
1072 self
.loginFailure("Exception while authenticating: %s" % failure
)
1074 def handle_CHG(self
, params
):
1076 if not self
._fireCallback
(id, params
[1]):
1077 if self
.factory
: self
.factory
.status
= params
[1]
1078 self
.statusChanged(params
[1])
1080 def handle_ILN(self
, params
):
1081 #checkParamLen(len(params), 6, 'ILN')
1082 msnContact
= self
.factory
.contacts
.getContact(params
[2])
1083 if not msnContact
: return
1084 msnContact
.status
= params
[1]
1085 msnContact
.screenName
= unquote(params
[3])
1086 if len(params
) > 4: msnContact
.caps
= int(params
[4])
1088 self
.handleAvatarHelper(msnContact
, params
[5])
1090 self
.handleAvatarGoneHelper(msnContact
)
1091 self
.gotContactStatus(params
[1], params
[2], unquote(params
[3]))
1093 def handleAvatarGoneHelper(self
, msnContact
):
1094 if msnContact
.msnobj
:
1095 msnContact
.msnobj
= None
1096 msnContact
.msnobjGot
= True
1097 self
.contactAvatarChanged(msnContact
.userHandle
, "")
1099 def handleAvatarHelper(self
, msnContact
, msnobjStr
):
1100 msnobj
= MSNObject(unquote(msnobjStr
))
1101 if not msnContact
.msnobj
or msnobj
.sha1d
!= msnContact
.msnobj
.sha1d
:
1102 if MSNP2PDEBUG
: log
.msg("Updated MSNObject received!" + msnobjStr
)
1103 msnContact
.msnobj
= msnobj
1104 msnContact
.msnobjGot
= False
1105 self
.contactAvatarChanged(msnContact
.userHandle
, msnContact
.msnobj
.sha1d
)
1107 def handle_CHL(self
, params
):
1108 checkParamLen(len(params
), 2, 'CHL')
1109 response
= msnp11chl
.doChallenge(params
[1])
1110 self
.sendLine("QRY %s %s %s" % (self
._nextTransactionID
(), msnp11chl
.MSNP11_PRODUCT_ID
, len(response
)))
1111 self
.transport
.write(response
)
1113 def handle_QRY(self
, params
):
1116 def handle_NLN(self
, params
):
1117 if not self
.factory
: return
1118 msnContact
= self
.factory
.contacts
.getContact(params
[1])
1119 if not msnContact
: return
1120 msnContact
.status
= params
[0]
1121 msnContact
.screenName
= unquote(params
[2])
1122 if len(params
) > 3: msnContact
.caps
= int(params
[3])
1124 self
.handleAvatarHelper(msnContact
, params
[4])
1126 self
.handleAvatarGoneHelper(msnContact
)
1127 self
.contactStatusChanged(params
[0], params
[1], unquote(params
[2]))
1129 def handle_FLN(self
, params
):
1130 checkParamLen(len(params
), 1, 'FLN')
1131 msnContact
= self
.factory
.contacts
.getContact(params
[0])
1133 msnContact
.status
= STATUS_OFFLINE
1134 self
.contactOffline(params
[0])
1136 def handle_LST(self
, params
):
1137 if self
._getState
() != 'SYNC': return
1139 userHandle
, screenName
, userGuid
, lists
, groups
= getVals(params
)
1141 if not userHandle
or lists
< 1:
1142 raise MSNProtocolError
, "Unknown LST " + str(params
) # debug
1143 contact
= MSNContact(userGuid
, userHandle
, screenName
, lists
)
1144 if contact
.lists
& FORWARD_LIST
:
1145 contact
.groups
.extend(map(str, groups
))
1146 self
._getStateData
('list').addContact(contact
)
1147 self
._setStateData
('last_contact', contact
)
1148 sofar
= self
._getStateData
('lst_sofar') + 1
1149 if sofar
== self
._getStateData
('lst_reply'):
1150 # this is the best place to determine that
1151 # a syn realy has finished - msn _may_ send
1152 # BPR information for the last contact
1153 # which is unfortunate because it means
1154 # that the real end of a syn is non-deterministic.
1155 # to handle this we'll keep 'last_contact' hanging
1156 # around in the state data and update it if we need
1158 self
._setState
('SESSION')
1159 contacts
= self
._getStateData
('list')
1160 phone
= self
._getStateData
('phone')
1161 id = self
._getStateData
('synid')
1162 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1163 self
._fireCallback
(id, contacts
, phone
)
1165 self
._setStateData
('lst_sofar',sofar
)
1167 def handle_BLP(self
, params
):
1168 # check to see if this is in response to a SYN
1169 if self
._getState
() == 'SYNC':
1170 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
1173 self
.factory
.contacts
.privacy
= listCodeToID
[params
[1].lower()]
1174 self
._fireCallback
(id, params
[1])
1176 def handle_GTC(self
, params
):
1177 # check to see if this is in response to a SYN
1178 if self
._getState
() == 'SYNC':
1179 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
1180 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
1181 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1184 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
1185 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
1186 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1188 def handle_SYN(self
, params
):
1190 self
._setStateData
('phone', []) # Always needs to be set
1191 if params
[3] == 0: # No LST will be received. New account?
1192 self
._setState
('SESSION')
1193 self
._fireCallback
(id, None, None)
1195 contacts
= MSNContactList()
1196 self
._setStateData
('list', contacts
)
1197 self
._setStateData
('lst_reply', int(params
[3]))
1198 self
._setStateData
('lsg_reply', int(params
[4]))
1199 self
._setStateData
('lst_sofar', 0)
1201 def handle_LSG(self
, params
):
1202 if self
._getState
() == 'SYNC':
1203 self
._getStateData
('list').groups
[params
[1]] = unquote(params
[0])
1205 def handle_PRP(self
, params
):
1206 if params
[1] == "MFN":
1207 self
._fireCallback
(int(params
[0]), unquote(params
[2]))
1208 elif self
._getState
() == 'SYNC':
1209 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
1211 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
1213 def handle_BPR(self
, params
):
1214 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_BPR called with no contact list" # debug
1215 numParams
= len(params
)
1216 if numParams
== 2: # part of a syn
1217 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
1218 elif numParams
== 4:
1219 self
.factory
.contacts
.version
= int(params
[0])
1220 userHandle
, phoneType
, number
= params
[1], params
[2], unquote(params
[3])
1221 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1222 self
.gotPhoneNumber(userHandle
, phoneType
, number
)
1225 def handle_ADG(self
, params
):
1226 checkParamLen(len(params
), 5, 'ADG')
1228 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
1229 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
1231 def handle_RMG(self
, params
):
1232 checkParamLen(len(params
), 3, 'RMG')
1234 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
1235 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
1237 def handle_REG(self
, params
):
1238 checkParamLen(len(params
), 5, 'REG')
1240 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
1241 raise MSNProtocolError
, "REG response does not match up to a request" # debug
1243 def handle_ADC(self
, params
):
1244 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_ADC called with no contact list"
1245 numParams
= len(params
)
1246 if numParams
< 3 or params
[1].upper() not in ('AL','BL','RL','FL', 'PL'):
1247 raise MSNProtocolError
, "Invalid Paramaters for ADC" # debug
1249 listType
= params
[1].lower()
1250 userHandle
, screenName
, userGuid
, ignored1
, groups
= getVals(params
[2:])
1252 if groups
and listType
.upper() != FORWARD_LIST
:
1253 raise MSNProtocolError
, "Only forward list can contain groups" # debug
1255 if not self
._fireCallback
(id, listCodeToID
[listType
], userGuid
, userHandle
, screenName
):
1256 c
= self
.factory
.contacts
.getContact(userHandle
)
1258 c
= MSNContact(userGuid
=userGuid
, userHandle
=userHandle
, screenName
=screenName
)
1259 self
.factory
.contacts
.addContact(c
)
1260 c
.addToList(PENDING_LIST
)
1261 self
.userAddedMe(userGuid
, userHandle
, screenName
)
1263 def handle_REM(self
, params
):
1264 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_REM called with no contact list available!"
1265 numParams
= len(params
)
1266 if numParams
< 3 or params
[1].upper() not in ('AL','BL','FL','RL'):
1267 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1269 listType
= params
[1].lower()
1270 userHandle
= params
[2]
1273 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1274 groupID
= int(params
[3])
1275 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, groupID
):
1276 if listType
.upper() != "RL": return
1277 c
= self
.factory
.contacts
.getContact(userHandle
)
1279 c
.removeFromList(REVERSE_LIST
)
1280 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1281 self
.userRemovedMe(userHandle
)
1283 def handle_XFR(self
, params
):
1284 checkParamLen(len(params
), 5, 'XFR')
1286 # check to see if they sent a host/port pair
1288 host
, port
= params
[2].split(':')
1293 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1294 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1296 def handle_RNG(self
, params
):
1297 checkParamLen(len(params
), 6, 'RNG')
1298 # check for host:port pair
1300 host
, port
= params
[1].split(":")
1305 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1308 def handle_NOT(self
, params
):
1309 checkParamLen(len(params
), 1, 'NOT')
1311 messageLen
= int(params
[0])
1312 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1313 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", specialMessage
=True)
1316 def handle_UBX(self
, params
):
1317 checkParamLen(len(params
), 2, 'UBX')
1319 messageLen
= int(params
[1])
1320 except ValueError: raise MSNProtocolError
, "Invalid Parameter for UBX length argument"
1322 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
="UBX", specialMessage
=True)
1325 self
.contactPersonalChanged(params
[0], '')
1327 def handle_UUX(self
, params
):
1328 checkParamLen(len(params
), 2, 'UUX')
1329 if params
[1] != '0': return
1331 self
._fireCallback
(id)
1333 def handle_OUT(self
, params
):
1334 checkParamLen(len(params
), 1, 'OUT')
1335 if params
[0] == "OTH": self
.multipleLogin()
1336 elif params
[0] == "SSD": self
.serverGoingDown()
1337 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1339 def handle_QNG(self
, params
):
1340 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1344 def pingChecker(self
):
1345 if self
.pingCounter
> 5:
1346 # The server has ignored 5 pings, lets kill the connection
1347 self
.transport
.loseConnection()
1349 self
.sendLine("PNG")
1350 self
.pingCounter
+= 1
1352 def pingCheckerStart(self
, *args
):
1353 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1354 self
.pingCheckTask
.start(PINGSPEED
)
1356 def loggedIn(self
, userHandle
, verified
):
1358 Called when the client has logged in.
1359 The default behaviour of this method is to
1360 update the factory with our screenName and
1361 to sync the contact list (factory.contacts).
1362 When this is complete self.listSynchronized
1365 @param userHandle: our userHandle
1366 @param verified: 1 if our passport has been (verified), 0 if not.
1367 (i'm not sure of the significace of this)
1371 d
.addCallback(self
.listSynchronized
)
1372 d
.addCallback(self
.pingCheckerStart
)
1374 def loginFailure(self
, message
):
1376 Called when the client fails to login.
1378 @param message: a message indicating the problem that was encountered
1382 def gotProfile(self
, message
):
1384 Called after logging in when the server sends an initial
1385 message with MSN/passport specific profile information
1386 such as country, number of kids, etc.
1387 Check the message headers for the specific values.
1389 @param message: The profile message
1393 def listSynchronized(self
, *args
):
1395 Lists are now synchronized by default upon logging in, this
1396 method is called after the synchronization has finished
1397 and the factory now has the up-to-date contacts.
1401 def contactAvatarChanged(self
, userHandle
, hash):
1403 Called when we receive the first, or a new <msnobj/> from a
1406 @param userHandle: contact who's msnobj has been changed
1407 @param hash: sha1 hash of their avatar
1410 def statusChanged(self
, statusCode
):
1412 Called when our status changes and its not in response to a
1415 @param statusCode: 3-letter status code
1419 def gotContactStatus(self
, statusCode
, userHandle
, screenName
):
1421 Called when we receive a list of statuses upon login.
1423 @param statusCode: 3-letter status code
1424 @param userHandle: the contact's user handle (passport)
1425 @param screenName: the contact's screen name
1429 def contactStatusChanged(self
, statusCode
, userHandle
, screenName
):
1431 Called when we're notified that a contact's status has changed.
1433 @param statusCode: 3-letter status code
1434 @param userHandle: the contact's user handle (passport)
1435 @param screenName: the contact's screen name
1439 def contactPersonalChanged(self
, userHandle
, personal
):
1441 Called when a contact's personal message changes.
1443 @param userHandle: the contact who changed their personal message
1444 @param personal : the new personal message
1448 def contactOffline(self
, userHandle
):
1450 Called when a contact goes offline.
1452 @param userHandle: the contact's user handle
1456 def gotMessage(self
, message
):
1458 Called when there is a message from the notification server
1459 that is not understood by default.
1461 @param message: the MSNMessage.
1465 def gotMSNAlert(self
, body
, action
, subscr
):
1467 Called when the server sends an MSN Alert (http://alerts.msn.com)
1469 @param body : the alert text
1470 @param action: a URL with more information for the user to view
1471 @param subscr: a URL the user can use to modify their alert subscription
1475 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1477 Called when the server sends you details about your hotmail
1478 inbox. This is only ever called once, on login.
1480 @param inboxunread : the number of unread items in your inbox
1481 @param foldersunread: the number of unread items in other folders
1485 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1487 Called when the server sends us realtime email
1488 notification. This means that you have received
1489 a new email in your hotmail inbox.
1491 @param mailfrom: the sender of the email
1492 @param fromaddr: the sender of the email (I don't know :P)
1493 @param subject : the email subject
1497 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1499 Called when the server sends us phone details about
1500 a specific user (for example after a user is added
1501 the server will send their status, phone details etc.
1503 @param userHandle: the contact's user handle (passport)
1504 @param phoneType: the specific phoneType
1505 (*_PHONE constants or HAS_PAGER)
1506 @param number: the value/phone number.
1510 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1512 Called when a user adds me to their list. (ie. they have been added to
1515 @param userHandle: the userHandle of the user
1516 @param screenName: the screen name of the user
1520 def userRemovedMe(self
, userHandle
):
1522 Called when a user removes us from their contact list
1523 (they are no longer on our reverseContacts list.
1525 @param userHandle: the contact's user handle (passport)
1529 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1530 key
, userHandle
, screenName
):
1532 Called when we get an invitation to a switchboard server.
1533 This happens when a user requests a chat session with us.
1535 @param sessionID: session ID number, must be remembered for logging in
1536 @param host: the hostname of the switchboard server
1537 @param port: the port to connect to
1538 @param key: used for authorization when connecting
1539 @param userHandle: the user handle of the person who invited us
1540 @param screenName: the screen name of the person who invited us
1544 def multipleLogin(self
):
1546 Called when the server says there has been another login
1547 under our account, the server should disconnect us right away.
1551 def serverGoingDown(self
):
1553 Called when the server has notified us that it is going down for
1560 def changeStatus(self
, status
):
1562 Change my current status. This method will add
1563 a default callback to the returned Deferred
1564 which will update the status attribute of the
1567 @param status: 3-letter status code (as defined by
1568 the STATUS_* constants)
1569 @return: A Deferred, the callback of which will be
1570 fired when the server confirms the change
1571 of status. The callback argument will be
1572 a tuple with the new status code as the
1576 id, d
= self
._createIDMapping
()
1577 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1579 self
.factory
.status
= r
[0]
1581 return d
.addCallback(_cb
)
1583 def setPrivacyMode(self
, privLevel
):
1585 Set my privacy mode on the server.
1588 This only keeps the current privacy setting on
1589 the server for later retrieval, it does not
1590 effect the way the server works at all.
1592 @param privLevel: This parameter can be true, in which
1593 case the server will keep the state as
1594 'al' which the official client interprets
1595 as -> allow messages from only users on
1596 the allow list. Alternatively it can be
1597 false, in which case the server will keep
1598 the state as 'bl' which the official client
1599 interprets as -> allow messages from all
1600 users except those on the block list.
1602 @return: A Deferred, the callback of which will be fired when
1603 the server replies with the new privacy setting.
1604 The callback argument will be a tuple, the only element
1605 of which being either 'al' or 'bl' (the new privacy setting).
1608 id, d
= self
._createIDMapping
()
1609 if privLevel
: self
.sendLine("BLP %s AL" % id)
1610 else: self
.sendLine("BLP %s BL" % id)
1615 Used for keeping an up-to-date contact list.
1616 A callback is added to the returned Deferred
1617 that updates the contact list on the factory
1618 and also sets my state to STATUS_ONLINE.
1621 This is called automatically upon signing
1622 in using the version attribute of
1623 factory.contacts, so you may want to persist
1624 this object accordingly. Because of this there
1625 is no real need to ever call this method
1628 @return: A Deferred, the callback of which will be
1629 fired when the server sends an adequate reply.
1630 The callback argument will be a tuple with two
1631 elements, the new list (MSNContactList) and
1632 your current state (a dictionary). If the version
1633 you sent _was_ the latest list version, both elements
1634 will be None. To just request the list send a version of 0.
1637 self
._setState
('SYNC')
1638 id, d
= self
._createIDMapping
(data
=None)
1639 self
._setStateData
('synid',id)
1640 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1642 self
.changeStatus(STATUS_ONLINE
)
1643 if r
[0] is not None:
1644 self
.factory
.contacts
= r
[0]
1646 return d
.addCallback(_cb
)
1648 def setPhoneDetails(self
, phoneType
, value
):
1650 Set/change my phone numbers stored on the server.
1652 @param phoneType: phoneType can be one of the following
1653 constants - HOME_PHONE, WORK_PHONE,
1654 MOBILE_PHONE, HAS_PAGER.
1655 These are pretty self-explanatory, except
1656 maybe HAS_PAGER which refers to whether or
1657 not you have a pager.
1658 @param value: for all of the *_PHONE constants the value is a
1659 phone number (str), for HAS_PAGER accepted values
1660 are 'Y' (for yes) and 'N' (for no).
1662 @return: A Deferred, the callback for which will be fired when
1663 the server confirms the change has been made. The
1664 callback argument will be a tuple with 2 elements, the
1665 first being the new list version (int) and the second
1666 being the new phone number value (str).
1668 raise "ProbablyDoesntWork"
1669 # XXX: Add a default callback which updates
1670 # factory.contacts.version and the relevant phone
1672 id, d
= self
._createIDMapping
()
1673 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1676 def addListGroup(self
, name
):
1678 Used to create a new list group.
1679 A default callback is added to the
1680 returned Deferred which updates the
1681 contacts attribute of the factory.
1683 @param name: The desired name of the new group.
1685 @return: A Deferred, the callbacck for which will be called
1686 when the server clarifies that the new group has been
1687 created. The callback argument will be a tuple with 3
1688 elements: the new list version (int), the new group name
1689 (str) and the new group ID (int).
1692 raise "ProbablyDoesntWork"
1693 id, d
= self
._createIDMapping
()
1694 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1696 if self
.factory
.contacts
:
1697 self
.factory
.contacts
.version
= r
[0]
1698 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1700 return d
.addCallback(_cb
)
1702 def remListGroup(self
, groupID
):
1704 Used to remove a list group.
1705 A default callback is added to the
1706 returned Deferred which updates the
1707 contacts attribute of the factory.
1709 @param groupID: the ID of the desired group to be removed.
1711 @return: A Deferred, the callback for which will be called when
1712 the server clarifies the deletion of the group.
1713 The callback argument will be a tuple with 2 elements:
1714 the new list version (int) and the group ID (int) of
1718 raise "ProbablyDoesntWork"
1719 id, d
= self
._createIDMapping
()
1720 self
.sendLine("RMG %s %s" % (id, groupID
))
1722 self
.factory
.contacts
.version
= r
[0]
1723 self
.factory
.contacts
.remGroup(r
[1])
1725 return d
.addCallback(_cb
)
1727 def renameListGroup(self
, groupID
, newName
):
1729 Used to rename an existing list group.
1730 A default callback is added to the returned
1731 Deferred which updates the contacts attribute
1734 @param groupID: the ID of the desired group to rename.
1735 @param newName: the desired new name for the group.
1737 @return: A Deferred, the callback for which will be called
1738 when the server clarifies the renaming.
1739 The callback argument will be a tuple of 3 elements,
1740 the new list version (int), the group id (int) and
1741 the new group name (str).
1744 raise "ProbablyDoesntWork"
1745 id, d
= self
._createIDMapping
()
1746 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1748 self
.factory
.contacts
.version
= r
[0]
1749 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1751 return d
.addCallback(_cb
)
1753 def addContact(self
, listType
, userHandle
):
1755 Used to add a contact to the desired list.
1756 A default callback is added to the returned
1757 Deferred which updates the contacts attribute of
1758 the factory with the new contact information.
1759 If you are adding a contact to the forward list
1760 and you want to associate this contact with multiple
1761 groups then you will need to call this method for each
1762 group you would like to add them to, changing the groupID
1763 parameter. The default callback will take care of updating
1764 the group information on the factory's contact list.
1766 @param listType: (as defined by the *_LIST constants)
1767 @param userHandle: the user handle (passport) of the contact
1770 @return: A Deferred, the callback for which will be called when
1771 the server has clarified that the user has been added.
1772 The callback argument will be a tuple with 4 elements:
1773 the list type, the contact's user handle, the new list
1774 version, and the group id (if relevant, otherwise it
1778 id, d
= self
._createIDMapping
()
1779 try: # Make sure the contact isn't actually on the list
1780 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1781 except AttributeError: pass
1782 listType
= listIDToCode
[listType
].upper()
1783 if listType
== "FL":
1784 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1786 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1789 if not self
.factory
: return
1790 c
= self
.factory
.contacts
.getContact(r
[2])
1792 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1793 #if r[3]: c.groups.append(r[3])
1796 return d
.addCallback(_cb
)
1798 def remContact(self
, listType
, userHandle
):
1800 Used to remove a contact from the desired list.
1801 A default callback is added to the returned deferred
1802 which updates the contacts attribute of the factory
1803 to reflect the new contact information.
1805 @param listType: (as defined by the *_LIST constants)
1806 @param userHandle: the user handle (passport) of the
1807 contact being removed
1809 @return: A Deferred, the callback for which will be called when
1810 the server has clarified that the user has been removed.
1811 The callback argument will be a tuple of 3 elements:
1812 the list type, the contact's user handle and the group ID
1813 (if relevant, otherwise it will be None)
1816 id, d
= self
._createIDMapping
()
1817 try: # Make sure the contact is actually on this list
1818 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1819 except AttributeError: return
1820 listType
= listIDToCode
[listType
].upper()
1821 if listType
== "FL":
1823 c
= self
.factory
.contacts
.getContact(userHandle
)
1824 userGuid
= c
.userGuid
1825 except AttributeError: return
1826 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1828 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1831 if listType
== "FL":
1832 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1833 l
= self
.factory
.contacts
1834 c
= l
.getContact(r
[1])
1838 if group
: # they may not have been removed from the list
1839 c
.groups
.remove(group
)
1840 if c
.groups
: shouldRemove
= 0
1842 c
.removeFromList(r
[0])
1843 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1845 return d
.addCallback(_cb
)
1847 def changeScreenName(self
, newName
):
1849 Used to change your current screen name.
1850 A default callback is added to the returned
1851 Deferred which updates the screenName attribute
1852 of the factory and also updates the contact list
1855 @param newName: the new screen name
1857 @return: A Deferred, the callback for which will be called
1858 when the server acknowledges the change.
1859 The callback argument will be a tuple of 1 element,
1860 the new screen name.
1863 id, d
= self
._createIDMapping
()
1864 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1866 self
.factory
.screenName
= r
[0]
1868 return d
.addCallback(_cb
)
1870 def changePersonalMessage(self
, personal
):
1872 Used to change your personal message.
1874 @param personal: the new screen name
1876 @return: A Deferred, the callback for which will be called
1877 when the server acknowledges the change.
1878 The callback argument will be a tuple of 1 element,
1879 the personal message.
1882 id, d
= self
._createIDMapping
()
1885 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1886 self
.sendLine("UUX %s %s" % (id, len(data
)))
1887 self
.transport
.write(data
)
1889 self
.factory
.personal
= personal
1891 return d
.addCallback(_cb
)
1893 def changeAvatar(self
, imageData
, push
):
1895 Used to change the avatar that other users see.
1897 @param imageData: the PNG image data to set as the avatar
1898 @param push : whether to push the update to the server
1899 (it will otherwise be sent with the next
1902 @return: If push==True, a Deferred, the callback for which
1903 will be called when the server acknowledges the change.
1904 The callback argument will be the same as for changeStatus.
1907 if self
.msnobj
and imageData
== self
.msnobj
.imageData
: return
1909 self
.msnobj
.setData(self
.factory
.userHandle
, imageData
)
1911 self
.msnobj
.setNull()
1912 if push
: return self
.changeStatus(self
.factory
.status
) # Push to server
1915 def requestSwitchboardServer(self
):
1917 Used to request a switchboard server to use for conversations.
1919 @return: A Deferred, the callback for which will be called when
1920 the server responds with the switchboard information.
1921 The callback argument will be a tuple with 3 elements:
1922 the host of the switchboard server, the port and a key
1923 used for logging in.
1926 id, d
= self
._createIDMapping
()
1927 self
.sendLine("XFR %s SB" % id)
1932 Used to log out of the notification server.
1933 After running the method the server is expected
1934 to close the connection.
1937 if self
.pingCheckTask
:
1938 self
.pingCheckTask
.stop()
1939 self
.pingCheckTask
= None
1940 self
.sendLine("OUT")
1942 class NotificationFactory(ClientFactory
):
1944 Factory for the NotificationClient protocol.
1945 This is basically responsible for keeping
1946 the state of the client and thus should be used
1947 in a 1:1 situation with clients.
1949 @ivar contacts: An MSNContactList instance reflecting
1950 the current contact list -- this is
1951 generally kept up to date by the default
1953 @ivar userHandle: The client's userHandle, this is expected
1954 to be set by the client and is used by the
1955 protocol (for logging in etc).
1956 @ivar screenName: The client's current screen-name -- this is
1957 generally kept up to date by the default
1959 @ivar password: The client's password -- this is (obviously)
1960 expected to be set by the client.
1961 @ivar passportServer: This must point to an msn passport server
1962 (the whole URL is required)
1963 @ivar status: The status of the client -- this is generally kept
1964 up to date by the default command handlers
1971 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
1973 protocol
= NotificationClient
1976 class SwitchboardClient(MSNEventBase
):
1978 This class provides support for clients connecting to a switchboard server.
1980 Switchboard servers are used for conversations with other people
1981 on the MSN network. This means that the number of conversations at
1982 any given time will be directly proportional to the number of
1983 connections to varioius switchboard servers.
1985 MSN makes no distinction between single and group conversations,
1986 so any number of users may be invited to join a specific conversation
1987 taking place on a switchboard server.
1989 @ivar key: authorization key, obtained when receiving
1990 invitation / requesting switchboard server.
1991 @ivar userHandle: your user handle (passport)
1992 @ivar sessionID: unique session ID, used if you are replying
1993 to a switchboard invitation
1994 @ivar reply: set this to 1 in connectionMade or before to signifiy
1995 that you are replying to a switchboard invitation.
1996 @ivar msnobj: the MSNObject for the user. So that the switchboard can distribute it.
2008 MSNEventBase
.__init
__(self
)
2009 self
.pendingUsers
= {}
2010 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
2013 def connectionMade(self
):
2014 MSNEventBase
.connectionMade(self
)
2017 def connectionLost(self
, reason
):
2018 self
.cookies
['iCookies'] = {}
2019 MSNEventBase
.connectionLost(self
, reason
)
2021 def _sendInit(self
):
2023 send initial data based on whether we are replying to an invitation
2026 id = self
._nextTransactionID
()
2028 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
2030 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
2032 def _newInvitationCookie(self
):
2034 if self
._iCookie
> 1000: self
._iCookie
= 1
2035 return self
._iCookie
2037 def _checkTyping(self
, message
, cTypes
):
2038 """ helper method for checkMessage """
2039 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
2040 self
.userTyping(message
)
2043 def _checkFileInvitation(self
, message
, info
):
2044 """ helper method for checkMessage """
2045 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
2047 cookie
= info
['Invitation-Cookie']
2048 filename
= info
['Application-File']
2049 filesize
= int(info
['Application-FileSize'])
2050 connectivity
= (info
.get('Connectivity').lower() == 'y')
2052 log
.msg('Received munged file transfer request ... ignoring.')
2054 raise NotImplementedError
2055 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
2058 def _handleP2PMessage(self
, message
):
2059 """ helper method for msnslp messages (file transfer & avatars) """
2060 if not message
.getHeader("P2P-Dest") == self
.userHandle
: return
2061 packet
= message
.message
2062 binaryFields
= BinaryFields(packet
=packet
)
2063 if binaryFields
[0] != 0:
2064 slpLink
= self
.slpLinks
[binaryFields
[0]]
2065 if slpLink
.remoteUser
== message
.userHandle
:
2066 if MSNP2PDEBUG
: print "Calling handlePacket", slpLink
.__class
__
2067 slpLink
.handlePacket(packet
)
2068 elif binaryFields
[5] == BinaryFields
.ACK
or binaryFields
[5] == BinaryFields
.BYEGOT
:
2069 pass # Ignore the ACKs
2071 slpMessage
= MSNSLPMessage(packet
)
2073 # Always try and give a slpMessage to a slpLink first.
2074 # If none can be found, and it was INVITE, then create
2075 # one to handle the session.
2076 for slpLink
in self
.slpLinks
.values():
2077 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
2078 slpLink
.handleSLPMessage(slpMessage
)
2081 slpLink
= None # Was not handled
2083 if not slpLink
and slpMessage
.method
== "INVITE":
2084 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
2085 context
= FileContext(slpMessage
.context
)
2086 slpLink
= SLPLink_FileReceive(remoteUser
=slpMessage
.fro
, switchboard
=self
, filename
=context
.filename
, filesize
=context
.filesize
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2087 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2088 self
.gotFileReceive(slpLink
)
2089 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
2090 # Check that we have an avatar to send
2092 slpLink
= SLPLink_AvatarSend(remoteUser
=slpMessage
.fro
, switchboard
=self
, filesize
=self
.msnobj
.size
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2093 slpLink
.write(self
.msnobj
.imageData
)
2096 # They shouldn't have sent a request if we have
2097 # no avatar. So we'll just ignore them.
2100 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2102 # Always need to ACK these packets if we can
2103 slpLink
.sendP2PACK(binaryFields
)
2106 def checkMessage(self
, message
):
2108 hook for detecting any notification type messages
2109 (e.g. file transfer)
2111 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2112 if self
._checkTyping
(message
, cTypes
): return 0
2113 if 'text/x-msmsgsinvite' in cTypes
:
2114 # header like info is sent as part of the message body.
2116 for line
in message
.message
.split('\r\n'):
2118 key
, val
= line
.split(':')
2119 info
[key
] = val
.lstrip()
2120 except ValueError: continue
2121 if self
._checkFileInvitation
(message
, info
): return 0
2122 elif 'application/x-msnmsgrp2p' in cTypes
:
2123 self
._handleP
2PMessage
(message
)
2128 def handle_USR(self
, params
):
2129 checkParamLen(len(params
), 4, 'USR')
2130 if params
[1] == "OK":
2134 def handle_CAL(self
, params
):
2135 checkParamLen(len(params
), 3, 'CAL')
2137 if params
[1].upper() == "RINGING":
2138 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2141 def handle_JOI(self
, params
):
2142 checkParamLen(len(params
), 2, 'JOI')
2143 self
.userJoined(params
[0], unquote(params
[1]))
2145 # users participating in the current chat
2146 def handle_IRO(self
, params
):
2147 checkParamLen(len(params
), 5, 'IRO')
2148 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2149 if params
[1] == params
[2]:
2150 self
.gotChattingUsers(self
.pendingUsers
)
2151 self
.pendingUsers
= {}
2153 # finished listing users
2154 def handle_ANS(self
, params
):
2155 checkParamLen(len(params
), 2, 'ANS')
2156 if params
[1] == "OK":
2159 def handle_ACK(self
, params
):
2160 checkParamLen(len(params
), 1, 'ACK')
2161 self
._fireCallback
(int(params
[0]), None)
2163 def handle_NAK(self
, params
):
2164 checkParamLen(len(params
), 1, 'NAK')
2165 self
._fireCallback
(int(params
[0]), None)
2167 def handle_BYE(self
, params
):
2168 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2169 self
.userLeft(params
[0])
2175 called when all login details have been negotiated.
2176 Messages can now be sent, or new users invited.
2180 def gotChattingUsers(self
, users
):
2182 called after connecting to an existing chat session.
2184 @param users: A dict mapping user handles to screen names
2185 (current users taking part in the conversation)
2189 def userJoined(self
, userHandle
, screenName
):
2191 called when a user has joined the conversation.
2193 @param userHandle: the user handle (passport) of the user
2194 @param screenName: the screen name of the user
2198 def userLeft(self
, userHandle
):
2200 called when a user has left the conversation.
2202 @param userHandle: the user handle (passport) of the user.
2206 def gotMessage(self
, message
):
2208 called when we receive a message.
2210 @param message: the associated MSNMessage object
2214 def gotFileReceive(self
, fileReceive
):
2216 called when we receive a file send request from a contact
2218 @param fileReceive: msnft.MSNFTReceive_Base instance
2223 def gotSendRequest(self
, fileReceive
):
2225 called when we receive a file send request from a contact
2227 @param fileReceive: msnft.MSNFTReceive_Base instance
2231 def userTyping(self
, message
):
2233 called when we receive the special type of message notifying
2234 us that a user is typing a message.
2236 @param message: the associated MSNMessage object
2242 def inviteUser(self
, userHandle
):
2244 used to invite a user to the current switchboard server.
2246 @param userHandle: the user handle (passport) of the desired user.
2248 @return: A Deferred, the callback for which will be called
2249 when the server notifies us that the user has indeed
2250 been invited. The callback argument will be a tuple
2251 with 1 element, the sessionID given to the invited user.
2252 I'm not sure if this is useful or not.
2255 id, d
= self
._createIDMapping
()
2256 self
.sendLine("CAL %s %s" % (id, userHandle
))
2259 def sendMessage(self
, message
):
2261 used to send a message.
2263 @param message: the corresponding MSNMessage object.
2265 @return: Depending on the value of message.ack.
2266 If set to MSNMessage.MESSAGE_ACK or
2267 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2268 the callback for which will be fired when an ACK or
2269 NACK is received - the callback argument will be
2270 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2271 the return value is None.
2274 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2275 else: id, d
= self
._createIDMapping
()
2276 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2277 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2278 # apparently order matters with at least MIME-Version and Content-Type
2279 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
2280 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
2281 # send the rest of the headers
2282 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
2283 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2284 self
.transport
.write("\r\n")
2285 self
.transport
.write(message
.message
)
2286 if MESSAGEDEBUG
: log
.msg(message
.message
)
2289 def sendAvatarRequest(self
, msnContact
):
2291 used to request an avatar from a user in this switchboard
2294 @param msnContact: the msnContact object to request an avatar for
2296 @return: A Deferred, the callback for which will be called
2297 when the avatar transfer succeeds.
2298 The callback argument will be a tuple with one element,
2299 the PNG avatar data.
2301 if not msnContact
.msnobj
: return
2303 def bufferClosed(data
):
2305 buffer = StringBuffer(bufferClosed
)
2306 slpLink
= SLPLink_AvatarReceive(remoteUser
=msnContact
.userHandle
, switchboard
=self
, consumer
=buffer, context
=msnContact
.msnobj
.text
)
2307 slpLink
.avatarDataBuffer
= buffer
2308 self
.slpLinks
[slpLink
.sessionID
] = slpLink
2311 def sendFile(self
, msnContact
, filename
, filesize
):
2313 used to send a file to a contact.
2315 @param msnContact: the MSNContact object to send a file to.
2316 @param filename: the name of the file to send.
2317 @param filesize: the size of the file to send.
2319 @return: (fileSend, d) A FileSend object and a Deferred.
2320 The Deferred will pass one argument in a tuple,
2321 whether or not the transfer is accepted. If you
2322 receive a True, then you can call write() on the
2323 fileSend object to send your file. Call close()
2324 when the file is done.
2325 NOTE: You MUST write() exactly as much as you
2326 declare in filesize.
2328 if not msnContact
.userHandle
: return
2329 # FIXME, check msnContact.caps to see if we should use old-style
2330 fileSend
= SLPLink_FileSend(remoteUser
=msnContact
.userHandle
, switchboard
=self
, filename
=filename
, filesize
=filesize
)
2331 self
.slpLinks
[fileSend
.sessionID
] = fileSend
2332 return fileSend
, fileSend
.acceptDeferred
2334 def sendTypingNotification(self
):
2336 Used to send a typing notification. Upon receiving this
2337 message the official client will display a 'user is typing'
2338 message to all other users in the chat session for 10 seconds.
2339 You should send one of these every 5 seconds as long as the
2343 m
.ack
= m
.MESSAGE_ACK_NONE
2344 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2345 m
.setHeader('TypingUser', self
.userHandle
)
2349 def sendFileInvitation(self
, fileName
, fileSize
):
2351 send an notification that we want to send a file.
2353 @param fileName: the file name
2354 @param fileSize: the file size
2356 @return: A Deferred, the callback of which will be fired
2357 when the user responds to this invitation with an
2358 appropriate message. The callback argument will be
2359 a tuple with 3 elements, the first being 1 or 0
2360 depending on whether they accepted the transfer
2361 (1=yes, 0=no), the second being an invitation cookie
2362 to identify your follow-up responses and the third being
2363 the message 'info' which is a dict of information they
2364 sent in their reply (this doesn't really need to be used).
2365 If you wish to proceed with the transfer see the
2366 sendTransferInfo method.
2368 cookie
= self
._newInvitationCookie
()
2371 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2372 m
.message
+= 'Application-Name: File Transfer\r\n'
2373 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2374 m
.message
+= 'Invitation-Command: INVITE\r\n'
2375 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2376 m
.message
+= 'Application-File: %s\r\n' % fileName
2377 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2378 m
.ack
= m
.MESSAGE_ACK_NONE
2380 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2383 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2385 send information relating to a file transfer session.
2387 @param accept: whether or not to go ahead with the transfer
2389 @param iCookie: the invitation cookie of previous replies
2390 relating to this transfer
2391 @param authCookie: the authentication cookie obtained from
2392 an FileSend instance
2394 @param port: the port on which an FileSend protocol is listening.
2397 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2398 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2399 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2400 m
.message
+= 'IP-Address: %s\r\n' % ip
2401 m
.message
+= 'Port: %s\r\n' % port
2402 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2404 m
.ack
= m
.MESSAGE_NACK
2409 def __init__(self
, filename
, filesize
, userHandle
):
2410 self
.consumer
= None
2411 self
.finished
= False
2414 self
.filename
, self
.filesize
, self
.userHandle
= filename
, filesize
, userHandle
2417 raise NotImplementedError
2419 def accept(self
, consumer
):
2420 if self
.consumer
: raise "AlreadyAccepted"
2421 self
.consumer
= consumer
2422 for data
in self
.buffer:
2423 self
.consumer
.write(data
)
2426 self
.consumer
.close()
2428 self
.consumer
.error()
2430 def write(self
, data
):
2431 if self
.error
or self
.finished
:
2432 raise IOError, "Attempt to write in an invalid state"
2434 self
.consumer
.write(data
)
2436 self
.buffer.append(data
)
2439 self
.finished
= True
2441 self
.consumer
.close()
2444 """ Represents the Context field for P2P file transfers """
2445 def __init__(self
, data
=""):
2453 if MSNP2PDEBUG
: print "FileContext packing:", self
.filename
, self
.filesize
2454 data
= struct
.pack("<LLQL", 574, 0x03, self
.filesize
, 0x01)
2455 data
+= self
.filename
.encode("utf-16")
2456 data
= data
.ljust(570, '\0')
2457 data
+= struct
.pack("<L", 0xFFFFFFFF)
2460 def parse(self
, packet
):
2461 self
.filesize
= struct
.unpack("<Q", packet
[8:16])[0]
2462 chunk
= packet
[20:540]
2463 chunk
= chunk
[:chunk
.find('\x00\x00')]
2464 self
.filename
= unicode(chunk
.decode("utf-16"))
2465 if MSNP2PDEBUG
: print "FileContext parsed:", self
.filesize
, self
.filename
2469 """ Utility class for the binary header & footer in p2p messages """
2478 def __init__(self
, fields
=None, packet
=None):
2480 self
.fields
= fields
2482 self
.fields
= [0] * 10
2484 self
.unpackFields(packet
)
2486 def __getitem__(self
, key
):
2487 return self
.fields
[key
]
2489 def __setitem__(self
, key
, value
):
2490 self
.fields
[key
] = value
2492 def unpackFields(self
, packet
):
2493 self
.fields
= struct
.unpack("<LLQQLLLLQ", packet
[0:48])
2494 self
.fields
+= struct
.unpack(">L", packet
[len(packet
)-4:])
2496 print "Unpacked fields:",
2497 for i
in self
.fields
:
2501 def packHeaders(self
):
2502 f
= tuple(self
.fields
)
2504 print "Packed fields:",
2505 for i
in self
.fields
:
2508 return struct
.pack("<LLQQLLLLQ", f
[0], f
[1], f
[2], f
[3], f
[4], f
[5], f
[6], f
[7], f
[8])
2510 def packFooter(self
):
2511 return struct
.pack(">L", self
.fields
[9])
2514 class MSNSLPMessage
:
2515 """ Representation of a single MSNSLP message """
2516 def __init__(self
, packet
=None):
2522 self
.sessionGuid
= ""
2523 self
.sessionID
= None
2525 self
.data
= "\r\n" + chr(0)
2529 def create(self
, method
=None, status
=None, to
=None, fro
=None, cseq
=0, sessionGuid
=None, data
=None):
2530 self
.method
= method
2531 self
.status
= status
2535 self
.sessionGuid
= sessionGuid
2536 if data
: self
.data
= data
2538 def setData(self
, ctype
, data
):
2541 order
= ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
2543 if key
== "Context" and data
.has_key(key
):
2544 s
.append("Context: %s\r\n" % b64enc(data
[key
]))
2545 elif data
.has_key(key
):
2546 s
.append("%s: %s\r\n" % (key
, str(data
[key
])))
2547 s
.append("\r\n"+chr(0))
2549 self
.data
= "".join(s
)
2553 if s
.find("MSNSLP/1.0") < 0: return
2555 lines
= s
.split("\r\n")
2557 # Get the MSNSLP method or status
2558 msnslp
= lines
[0].split(" ")
2559 if MSNP2PDEBUG
: print "Parsing MSNSLPMessage", len(s
), s
2560 if msnslp
[0] in ("INVITE", "BYE"):
2561 self
.method
= msnslp
[0].strip()
2563 self
.status
= msnslp
[1].strip()
2565 lines
.remove(lines
[0])
2568 line
= line
.split(":")
2570 if len(line
) > 2 and line
[0] == "To":
2571 self
.to
= line
[2][:line
[2].find('>')]
2572 elif len(line
) > 2 and line
[0] == "From":
2573 self
.fro
= line
[2][:line
[2].find('>')]
2574 elif line
[0] == "Call-ID":
2575 self
.sessionGuid
= line
[1].strip()
2576 elif line
[0] == "CSeq":
2577 self
.cseq
= int(line
[1].strip())
2578 elif line
[0] == "SessionID":
2579 self
.sessionID
= int(line
[1].strip())
2580 elif line
[0] == "EUF-GUID":
2581 self
.euf_guid
= line
[1].strip()
2582 elif line
[0] == "Content-Type":
2583 self
.ctype
= line
[1].strip()
2584 elif line
[0] == "Context":
2585 self
.context
= base64
.decodestring(line
[1])
2590 s
.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self
.method
, self
.to
))
2592 if self
.status
== "200": status
= "200 OK"
2593 elif self
.status
== "603": status
= "603 Decline"
2594 s
.append("MSNSLP/1.0 %s\r\n" % status
)
2595 s
.append("To: <msnmsgr:%s>\r\n" % self
.to
)
2596 s
.append("From: <msnmsgr:%s>\r\n" % self
.fro
)
2597 s
.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % random_guid())
2598 s
.append("CSeq: %s \r\n" % str(self
.cseq
))
2599 s
.append("Call-ID: %s\r\n" % self
.sessionGuid
)
2600 s
.append("Max-Forwards: 0\r\n")
2601 s
.append("Content-Type: %s\r\n" % self
.ctype
)
2602 s
.append("Content-Length: %s\r\n\r\n" % len(self
.data
))
2608 """ Utility for handling the weird sequence IDs in p2p messages """
2609 def __init__(self
, baseID
=None):
2613 self
.seqID
= random
.randint(1000, sys
.maxint
)
2620 return self
.seqID
+ self
.pos
- 3
2628 class StringBuffer(StringIO
.StringIO
):
2629 def __init__(self
, notifyFunc
=None):
2630 self
.notifyFunc
= notifyFunc
2631 StringIO
.StringIO
.__init
__(self
)
2635 self
.notifyFunc(self
.getvalue())
2636 self
.notifyFunc
= None
2637 StringIO
.StringIO
.close(self
)
2641 def __init__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
):
2643 sessionID
= random
.randint(1000, sys
.maxint
)
2645 sessionGuid
= random_guid()
2646 self
.remoteUser
= remoteUser
2647 self
.switchboard
= switchboard
2648 self
.sessionID
= sessionID
2649 self
.sessionGuid
= sessionGuid
2650 self
.seqID
= SeqID()
2654 del self
.switchboard
.slpLinks
[self
.sessionID
]
2655 self
.switchboard
= None
2656 # This is so that handleP2PMessage can still use the SLPLink
2657 # one last time, for ACKing BYEs and 601s.
2658 reactor
.callLater(0, kill
)
2660 def setError(self
, text
):
2662 print "ERROR in avatar transfer: ", self
, text
, "in state:", self
.state
2664 def warn(self
, text
):
2666 print "Warning in avatar transfer: ", self
, text
, "in state:", self
.state
2668 def sendP2PACK(self
, ackHeaders
):
2669 binaryFields
= BinaryFields()
2670 binaryFields
[1] = self
.seqID
.next()
2671 binaryFields
[3] = ackHeaders
[3]
2672 binaryFields
[5] = BinaryFields
.ACK
2673 binaryFields
[6] = ackHeaders
[1]
2674 binaryFields
[7] = ackHeaders
[6]
2675 binaryFields
[8] = ackHeaders
[3]
2676 self
.sendP2PMessage(binaryFields
, "")
2678 def sendSLPMessage(self
, cmd
, ctype
, data
):
2679 msg
= MSNSLPMessage()
2681 msg
.create(status
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, cseq
=1, sessionGuid
=self
.sessionGuid
)
2683 msg
.create(method
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, cseq
=0, sessionGuid
=self
.sessionGuid
)
2684 msg
.setData(ctype
, data
)
2686 binaryFields
= BinaryFields()
2687 binaryFields
[1] = self
.seqID
.next()
2688 binaryFields
[3] = len(msgStr
)
2689 binaryFields
[4] = binaryFields
[3]
2690 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2691 self
.sendP2PMessage(binaryFields
, msgStr
)
2693 def sendP2PMessage(self
, binaryFields
, msgStr
):
2694 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2696 message
= MSNMessage(message
=packet
)
2697 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2698 message
.setHeader("P2P-Dest", self
.remoteUser
)
2699 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2700 self
.switchboard
.sendMessage(message
)
2702 def handleSLPMessage(self
):
2703 raise NotImplementedError
2709 class SLPLink_Send(SLPLink
):
2710 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2711 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2712 self
.handlePacket
= None
2714 self
.filesize
= filesize
2717 def send_dataprep(self
):
2718 binaryFields
= BinaryFields()
2719 binaryFields
[0] = self
.sessionID
2720 binaryFields
[1] = self
.seqID
.next()
2723 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2725 self
.sendP2PMessage(binaryFields
, chr(0) * 4)
2727 def write(self
, data
):
2731 if i
+ 1202 < length
:
2732 self
._writeChunk
(data
[i
:i
+1202])
2735 self
.data
+= data
[i
:]
2736 if len(self
.data
) >= 1202:
2742 def _writeChunk(self
, chunk
):
2743 binaryFields
= BinaryFields()
2744 binaryFields
[0] = self
.sessionID
2745 binaryFields
[1] = self
.seqID
.get()
2746 binaryFields
[2] = self
.offset
2747 binaryFields
[3] = self
.filesize
2748 binaryFields
[4] = len(chunk
)
2749 binaryFields
[5] = BinaryFields
.DATA
2750 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2752 self
.offset
+= len(chunk
)
2753 self
.sendP2PMessage(binaryFields
, chunk
)
2757 self
._writeChunk
(self
.data
)
2762 # FIXME, should send 601 or something
2764 class SLPLink_FileSend(SLPLink_Send
):
2765 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
):
2766 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
)
2767 # Send invite & wait for 200OK before sending dataprep
2768 context
= FileContext()
2769 context
.filename
= filename
2770 context
.filesize
= filesize
2771 data
= {"EUF-GUID" : MSN_MSNFTP_GUID
,\
2772 "SessionID": self
.sessionID
,\
2774 "Context" : context
.pack() }
2775 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2776 self
.acceptDeferred
= Deferred()
2778 def handleSLPMessage(self
, slpMessage
):
2779 if slpMessage
.status
== "200":
2780 if slpMessage
.ctype
== "application/x-msnmsgr-sessionreqbody":
2781 data
= {"Bridges" : "TRUDPv1 TCPv1",\
2782 "NetID" : "-123657987",\
2783 "Conn-Type" : "Restrict-NAT",\
2784 "UPnPNat" : "false",\
2786 "Hashed-Nonce": random_guid()}
2787 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data
)
2788 elif slpMessage
.ctype
== "application/x-msnmsgr-transrespbody":
2789 self
.send_dataprep()
2790 self
.acceptDeferred
.callback((True,))
2792 if slpMessage
.status
== "603":
2793 self
.acceptDeferred
.callback((False,))
2794 # SLPLink is over due to decline, error or BYE
2798 SLPLink_Send
.close(self
)
2799 # FIXME, check whether we should wait for a BYE or send one
2800 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2803 class SLPLink_AvatarSend(SLPLink_Send
):
2804 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2805 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2806 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2807 self
.send_dataprep()
2810 SLPLink_Send
.close(self
)
2811 # Keep the link open to wait for a BYE
2813 class SLPLink_Receive(SLPLink
):
2814 def __init__(self
, remoteUser
, switchboard
, consumer
, context
=None, sessionID
=None, sessionGuid
=None):
2815 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2816 self
.handlePacket
= None
2817 self
.consumer
= consumer
2820 def wait_dataprep(self
, packet
):
2821 binaryFields
= BinaryFields()
2822 binaryFields
.unpackFields(packet
)
2824 if binaryFields
[0] != self
.sessionID
:
2825 self
.warn("field0," + str(binaryFields
[0]) + "," + str(self
.sessionID
))
2827 if binaryFields
[3] != 4:
2828 self
.setError("field3," + str(binaryFields
[3]))
2830 if binaryFields
[4] != 4:
2831 self
.setError("field4," + str(binaryFields
[4]))
2833 if binaryFields
[9] != 1:
2834 self
.warn("field9," + str(binaryFields
[9]))
2837 self
.sendP2PACK(binaryFields
)
2838 self
.handlePacket
= self
.wait_data
2840 def wait_data(self
, packet
):
2841 binaryFields
= BinaryFields()
2842 binaryFields
.unpackFields(packet
)
2843 if binaryFields
[0] != self
.sessionID
:
2844 self
.warn("field0," + str(binaryFields
[0]) + "," + str(self
.sessionID
))
2846 if binaryFields
[5] != BinaryFields
.DATA
:
2847 self
.setError("field5," + str(binaryFields
[5]))
2849 if binaryFields
[9] != 1:
2850 self
.warn("field9," + str(binaryFields
[9]))
2852 offset
= binaryFields
[2]
2853 total
= binaryFields
[3]
2854 length
= binaryFields
[4]
2856 data
= packet
[48:-4]
2857 if offset
!= self
.pos
:
2858 self
.setError("Received packet out of order")
2859 self
.consumer
.error()
2861 if len(data
) != length
:
2862 self
.setError("Received bad length of slp")
2863 self
.consumer
.error()
2868 self
.consumer
.write(data
)
2870 if self
.pos
== total
:
2871 self
.sendP2PACK(binaryFields
)
2872 self
.consumer
.close()
2873 self
.handlePacket
= None
2876 def doFinished(self
):
2877 raise NotImplementedError
2880 class SLPLink_FileReceive(SLPLink_Receive
, FileReceive
):
2881 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
, sessionID
, sessionGuid
):
2882 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=self
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2883 FileReceive
.__init
__(self
, filename
, filesize
, remoteUser
)
2886 # Send a 603 decline
2887 self
.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2890 def accept(self
, consumer
):
2891 FileReceive
.accept(self
, consumer
)
2892 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2894 def handleSLPMessage(self
, slpMessage
):
2895 if slpMessage
.method
== "INVITE": # The second invite
2896 data
= {"Bridge" : "TCPv1",\
2897 "Listening" : "false",\
2898 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
2899 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
)
2900 self
.handlePacket
= self
.wait_dataprep
2902 self
.killLink() # It's either a BYE or an error
2903 # FIXME, do some error handling if it was an error
2905 def doFinished(self
):
2906 pass # Link is kept around waiting for a BYE
2908 class SLPLink_AvatarReceive(SLPLink_Receive
):
2909 def __init__(self
, remoteUser
, switchboard
, consumer
, context
):
2910 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=consumer
, context
=context
)
2911 data
= {"EUF-GUID" : MSN_AVATAR_GUID
,\
2912 "SessionID": self
.sessionID
,\
2914 "Context" : context
}
2915 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2917 def handleSLPMessage(self
, slpMessage
):
2918 if slpMessage
.status
== "200":
2919 self
.handlePacket
= self
.wait_dataprep
2921 # SLPLink is over due to error or BYE
2924 def doFinished(self
):
2925 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2927 # mapping of error codes to error messages
2930 200 : "Syntax error",
2931 201 : "Invalid parameter",
2932 205 : "Invalid user",
2933 206 : "Domain name missing",
2934 207 : "Already logged in",
2935 208 : "Invalid username",
2936 209 : "Invalid screen name",
2937 210 : "User list full",
2938 215 : "User already there",
2939 216 : "User already on list",
2940 217 : "User not online",
2941 218 : "Already in mode",
2942 219 : "User is in the opposite list",
2943 223 : "Too many groups",
2944 224 : "Invalid group",
2945 225 : "User not in group",
2946 229 : "Group name too long",
2947 230 : "Cannot remove group 0",
2948 231 : "Invalid group",
2949 280 : "Switchboard failed",
2950 281 : "Transfer to switchboard failed",
2952 300 : "Required field missing",
2953 301 : "Too many FND responses",
2954 302 : "Not logged in",
2956 402 : "Error accessing contact list",
2957 403 : "Error accessing contact list",
2959 500 : "Internal server error",
2960 501 : "Database server error",
2961 502 : "Command disabled",
2962 510 : "File operation failed",
2963 520 : "Memory allocation failed",
2964 540 : "Wrong CHL value sent to server",
2966 600 : "Server is busy",
2967 601 : "Server is unavaliable",
2968 602 : "Peer nameserver is down",
2969 603 : "Database connection failed",
2970 604 : "Server is going down",
2971 605 : "Server unavailable",
2973 707 : "Could not create connection",
2974 710 : "Invalid CVR parameters",
2975 711 : "Write is blocking",
2976 712 : "Session is overloaded",
2977 713 : "Too many active users",
2978 714 : "Too many sessions",
2979 715 : "Not expected",
2980 717 : "Bad friend file",
2981 731 : "Not expected",
2983 800 : "Requests too rapid",
2985 910 : "Server too busy",
2986 911 : "Authentication failed",
2987 912 : "Server too busy",
2988 913 : "Not allowed when offline",
2989 914 : "Server too busy",
2990 915 : "Server too busy",
2991 916 : "Server too busy",
2992 917 : "Server too busy",
2993 918 : "Server too busy",
2994 919 : "Server too busy",
2995 920 : "Not accepting new users",
2996 921 : "Server too busy",
2997 922 : "Server too busy",
2998 923 : "No parent consent",
2999 924 : "Passport account not yet verified"
3003 # mapping of status codes to readable status format
3006 STATUS_ONLINE
: "Online",
3007 STATUS_OFFLINE
: "Offline",
3008 STATUS_HIDDEN
: "Appear Offline",
3009 STATUS_IDLE
: "Idle",
3010 STATUS_AWAY
: "Away",
3011 STATUS_BUSY
: "Busy",
3012 STATUS_BRB
: "Be Right Back",
3013 STATUS_PHONE
: "On the Phone",
3014 STATUS_LUNCH
: "Out to Lunch"
3018 # mapping of list ids to list codes
3021 FORWARD_LIST
: 'fl',
3024 REVERSE_LIST
: 'rl',
3029 # mapping of list codes to list ids
3031 for id,code
in listIDToCode
.items():
3032 listCodeToID
[code
] = id