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
, array
, codecs
, binascii
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 P2PSEQ
= [-3, -2, 0, -1, 1, 2, 3, 4, 5, 6, 7, 8]
174 return inp
.split('=')[1]
186 userHandle
= getVal(p
)
188 screenName
= unquote(getVal(p
))
193 else: # Must be the groups
195 groups
= p
.split(',')
197 raise MSNProtocolError
, "Unknown LST/ADC response" + str(params
) # debug
199 return userHandle
, screenName
, userGuid
, lists
, groups
202 """ Needed for Python 2.3 compatibility """
203 return s
+ (n
-len(s
))*c
205 if sys
.byteorder
== "little":
207 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
208 a
= array
.array("h", s
.encode("utf-16")[2:])
213 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
214 return s
.encode("utf-16")[2:]
217 return base64
.encodestring(s
).replace("\n", "")
220 for pad
in ["", "=", "==", "A", "A=", "A=="]: # Stupid MSN client!
222 return base64
.decodestring(s
+ pad
)
225 raise ValueError("Got some very bad base64!")
228 format
= "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
231 data
.append(random
.random() * 0xAAFF + 0x1111)
236 def checkParamLen(num
, expected
, cmd
, error
=None):
237 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
238 if num
!= expected
: raise MSNProtocolError
, error
240 def _parseHeader(h
, v
):
242 Split a certin number of known
243 header values with the format:
244 field1=val,field2=val,field3=val into
245 a dict mapping fields to values.
246 @param h: the header's key
247 @param v: the header's value as a string
250 if h
in ('passporturls','authentication-info','www-authenticate'):
251 v
= v
.replace('Passport1.4','').lstrip()
253 for fieldPair
in v
.split(','):
255 field
,value
= fieldPair
.split('=',1)
256 fields
[field
.lower()] = value
258 fields
[field
.lower()] = ''
262 def _parsePrimitiveHost(host
):
264 h
,p
= host
.replace('https://','').split('/',1)
268 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
=''):
270 This function is used internally and should not ever be called
274 def _cb(server
, auth
):
275 loginFac
= ClientFactory()
276 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
277 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
280 _cb(nexusServer
, authData
)
282 fac
= ClientFactory()
284 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
285 d
.addErrback(lambda f
: cb
.errback(f
))
286 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
287 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
291 class PassportNexus(HTTPClient
):
294 Used to obtain the URL of a valid passport
297 This class is used internally and should
298 not be instantiated directly -- that is,
299 The passport logging in process is handled
300 transparantly by NotificationClient.
303 def __init__(self
, deferred
, host
):
304 self
.deferred
= deferred
305 self
.host
, self
.path
= _parsePrimitiveHost(host
)
307 def connectionMade(self
):
308 HTTPClient
.connectionMade(self
)
309 self
.sendCommand('GET', self
.path
)
310 self
.sendHeader('Host', self
.host
)
314 def handleHeader(self
, header
, value
):
316 self
.headers
[h
] = _parseHeader(h
, value
)
318 def handleEndHeaders(self
):
319 if self
.connected
: self
.transport
.loseConnection()
320 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
321 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
323 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
325 def handleResponse(self
, r
): pass
327 class PassportLogin(HTTPClient
):
329 This class is used internally to obtain
330 a login ticket from a passport HTTPS
331 server -- it should not be used directly.
336 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
337 self
.deferred
= deferred
338 self
.userHandle
= userHandle
340 self
.authData
= authData
341 self
.host
, self
.path
= _parsePrimitiveHost(host
)
343 def connectionMade(self
):
344 self
.sendCommand('GET', self
.path
)
345 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
346 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), self
.passwd
,self
.authData
))
347 self
.sendHeader('Host', self
.host
)
351 def handleHeader(self
, header
, value
):
353 self
.headers
[h
] = _parseHeader(h
, value
)
355 def handleEndHeaders(self
):
356 if self
._finished
: return
357 self
._finished
= 1 # I think we need this because of HTTPClient
358 if self
.connected
: self
.transport
.loseConnection()
359 authHeader
= 'authentication-info'
360 _interHeader
= 'www-authenticate'
361 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
363 info
= self
.headers
[authHeader
]
364 status
= info
['da-status']
365 handler
= getattr(self
, 'login_%s' % (status
,), None)
368 else: raise Exception()
370 self
.deferred
.errback(failure
.Failure(e
))
372 def handleResponse(self
, r
): pass
374 def login_success(self
, info
):
375 ticket
= info
['from-pp']
376 ticket
= ticket
[1:len(ticket
)-1]
377 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
379 def login_failed(self
, info
):
380 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
382 def login_redir(self
, info
):
383 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
385 class MSNProtocolError(Exception):
387 This Exception is basically used for debugging
388 purposes, as the official MSN server should never
389 send anything _wrong_ and nobody in their right
390 mind would run their B{own} MSN server.
391 If it is raised by default command handlers
392 (handle_BLAH) the error will be logged.
399 I am the class used to represent an 'instant' message.
401 @ivar userHandle: The user handle (passport) of the sender
402 (this is only used when receiving a message)
403 @ivar screenName: The screen name of the sender (this is only used
404 when receiving a message)
405 @ivar message: The message
406 @ivar headers: The message headers
408 @ivar length: The message length (including headers and line endings)
409 @ivar ack: This variable is used to tell the server how to respond
410 once the message has been sent. If set to MESSAGE_ACK
411 (default) the server will respond with an ACK upon receiving
412 the message, if set to MESSAGE_NACK the server will respond
413 with a NACK upon failure to receive the message.
414 If set to MESSAGE_ACK_NONE the server will do nothing.
415 This is relevant for the return value of
416 SwitchboardClient.sendMessage (which will return
417 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
418 and will fire when the respective ACK or NACK is received).
419 If set to MESSAGE_ACK_NONE sendMessage will return None.
422 MESSAGE_ACK_FAT
= 'D'
424 MESSAGE_ACK_NONE
= 'U'
428 def __init__(self
, length
=0, userHandle
="", screenName
="", message
="", specialMessage
=False):
429 self
.userHandle
= userHandle
430 self
.screenName
= screenName
431 self
.specialMessage
= specialMessage
432 self
.message
= message
433 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
437 def _calcMessageLen(self
):
439 used to calculte the number to send
440 as the message length when sending a message.
442 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
444 def setHeader(self
, header
, value
):
445 """ set the desired header """
446 self
.headers
[header
] = value
448 def getHeader(self
, header
):
450 get the desired header value
451 @raise KeyError: if no such header exists.
453 return self
.headers
[header
]
455 def hasHeader(self
, header
):
456 """ check to see if the desired header exists """
457 return self
.headers
.has_key(header
)
459 def getMessage(self
):
460 """ return the message - not including headers """
463 def setMessage(self
, message
):
464 """ set the message text """
465 self
.message
= message
470 Used to represent a MSNObject. This can be currently only be an avatar.
472 @ivar creator: The userHandle of the creator of this picture.
473 @ivar imageData: The PNG image data (only for our own avatar)
474 @ivar type: Always set to 3, for avatar.
475 @ivar size: The size of the image.
476 @ivar location: The filename of the image.
477 @ivar friendly: Unknown.
478 @ivar text: The textual representation of this MSNObject.
480 def __init__(self
, s
=""):
481 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
487 def setData(self
, creator
, imageData
):
488 """ Set the creator and imageData for this object """
489 self
.creator
= creator
490 self
.imageData
= imageData
491 self
.size
= len(imageData
)
493 self
.location
= "TMP" + str(random
.randint(1000,9999))
494 self
.friendly
= "AAA="
495 self
.sha1d
= b64enc(sha
.sha(imageData
).digest())
509 """ Makes a textual representation of this MSNObject. Stores it in self.text """
512 h
.append(self
.creator
)
514 h
.append(str(self
.size
))
516 h
.append(str(self
.type))
518 h
.append(self
.location
)
520 h
.append(self
.friendly
)
523 sha1c
= b64enc(sha
.sha("".join(h
)).digest())
524 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
)
527 e
= xmlw
.parseText(s
, True)
528 self
.creator
= e
.getAttribute("Creator")
529 self
.size
= int(e
.getAttribute("Size"))
530 self
.type = int(e
.getAttribute("Type"))
531 self
.location
= e
.getAttribute("Location")
532 self
.friendly
= e
.getAttribute("Friendly")
533 self
.sha1d
= e
.getAttribute("SHA1D")
540 This class represents a contact (user).
542 @ivar userGuid: The contact's user guid (unique string)
543 @ivar userHandle: The contact's user handle (passport).
544 @ivar screenName: The contact's screen name.
545 @ivar groups: A list of all the group IDs which this
547 @ivar lists: An integer representing the sum of all lists
548 that this contact belongs to.
549 @ivar caps: int, The capabilities of this client
550 @ivar msnobj: The MSNObject representing the contact's avatar
551 @ivar status: The contact's status code.
552 @type status: str if contact's status is known, None otherwise.
553 @ivar personal: The contact's personal message .
554 @type personal: str if contact's personal message is known, None otherwise.
556 @ivar homePhone: The contact's home phone number.
557 @type homePhone: str if known, otherwise None.
558 @ivar workPhone: The contact's work phone number.
559 @type workPhone: str if known, otherwise None.
560 @ivar mobilePhone: The contact's mobile phone number.
561 @type mobilePhone: str if known, otherwise None.
562 @ivar hasPager: Whether or not this user has a mobile pager
563 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
571 def __init__(self
, userGuid
="", userHandle
="", screenName
="", lists
=0, caps
=0, msnobj
=None, groups
={}, status
=None, personal
=""):
572 self
.userGuid
= userGuid
573 self
.userHandle
= userHandle
574 self
.screenName
= screenName
578 self
.msnobjGot
= True
579 self
.groups
= [] # if applicable
580 self
.status
= status
# current status
581 self
.personal
= personal
584 self
.homePhone
= None
585 self
.workPhone
= None
586 self
.mobilePhone
= None
590 def setPhone(self
, phoneType
, value
):
592 set phone numbers/values for this specific user.
593 for phoneType check the *_PHONE constants and HAS_PAGER
596 t
= phoneType
.upper()
597 if t
== HOME_PHONE
: self
.homePhone
= value
598 elif t
== WORK_PHONE
: self
.workPhone
= value
599 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
600 elif t
== HAS_PAGER
: self
.hasPager
= value
601 elif t
== HAS_BLOG
: self
.hasBlog
= value
602 #else: raise ValueError, "Invalid Phone Type: " + t
604 def addToList(self
, listType
):
606 Update the lists attribute to
607 reflect being part of the
610 self
.lists |
= listType
612 def removeFromList(self
, listType
):
614 Update the lists attribute to
615 reflect being removed from the
618 self
.lists ^
= listType
620 class MSNContactList
:
622 This class represents a basic MSN contact list.
624 @ivar contacts: All contacts on my various lists
625 @type contacts: dict (mapping user handles to MSNContact objects)
626 @ivar groups: a mapping of group ids to group names
627 (groups can only exist on the forward list)
631 This is used only for storage and doesn't effect the
632 server's contact list.
642 def _getContactsFromList(self
, listType
):
644 Obtain all contacts which belong
645 to the given list type.
647 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
649 def addContact(self
, contact
):
653 self
.contacts
[contact
.userHandle
] = contact
655 def remContact(self
, userHandle
):
660 del self
.contacts
[userHandle
]
661 except KeyError: pass
663 def getContact(self
, userHandle
):
665 Obtain the MSNContact object
666 associated with the given
668 @return: the MSNContact object if
669 the user exists, or None.
672 return self
.contacts
[userHandle
]
676 def getBlockedContacts(self
):
678 Obtain all the contacts on my block list
680 return self
._getContactsFromList
(BLOCK_LIST
)
682 def getAuthorizedContacts(self
):
684 Obtain all the contacts on my auth list.
685 (These are contacts which I have verified
686 can view my state changes).
688 return self
._getContactsFromList
(ALLOW_LIST
)
690 def getReverseContacts(self
):
692 Get all contacts on my reverse list.
693 (These are contacts which have added me
694 to their forward list).
696 return self
._getContactsFromList
(REVERSE_LIST
)
698 def getContacts(self
):
700 Get all contacts on my forward list.
701 (These are the contacts which I have added
704 return self
._getContactsFromList
(FORWARD_LIST
)
706 def setGroup(self
, id, name
):
708 Keep a mapping from the given id
711 self
.groups
[id] = name
713 def remGroup(self
, id):
715 Removed the stored group
716 mapping for the given id.
720 except KeyError: pass
721 for c
in self
.contacts
:
722 if id in c
.groups
: c
.groups
.remove(id)
725 class MSNEventBase(LineReceiver
):
727 This class provides support for handling / dispatching events and is the
728 base class of the three main client protocols (DispatchClient,
729 NotificationClient, SwitchboardClient)
733 self
.ids
= {} # mapping of ids to Deferreds
737 self
.currentMessage
= None
739 def connectionLost(self
, reason
):
743 def connectionMade(self
):
746 def _fireCallback(self
, id, *args
):
748 Fire the callback for the given id
749 if one exists and return 1, else return false
751 if self
.ids
.has_key(id):
752 self
.ids
[id][0].callback(args
)
757 def _nextTransactionID(self
):
758 """ return a usable transaction ID """
760 if self
.currentID
> 1000: self
.currentID
= 1
761 return self
.currentID
763 def _createIDMapping(self
, data
=None):
765 return a unique transaction ID that is mapped internally to a
766 deferred .. also store arbitrary data if it is needed
768 id = self
._nextTransactionID
()
770 self
.ids
[id] = (d
, data
)
773 def checkMessage(self
, message
):
775 process received messages to check for file invitations and
776 typing notifications and other control type messages
778 raise NotImplementedError
780 def sendLine(self
, line
):
781 if LINEDEBUG
: log
.msg(">> " + line
)
782 LineReceiver
.sendLine(self
, line
)
784 def lineReceived(self
, line
):
785 if LINEDEBUG
: log
.msg("<< " + line
)
786 if self
.currentMessage
:
787 self
.currentMessage
.readPos
+= len(line
+"\r\n")
789 header
, value
= line
.split(':')
790 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
793 #raise MSNProtocolError, "Invalid Message Header"
795 if line
== "" or self
.currentMessage
.specialMessage
:
797 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
800 cmd
, params
= line
.split(' ', 1)
802 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
804 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
806 if self
.ids
.has_key(params
.split(' ')[0]):
807 self
.ids
[id].errback(int(cmd
))
810 else: # we received an error which doesn't map to a sent command
811 self
.gotError(int(cmd
))
814 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
816 try: handler(params
.split(' '))
817 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
819 self
.handle_UNKNOWN(cmd
, params
.split(' '))
821 def rawDataReceived(self
, data
):
823 self
.currentMessage
.readPos
+= len(data
)
824 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
826 self
.currentMessage
.message
+= data
[:-diff
]
829 self
.currentMessage
.message
+= data
831 self
.currentMessage
.message
+= data
833 del self
.currentMessage
.readPos
834 m
= self
.currentMessage
835 self
.currentMessage
= None
836 if MESSAGEDEBUG
: log
.msg(m
.message
)
838 if not self
.checkMessage(m
):
839 self
.setLineMode(extra
)
842 log
.msg("Traceback - ERROR in checkMessage: " + str(e
))
843 self
.setLineMode(extra
)
846 self
.setLineMode(extra
)
848 ### protocol command handlers - no need to override these.
850 def handle_MSG(self
, params
):
851 checkParamLen(len(params
), 3, 'MSG')
853 messageLen
= int(params
[2])
854 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
855 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
857 def handle_UNKNOWN(self
, cmd
, params
):
858 """ implement me in subclasses if you want to handle unknown events """
859 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
863 def gotBadLine(self
, line
, why
):
864 """ called when a handler notifies me that this line is broken """
865 log
.msg('Error in line: %s (%s)' % (line
, why
))
867 def gotError(self
, errorCode
):
869 called when the server sends an error which is not in
870 response to a sent command (ie. it has no matching transaction ID)
872 log
.msg('Error %s' % (errorCodes
[errorCode
]))
875 class DispatchClient(MSNEventBase
):
877 This class provides support for clients connecting to the dispatch server
878 @ivar userHandle: your user handle (passport) needed before connecting.
881 def connectionMade(self
):
882 MSNEventBase
.connectionMade(self
)
883 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
885 ### protocol command handlers ( there is no need to override these )
887 def handle_VER(self
, params
):
888 versions
= params
[1:]
889 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
890 self
.transport
.loseConnection()
891 raise MSNProtocolError
, "Invalid version response"
892 id = self
._nextTransactionID
()
893 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.factory
.userHandle
))
895 def handle_CVR(self
, params
):
896 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
898 def handle_XFR(self
, params
):
899 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
900 id, refType
, addr
= params
[:3]
901 # was addr a host:port pair?
903 host
, port
= addr
.split(':')
908 self
.gotNotificationReferral(host
, int(port
))
912 def gotNotificationReferral(self
, host
, port
):
914 called when we get a referral to the notification server.
916 @param host: the notification server's hostname
917 @param port: the port to connect to
922 class DispatchFactory(ClientFactory
):
924 This class keeps the state for the DispatchClient.
926 @ivar userHandle: the userHandle to request a notification
929 protocol
= DispatchClient
934 class NotificationClient(MSNEventBase
):
936 This class provides support for clients connecting
937 to the notification server.
940 factory
= None # sssh pychecker
942 def __init__(self
, currentID
=0):
943 MSNEventBase
.__init
__(self
)
944 self
.currentID
= currentID
945 self
._state
= ['DISCONNECTED', {}]
947 self
.pingCheckTask
= None
948 self
.msnobj
= MSNObject()
950 def _setState(self
, state
):
951 self
._state
[0] = state
954 return self
._state
[0]
956 def _getStateData(self
, key
):
957 return self
._state
[1][key
]
959 def _setStateData(self
, key
, value
):
960 self
._state
[1][key
] = value
962 def _remStateData(self
, *args
):
963 for key
in args
: del self
._state
[1][key
]
965 def connectionMade(self
):
966 MSNEventBase
.connectionMade(self
)
967 self
._setState
('CONNECTED')
968 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
970 def connectionLost(self
, reason
):
971 self
._setState
('DISCONNECTED')
973 if self
.pingCheckTask
:
974 self
.pingCheckTask
.stop()
975 self
.pingCheckTask
= None
976 MSNEventBase
.connectionLost(self
, reason
)
978 def _getEmailFields(self
, message
):
979 fields
= message
.getMessage().strip().split('\n')
983 if len(a
) != 2: continue
990 def _gotInitialEmailNotification(self
, message
):
991 values
= self
._getEmailFields
(message
)
993 inboxunread
= int(values
["Inbox-Unread"])
994 foldersunread
= int(values
["Folders-Unread"])
997 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
998 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
1000 def _gotEmailNotification(self
, message
):
1001 values
= self
._getEmailFields
(message
)
1003 mailfrom
= values
["From"]
1004 fromaddr
= values
["From-Addr"]
1005 subject
= values
["Subject"]
1006 junkbeginning
= "=?\"us-ascii\"?Q?"
1008 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
1010 # If any of the fields weren't found then it's not a big problem. We just ignore the message
1012 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
1014 def _gotMSNAlert(self
, message
):
1015 notification
= xmlw
.parseText(message
.message
, beExtremelyLenient
=True)
1016 siteurl
= notification
.getAttribute("siteurl")
1017 notid
= notification
.getAttribute("id")
1020 for e
in notification
.elements():
1026 msgid
= msg
.getAttribute("id")
1031 for e
in msg
.elements():
1032 if e
.name
== "ACTION":
1033 action
= e
.getAttribute("url")
1034 if e
.name
== "SUBSCR":
1035 subscr
= e
.getAttribute("url")
1036 if e
.name
== "BODY":
1037 for e2
in e
.elements():
1038 if e2
.name
== "TEXT":
1039 bodytext
= e2
.__str__()
1040 if not (action
and subscr
and bodytext
): return
1042 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
1043 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
1045 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
1047 def _gotUBX(self
, message
):
1048 lm
= message
.message
.lower()
1049 p1
= lm
.find("<psm>") + 5
1050 p2
= lm
.find("</psm>")
1051 if p1
>= 0 and p2
>= 0:
1052 personal
= unescapeFromXml(message
.message
[p1
:p2
])
1053 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
1054 if not msnContact
: return
1055 msnContact
.personal
= personal
1056 self
.contactPersonalChanged(message
.userHandle
, 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])
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])
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
.contactPersonalChanged(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 if params
[0] == "OTH": self
.multipleLogin()
1382 elif params
[0] == "SSD": self
.serverGoingDown()
1383 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1385 def handle_QNG(self
, params
):
1386 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1390 def pingChecker(self
):
1391 if self
.pingCounter
> 5:
1392 # The server has ignored 5 pings, lets kill the connection
1393 self
.transport
.loseConnection()
1395 self
.sendLine("PNG")
1396 self
.pingCounter
+= 1
1398 def pingCheckerStart(self
, *args
):
1399 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1400 self
.pingCheckTask
.start(PINGSPEED
)
1402 def loggedIn(self
, userHandle
, verified
):
1404 Called when the client has logged in.
1405 The default behaviour of this method is to
1406 update the factory with our screenName and
1407 to sync the contact list (factory.contacts).
1408 When this is complete self.listSynchronized
1411 @param userHandle: our userHandle
1412 @param verified: 1 if our passport has been (verified), 0 if not.
1413 (i'm not sure of the significace of this)
1417 d
.addCallback(self
.listSynchronized
)
1418 d
.addCallback(self
.pingCheckerStart
)
1420 def loginFailure(self
, message
):
1422 Called when the client fails to login.
1424 @param message: a message indicating the problem that was encountered
1428 def gotProfile(self
, message
):
1430 Called after logging in when the server sends an initial
1431 message with MSN/passport specific profile information
1432 such as country, number of kids, etc.
1433 Check the message headers for the specific values.
1435 @param message: The profile message
1439 def listSynchronized(self
, *args
):
1441 Lists are now synchronized by default upon logging in, this
1442 method is called after the synchronization has finished
1443 and the factory now has the up-to-date contacts.
1447 def contactAvatarChanged(self
, userHandle
, hash):
1449 Called when we receive the first, or a new <msnobj/> from a
1452 @param userHandle: contact who's msnobj has been changed
1453 @param hash: sha1 hash of their avatar as hex string
1456 def statusChanged(self
, statusCode
):
1458 Called when our status changes and its not in response to a
1461 @param statusCode: 3-letter status code
1465 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
1467 Called when we receive a list of statuses upon login.
1469 @param userHandle: the contact's user handle (passport)
1470 @param statusCode: 3-letter status code
1471 @param screenName: the contact's screen name
1475 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
1477 Called when we're notified that a contact's status has changed.
1479 @param userHandle: the contact's user handle (passport)
1480 @param statusCode: 3-letter status code
1481 @param screenName: the contact's screen name
1485 def contactPersonalChanged(self
, userHandle
, personal
):
1487 Called when a contact's personal message changes.
1489 @param userHandle: the contact who changed their personal message
1490 @param personal : the new personal message
1494 def contactOffline(self
, userHandle
):
1496 Called when a contact goes offline.
1498 @param userHandle: the contact's user handle
1502 def gotMessage(self
, message
):
1504 Called when there is a message from the notification server
1505 that is not understood by default.
1507 @param message: the MSNMessage.
1511 def gotMSNAlert(self
, body
, action
, subscr
):
1513 Called when the server sends an MSN Alert (http://alerts.msn.com)
1515 @param body : the alert text
1516 @param action: a URL with more information for the user to view
1517 @param subscr: a URL the user can use to modify their alert subscription
1521 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1523 Called when the server sends you details about your hotmail
1524 inbox. This is only ever called once, on login.
1526 @param inboxunread : the number of unread items in your inbox
1527 @param foldersunread: the number of unread items in other folders
1531 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1533 Called when the server sends us realtime email
1534 notification. This means that you have received
1535 a new email in your hotmail inbox.
1537 @param mailfrom: the sender of the email
1538 @param fromaddr: the sender of the email (I don't know :P)
1539 @param subject : the email subject
1543 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1545 Called when the server sends us phone details about
1546 a specific user (for example after a user is added
1547 the server will send their status, phone details etc.
1549 @param userHandle: the contact's user handle (passport)
1550 @param phoneType: the specific phoneType
1551 (*_PHONE constants or HAS_PAGER)
1552 @param number: the value/phone number.
1556 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1558 Called when a user adds me to their list. (ie. they have been added to
1561 @param userHandle: the userHandle of the user
1562 @param screenName: the screen name of the user
1566 def userRemovedMe(self
, userHandle
):
1568 Called when a user removes us from their contact list
1569 (they are no longer on our reverseContacts list.
1571 @param userHandle: the contact's user handle (passport)
1575 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1576 key
, userHandle
, screenName
):
1578 Called when we get an invitation to a switchboard server.
1579 This happens when a user requests a chat session with us.
1581 @param sessionID: session ID number, must be remembered for logging in
1582 @param host: the hostname of the switchboard server
1583 @param port: the port to connect to
1584 @param key: used for authorization when connecting
1585 @param userHandle: the user handle of the person who invited us
1586 @param screenName: the screen name of the person who invited us
1590 def multipleLogin(self
):
1592 Called when the server says there has been another login
1593 under our account, the server should disconnect us right away.
1597 def serverGoingDown(self
):
1599 Called when the server has notified us that it is going down for
1606 def changeStatus(self
, status
):
1608 Change my current status. This method will add
1609 a default callback to the returned Deferred
1610 which will update the status attribute of the
1613 @param status: 3-letter status code (as defined by
1614 the STATUS_* constants)
1615 @return: A Deferred, the callback of which will be
1616 fired when the server confirms the change
1617 of status. The callback argument will be
1618 a tuple with the new status code as the
1622 id, d
= self
._createIDMapping
()
1623 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1625 self
.factory
.status
= r
[0]
1627 return d
.addCallback(_cb
)
1629 def setPrivacyMode(self
, privLevel
):
1631 Set my privacy mode on the server.
1634 This only keeps the current privacy setting on
1635 the server for later retrieval, it does not
1636 effect the way the server works at all.
1638 @param privLevel: This parameter can be true, in which
1639 case the server will keep the state as
1640 'al' which the official client interprets
1641 as -> allow messages from only users on
1642 the allow list. Alternatively it can be
1643 false, in which case the server will keep
1644 the state as 'bl' which the official client
1645 interprets as -> allow messages from all
1646 users except those on the block list.
1648 @return: A Deferred, the callback of which will be fired when
1649 the server replies with the new privacy setting.
1650 The callback argument will be a tuple, the only element
1651 of which being either 'al' or 'bl' (the new privacy setting).
1654 id, d
= self
._createIDMapping
()
1655 if privLevel
: self
.sendLine("BLP %s AL" % id)
1656 else: self
.sendLine("BLP %s BL" % id)
1661 Used for keeping an up-to-date contact list.
1662 A callback is added to the returned Deferred
1663 that updates the contact list on the factory
1664 and also sets my state to STATUS_ONLINE.
1667 This is called automatically upon signing
1668 in using the version attribute of
1669 factory.contacts, so you may want to persist
1670 this object accordingly. Because of this there
1671 is no real need to ever call this method
1674 @return: A Deferred, the callback of which will be
1675 fired when the server sends an adequate reply.
1676 The callback argument will be a tuple with two
1677 elements, the new list (MSNContactList) and
1678 your current state (a dictionary). If the version
1679 you sent _was_ the latest list version, both elements
1680 will be None. To just request the list send a version of 0.
1683 self
._setState
('SYNC')
1684 id, d
= self
._createIDMapping
(data
=None)
1685 self
._setStateData
('synid',id)
1686 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1688 self
.changeStatus(STATUS_ONLINE
)
1689 if r
[0] is not None:
1690 self
.factory
.contacts
= r
[0]
1692 return d
.addCallback(_cb
)
1694 def setPhoneDetails(self
, phoneType
, value
):
1696 Set/change my phone numbers stored on the server.
1698 @param phoneType: phoneType can be one of the following
1699 constants - HOME_PHONE, WORK_PHONE,
1700 MOBILE_PHONE, HAS_PAGER.
1701 These are pretty self-explanatory, except
1702 maybe HAS_PAGER which refers to whether or
1703 not you have a pager.
1704 @param value: for all of the *_PHONE constants the value is a
1705 phone number (str), for HAS_PAGER accepted values
1706 are 'Y' (for yes) and 'N' (for no).
1708 @return: A Deferred, the callback for which will be fired when
1709 the server confirms the change has been made. The
1710 callback argument will be a tuple with 2 elements, the
1711 first being the new list version (int) and the second
1712 being the new phone number value (str).
1714 raise "ProbablyDoesntWork"
1715 # XXX: Add a default callback which updates
1716 # factory.contacts.version and the relevant phone
1718 id, d
= self
._createIDMapping
()
1719 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1722 def addListGroup(self
, name
):
1724 Used to create a new list group.
1725 A default callback is added to the
1726 returned Deferred which updates the
1727 contacts attribute of the factory.
1729 @param name: The desired name of the new group.
1731 @return: A Deferred, the callbacck for which will be called
1732 when the server clarifies that the new group has been
1733 created. The callback argument will be a tuple with 3
1734 elements: the new list version (int), the new group name
1735 (str) and the new group ID (int).
1738 raise "ProbablyDoesntWork"
1739 id, d
= self
._createIDMapping
()
1740 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1742 if self
.factory
.contacts
:
1743 self
.factory
.contacts
.version
= r
[0]
1744 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1746 return d
.addCallback(_cb
)
1748 def remListGroup(self
, groupID
):
1750 Used to remove a list group.
1751 A default callback is added to the
1752 returned Deferred which updates the
1753 contacts attribute of the factory.
1755 @param groupID: the ID of the desired group to be removed.
1757 @return: A Deferred, the callback for which will be called when
1758 the server clarifies the deletion of the group.
1759 The callback argument will be a tuple with 2 elements:
1760 the new list version (int) and the group ID (int) of
1764 raise "ProbablyDoesntWork"
1765 id, d
= self
._createIDMapping
()
1766 self
.sendLine("RMG %s %s" % (id, groupID
))
1768 self
.factory
.contacts
.version
= r
[0]
1769 self
.factory
.contacts
.remGroup(r
[1])
1771 return d
.addCallback(_cb
)
1773 def renameListGroup(self
, groupID
, newName
):
1775 Used to rename an existing list group.
1776 A default callback is added to the returned
1777 Deferred which updates the contacts attribute
1780 @param groupID: the ID of the desired group to rename.
1781 @param newName: the desired new name for the group.
1783 @return: A Deferred, the callback for which will be called
1784 when the server clarifies the renaming.
1785 The callback argument will be a tuple of 3 elements,
1786 the new list version (int), the group id (int) and
1787 the new group name (str).
1790 raise "ProbablyDoesntWork"
1791 id, d
= self
._createIDMapping
()
1792 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1794 self
.factory
.contacts
.version
= r
[0]
1795 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1797 return d
.addCallback(_cb
)
1799 def addContact(self
, listType
, userHandle
):
1801 Used to add a contact to the desired list.
1802 A default callback is added to the returned
1803 Deferred which updates the contacts attribute of
1804 the factory with the new contact information.
1805 If you are adding a contact to the forward list
1806 and you want to associate this contact with multiple
1807 groups then you will need to call this method for each
1808 group you would like to add them to, changing the groupID
1809 parameter. The default callback will take care of updating
1810 the group information on the factory's contact list.
1812 @param listType: (as defined by the *_LIST constants)
1813 @param userHandle: the user handle (passport) of the contact
1816 @return: A Deferred, the callback for which will be called when
1817 the server has clarified that the user has been added.
1818 The callback argument will be a tuple with 4 elements:
1819 the list type, the contact's user handle, the new list
1820 version, and the group id (if relevant, otherwise it
1824 id, d
= self
._createIDMapping
()
1825 try: # Make sure the contact isn't actually on the list
1826 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1827 except AttributeError: pass
1828 listType
= listIDToCode
[listType
].upper()
1829 if listType
== "FL":
1830 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1832 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1835 if not self
.factory
: return
1836 c
= self
.factory
.contacts
.getContact(r
[2])
1838 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1839 self
.factory
.contacts
.addContact(c
)
1840 #if r[3]: c.groups.append(r[3])
1843 return d
.addCallback(_cb
)
1845 def remContact(self
, listType
, userHandle
):
1847 Used to remove a contact from the desired list.
1848 A default callback is added to the returned deferred
1849 which updates the contacts attribute of the factory
1850 to reflect the new contact information.
1852 @param listType: (as defined by the *_LIST constants)
1853 @param userHandle: the user handle (passport) of the
1854 contact being removed
1856 @return: A Deferred, the callback for which will be called when
1857 the server has clarified that the user has been removed.
1858 The callback argument will be a tuple of 3 elements:
1859 the list type, the contact's user handle and the group ID
1860 (if relevant, otherwise it will be None)
1863 id, d
= self
._createIDMapping
()
1864 try: # Make sure the contact is actually on this list
1865 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1866 except AttributeError: return
1867 listType
= listIDToCode
[listType
].upper()
1868 if listType
== "FL":
1870 c
= self
.factory
.contacts
.getContact(userHandle
)
1871 userGuid
= c
.userGuid
1872 except AttributeError: return
1873 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1875 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1878 if listType
== "FL":
1879 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1880 l
= self
.factory
.contacts
1881 c
= l
.getContact(r
[1])
1885 if group
: # they may not have been removed from the list
1886 c
.groups
.remove(group
)
1887 if c
.groups
: shouldRemove
= 0
1889 c
.removeFromList(r
[0])
1890 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1892 return d
.addCallback(_cb
)
1894 def changeScreenName(self
, newName
):
1896 Used to change your current screen name.
1897 A default callback is added to the returned
1898 Deferred which updates the screenName attribute
1899 of the factory and also updates the contact list
1902 @param newName: the new screen name
1904 @return: A Deferred, the callback for which will be called
1905 when the server acknowledges the change.
1906 The callback argument will be an empty tuple.
1909 id, d
= self
._createIDMapping
()
1910 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1912 self
.factory
.screenName
= newName
1914 return d
.addCallback(_cb
)
1916 def changePersonalMessage(self
, personal
):
1918 Used to change your personal message.
1920 @param personal: the new screen name
1922 @return: A Deferred, the callback for which will be called
1923 when the server acknowledges the change.
1924 The callback argument will be a tuple of 1 element,
1925 the personal message.
1928 id, d
= self
._createIDMapping
()
1931 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1932 self
.sendLine("UUX %s %s" % (id, len(data
)))
1933 self
.transport
.write(data
)
1935 self
.factory
.personal
= personal
1937 return d
.addCallback(_cb
)
1939 def changeAvatar(self
, imageData
, push
):
1941 Used to change the avatar that other users see.
1943 @param imageData: the PNG image data to set as the avatar
1944 @param push : whether to push the update to the server
1945 (it will otherwise be sent with the next
1948 @return: If push==True, a Deferred, the callback for which
1949 will be called when the server acknowledges the change.
1950 The callback argument will be the same as for changeStatus.
1953 if self
.msnobj
and imageData
== self
.msnobj
.imageData
: return
1955 self
.msnobj
.setData(self
.factory
.userHandle
, imageData
)
1957 self
.msnobj
.setNull()
1958 if push
: return self
.changeStatus(self
.factory
.status
) # Push to server
1961 def requestSwitchboardServer(self
):
1963 Used to request a switchboard server to use for conversations.
1965 @return: A Deferred, the callback for which will be called when
1966 the server responds with the switchboard information.
1967 The callback argument will be a tuple with 3 elements:
1968 the host of the switchboard server, the port and a key
1969 used for logging in.
1972 id, d
= self
._createIDMapping
()
1973 self
.sendLine("XFR %s SB" % id)
1978 Used to log out of the notification server.
1979 After running the method the server is expected
1980 to close the connection.
1983 if self
.pingCheckTask
:
1984 self
.pingCheckTask
.stop()
1985 self
.pingCheckTask
= None
1986 self
.sendLine("OUT")
1987 self
.transport
.loseConnection()
1989 class NotificationFactory(ClientFactory
):
1991 Factory for the NotificationClient protocol.
1992 This is basically responsible for keeping
1993 the state of the client and thus should be used
1994 in a 1:1 situation with clients.
1996 @ivar contacts: An MSNContactList instance reflecting
1997 the current contact list -- this is
1998 generally kept up to date by the default
2000 @ivar userHandle: The client's userHandle, this is expected
2001 to be set by the client and is used by the
2002 protocol (for logging in etc).
2003 @ivar screenName: The client's current screen-name -- this is
2004 generally kept up to date by the default
2006 @ivar password: The client's password -- this is (obviously)
2007 expected to be set by the client.
2008 @ivar passportServer: This must point to an msn passport server
2009 (the whole URL is required)
2010 @ivar status: The status of the client -- this is generally kept
2011 up to date by the default command handlers
2018 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
2020 protocol
= NotificationClient
2023 class SwitchboardClient(MSNEventBase
):
2025 This class provides support for clients connecting to a switchboard server.
2027 Switchboard servers are used for conversations with other people
2028 on the MSN network. This means that the number of conversations at
2029 any given time will be directly proportional to the number of
2030 connections to varioius switchboard servers.
2032 MSN makes no distinction between single and group conversations,
2033 so any number of users may be invited to join a specific conversation
2034 taking place on a switchboard server.
2036 @ivar key: authorization key, obtained when receiving
2037 invitation / requesting switchboard server.
2038 @ivar userHandle: your user handle (passport)
2039 @ivar sessionID: unique session ID, used if you are replying
2040 to a switchboard invitation
2041 @ivar reply: set this to 1 in connectionMade or before to signifiy
2042 that you are replying to a switchboard invitation.
2043 @ivar msnobj: the MSNObject for the user's avatar. So that the
2044 switchboard can distribute it to anyone who asks.
2056 MSNEventBase
.__init
__(self
)
2057 self
.pendingUsers
= {}
2058 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
2061 def connectionMade(self
):
2062 MSNEventBase
.connectionMade(self
)
2065 def connectionLost(self
, reason
):
2066 self
.cookies
['iCookies'] = {}
2067 MSNEventBase
.connectionLost(self
, reason
)
2069 def _sendInit(self
):
2071 send initial data based on whether we are replying to an invitation
2074 id = self
._nextTransactionID
()
2076 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
2078 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
2080 def _newInvitationCookie(self
):
2082 if self
._iCookie
> 1000: self
._iCookie
= 1
2083 return self
._iCookie
2085 def _checkTyping(self
, message
, cTypes
):
2086 """ helper method for checkMessage """
2087 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
2088 self
.gotContactTyping(message
)
2091 def _checkFileInvitation(self
, message
, info
):
2092 """ helper method for checkMessage """
2093 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
2095 cookie
= info
['Invitation-Cookie']
2096 filename
= info
['Application-File']
2097 filesize
= int(info
['Application-FileSize'])
2098 connectivity
= (info
.get('Connectivity', 'n').lower() == 'y')
2100 log
.msg('Received munged file transfer request ... ignoring.')
2102 raise NotImplementedError
2103 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
2106 def _handleP2PMessage(self
, message
):
2107 """ helper method for msnslp messages (file transfer & avatars) """
2108 if not message
.getHeader("P2P-Dest") == self
.userHandle
: return
2109 packet
= message
.message
2110 binaryFields
= BinaryFields(packet
=packet
)
2111 if binaryFields
[0] != 0:
2112 slpLink
= self
.slpLinks
.get(binaryFields
[0])
2114 # Link has been killed. Ignore
2116 if slpLink
.remoteUser
== message
.userHandle
:
2117 slpLink
.handlePacket(packet
)
2118 if binaryFields
[5] == BinaryFields
.ACK
or binaryFields
[5] == BinaryFields
.BYEGOT
:
2119 pass # Ignore the ACKs to SLP messages
2121 slpMessage
= MSNSLPMessage(packet
)
2123 # Always try and give a slpMessage to a slpLink first.
2124 # If none can be found, and it was INVITE, then create
2125 # one to handle the session.
2126 for slpLink
in self
.slpLinks
.values():
2127 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
2128 slpLink
.handleSLPMessage(slpMessage
)
2131 slpLink
= None # Was not handled
2133 if not slpLink
and slpMessage
.method
== "INVITE":
2134 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
2135 context
= FileContext(slpMessage
.context
)
2136 slpLink
= SLPLink_FileReceive(remoteUser
=slpMessage
.fro
, switchboard
=self
, filename
=context
.filename
, filesize
=context
.filesize
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
, branch
=slpMessage
.branch
)
2137 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2138 self
.gotFileReceive(slpLink
)
2139 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
2140 # Check that we have an avatar to send
2142 slpLink
= SLPLink_AvatarSend(remoteUser
=slpMessage
.fro
, switchboard
=self
, filesize
=self
.msnobj
.size
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2143 slpLink
.write(self
.msnobj
.imageData
)
2146 # They shouldn't have sent a request if we have
2147 # no avatar. So we'll just ignore them.
2148 # FIXME We should really send an error
2151 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2153 # Always need to ACK these packets if we can
2154 slpLink
.sendP2PACK(binaryFields
)
2157 def checkMessage(self
, message
):
2159 hook for detecting any notification type messages
2160 (e.g. file transfer)
2162 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2163 if self
._checkTyping
(message
, cTypes
): return 0
2164 if 'text/x-msmsgsinvite' in cTypes
:
2165 # header like info is sent as part of the message body.
2167 for line
in message
.message
.split('\r\n'):
2169 key
, val
= line
.split(':')
2170 info
[key
] = val
.lstrip()
2171 except ValueError: continue
2172 if self
._checkFileInvitation
(message
, info
): return 0
2173 elif 'application/x-msnmsgrp2p' in cTypes
:
2174 self
._handleP
2PMessage
(message
)
2179 def handle_USR(self
, params
):
2180 checkParamLen(len(params
), 4, 'USR')
2181 if params
[1] == "OK":
2185 def handle_CAL(self
, params
):
2186 checkParamLen(len(params
), 3, 'CAL')
2188 if params
[1].upper() == "RINGING":
2189 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2192 def handle_JOI(self
, params
):
2193 checkParamLen(len(params
), 2, 'JOI')
2194 self
.userJoined(params
[0], unquote(params
[1]))
2196 # users participating in the current chat
2197 def handle_IRO(self
, params
):
2198 checkParamLen(len(params
), 5, 'IRO')
2199 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2200 if params
[1] == params
[2]:
2201 self
.gotChattingUsers(self
.pendingUsers
)
2202 self
.pendingUsers
= {}
2204 # finished listing users
2205 def handle_ANS(self
, params
):
2206 checkParamLen(len(params
), 2, 'ANS')
2207 if params
[1] == "OK":
2210 def handle_ACK(self
, params
):
2211 checkParamLen(len(params
), 1, 'ACK')
2212 self
._fireCallback
(int(params
[0]), None)
2214 def handle_NAK(self
, params
):
2215 checkParamLen(len(params
), 1, 'NAK')
2216 self
._fireCallback
(int(params
[0]), None)
2218 def handle_BYE(self
, params
):
2219 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2220 self
.userLeft(params
[0])
2226 called when all login details have been negotiated.
2227 Messages can now be sent, or new users invited.
2231 def gotChattingUsers(self
, users
):
2233 called after connecting to an existing chat session.
2235 @param users: A dict mapping user handles to screen names
2236 (current users taking part in the conversation)
2240 def userJoined(self
, userHandle
, screenName
):
2242 called when a user has joined the conversation.
2244 @param userHandle: the user handle (passport) of the user
2245 @param screenName: the screen name of the user
2249 def userLeft(self
, userHandle
):
2251 called when a user has left the conversation.
2253 @param userHandle: the user handle (passport) of the user.
2257 def gotMessage(self
, message
):
2259 called when we receive a message.
2261 @param message: the associated MSNMessage object
2265 def gotFileReceive(self
, fileReceive
):
2267 called when we receive a file send request from a contact.
2268 Default action is to reject the file.
2270 @param fileReceive: msnft.MSNFTReceive_Base instance
2272 fileReceive
.reject()
2275 def gotSendRequest(self
, fileReceive
):
2277 called when we receive a file send request from a contact
2279 @param fileReceive: msnft.MSNFTReceive_Base instance
2283 def gotContactTyping(self
, message
):
2285 called when we receive the special type of message notifying
2286 us that a contact is typing a message.
2288 @param message: the associated MSNMessage object
2294 def inviteUser(self
, userHandle
):
2296 used to invite a user to the current switchboard server.
2298 @param userHandle: the user handle (passport) of the desired user.
2300 @return: A Deferred, the callback for which will be called
2301 when the server notifies us that the user has indeed
2302 been invited. The callback argument will be a tuple
2303 with 1 element, the sessionID given to the invited user.
2304 I'm not sure if this is useful or not.
2307 id, d
= self
._createIDMapping
()
2308 self
.sendLine("CAL %s %s" % (id, userHandle
))
2311 def sendMessage(self
, message
):
2313 used to send a message.
2315 @param message: the corresponding MSNMessage object.
2317 @return: Depending on the value of message.ack.
2318 If set to MSNMessage.MESSAGE_ACK or
2319 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2320 the callback for which will be fired when an ACK or
2321 NACK is received - the callback argument will be
2322 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2323 the return value is None.
2326 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2327 else: id, d
= self
._createIDMapping
()
2328 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2329 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2330 # apparently order matters with at least MIME-Version and Content-Type
2331 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
2332 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
2333 # send the rest of the headers
2334 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
2335 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2336 self
.transport
.write("\r\n")
2337 self
.transport
.write(message
.message
)
2338 if MESSAGEDEBUG
: log
.msg(message
.message
)
2341 def sendAvatarRequest(self
, msnContact
):
2343 used to request an avatar from a user in this switchboard
2346 @param msnContact: the msnContact object to request an avatar for
2348 @return: A Deferred, the callback for which will be called
2349 when the avatar transfer succeeds.
2350 The callback argument will be a tuple with one element,
2351 the PNG avatar data.
2353 if not msnContact
.msnobj
: return
2355 def bufferClosed(data
):
2357 buffer = StringBuffer(bufferClosed
)
2358 slpLink
= SLPLink_AvatarReceive(remoteUser
=msnContact
.userHandle
, switchboard
=self
, consumer
=buffer, context
=msnContact
.msnobj
.text
)
2359 self
.slpLinks
[slpLink
.sessionID
] = slpLink
2362 def sendFile(self
, msnContact
, filename
, filesize
):
2364 used to send a file to a contact.
2366 @param msnContact: the MSNContact object to send a file to.
2367 @param filename: the name of the file to send.
2368 @param filesize: the size of the file to send.
2370 @return: (fileSend, d) A FileSend object and a Deferred.
2371 The Deferred will pass one argument in a tuple,
2372 whether or not the transfer is accepted. If you
2373 receive a True, then you can call write() on the
2374 fileSend object to send your file. Call close()
2375 when the file is done.
2376 NOTE: You MUST write() exactly as much as you
2377 declare in filesize.
2379 if not msnContact
.userHandle
: return
2380 # FIXME, check msnContact.caps to see if we should use old-style
2381 fileSend
= SLPLink_FileSend(remoteUser
=msnContact
.userHandle
, switchboard
=self
, filename
=filename
, filesize
=filesize
)
2382 self
.slpLinks
[fileSend
.sessionID
] = fileSend
2383 return fileSend
, fileSend
.acceptDeferred
2385 def sendTypingNotification(self
):
2387 Used to send a typing notification. Upon receiving this
2388 message the official client will display a 'user is typing'
2389 message to all other users in the chat session for 10 seconds.
2390 You should send one of these every 5 seconds as long as the
2394 m
.ack
= m
.MESSAGE_ACK_NONE
2395 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2396 m
.setHeader('TypingUser', self
.userHandle
)
2400 def sendFileInvitation(self
, fileName
, fileSize
):
2402 send an notification that we want to send a file.
2404 @param fileName: the file name
2405 @param fileSize: the file size
2407 @return: A Deferred, the callback of which will be fired
2408 when the user responds to this invitation with an
2409 appropriate message. The callback argument will be
2410 a tuple with 3 elements, the first being 1 or 0
2411 depending on whether they accepted the transfer
2412 (1=yes, 0=no), the second being an invitation cookie
2413 to identify your follow-up responses and the third being
2414 the message 'info' which is a dict of information they
2415 sent in their reply (this doesn't really need to be used).
2416 If you wish to proceed with the transfer see the
2417 sendTransferInfo method.
2419 cookie
= self
._newInvitationCookie
()
2422 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2423 m
.message
+= 'Application-Name: File Transfer\r\n'
2424 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2425 m
.message
+= 'Invitation-Command: INVITE\r\n'
2426 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2427 m
.message
+= 'Application-File: %s\r\n' % fileName
2428 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2429 m
.ack
= m
.MESSAGE_ACK_NONE
2431 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2434 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2436 send information relating to a file transfer session.
2438 @param accept: whether or not to go ahead with the transfer
2440 @param iCookie: the invitation cookie of previous replies
2441 relating to this transfer
2442 @param authCookie: the authentication cookie obtained from
2443 an FileSend instance
2445 @param port: the port on which an FileSend protocol is listening.
2448 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2449 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2450 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2451 m
.message
+= 'IP-Address: %s\r\n' % ip
2452 m
.message
+= 'Port: %s\r\n' % port
2453 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2455 m
.ack
= m
.MESSAGE_NACK
2460 def __init__(self
, filename
, filesize
, userHandle
):
2461 self
.consumer
= None
2462 self
.finished
= False
2465 self
.filename
, self
.filesize
, self
.userHandle
= filename
, filesize
, userHandle
2468 raise NotImplementedError
2470 def accept(self
, consumer
):
2471 if self
.consumer
: raise "AlreadyAccepted"
2472 self
.consumer
= consumer
2473 for data
in self
.buffer:
2474 self
.consumer
.write(data
)
2477 self
.consumer
.close()
2479 self
.consumer
.error()
2481 def write(self
, data
):
2482 if self
.error
or self
.finished
:
2483 raise IOError, "Attempt to write in an invalid state"
2485 self
.consumer
.write(data
)
2487 self
.buffer.append(data
)
2490 self
.finished
= True
2492 self
.consumer
.close()
2495 """ Represents the Context field for P2P file transfers """
2496 def __init__(self
, data
=""):
2504 if MSNP2PDEBUG
: log
.msg("FileContext packing:", self
.filename
, self
.filesize
)
2505 data
= struct
.pack("<LLQL", 638, 0x03, self
.filesize
, 0x01)
2506 data
= data
[:-1] # Uck, weird, but it works
2507 data
+= utf16net(self
.filename
)
2508 data
= ljust(data
, 570, '\0')
2509 data
+= struct
.pack("<L", 0xFFFFFFFFL
)
2510 data
= ljust(data
, 638, '\0')
2513 def parse(self
, packet
):
2514 self
.filesize
= struct
.unpack("<Q", packet
[8:16])[0]
2515 chunk
= packet
[19:540]
2516 chunk
= chunk
[:chunk
.find('\x00\x00')]
2517 self
.filename
= unicode((codecs
.BOM_UTF16_BE
+ chunk
).decode("utf-16"))
2518 if MSNP2PDEBUG
: log
.msg("FileContext parsed:", self
.filesize
, self
.filename
)
2522 """ Utility class for the binary header & footer in p2p messages """
2531 def __init__(self
, fields
=None, packet
=None):
2533 self
.fields
= fields
2535 self
.fields
= [0] * 10
2537 self
.unpackFields(packet
)
2539 def __getitem__(self
, key
):
2540 return self
.fields
[key
]
2542 def __setitem__(self
, key
, value
):
2543 self
.fields
[key
] = value
2545 def unpackFields(self
, packet
):
2546 self
.fields
= struct
.unpack("<LLQQLLLLQ", packet
[0:48])
2547 self
.fields
+= struct
.unpack(">L", packet
[len(packet
)-4:])
2549 out
= "Unpacked fields: "
2550 for i
in self
.fields
:
2554 def packHeaders(self
):
2555 f
= tuple(self
.fields
)
2557 out
= "Packed fields: "
2558 for i
in self
.fields
:
2561 return struct
.pack("<LLQQLLLLQ", f
[0], f
[1], f
[2], f
[3], f
[4], f
[5], f
[6], f
[7], f
[8])
2563 def packFooter(self
):
2564 return struct
.pack(">L", self
.fields
[9])
2567 class MSNSLPMessage
:
2568 """ Representation of a single MSNSLP message """
2569 def __init__(self
, packet
=None):
2576 self
.sessionGuid
= ""
2577 self
.sessionID
= None
2579 self
.data
= "\r\n" + chr(0)
2583 def create(self
, method
=None, status
=None, to
=None, fro
=None, branch
=None, cseq
=0, sessionGuid
=None, data
=None):
2584 self
.method
= method
2585 self
.status
= status
2588 self
.branch
= branch
2590 self
.sessionGuid
= sessionGuid
2591 if data
: self
.data
= data
2593 def setData(self
, ctype
, data
):
2596 order
= ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
2598 if key
== "Context" and data
.has_key(key
):
2599 s
.append("Context: %s\r\n" % b64enc(data
[key
]))
2600 elif data
.has_key(key
):
2601 s
.append("%s: %s\r\n" % (key
, str(data
[key
])))
2602 s
.append("\r\n"+chr(0))
2604 self
.data
= "".join(s
)
2608 if s
.find("MSNSLP/1.0") < 0: return
2610 lines
= s
.split("\r\n")
2612 # Get the MSNSLP method or status
2613 msnslp
= lines
[0].split(" ")
2614 if MSNP2PDEBUG
: log
.msg("Parsing MSNSLPMessage %s %s" % (len(s
), s
))
2615 if msnslp
[0] in ("INVITE", "BYE"):
2616 self
.method
= msnslp
[0].strip()
2618 self
.status
= msnslp
[1].strip()
2620 lines
.remove(lines
[0])
2623 line
= line
.split(":")
2624 if len(line
) < 1: continue
2626 if len(line
) > 2 and line
[0] == "To":
2627 self
.to
= line
[2][:line
[2].find('>')]
2628 elif len(line
) > 2 and line
[0] == "From":
2629 self
.fro
= line
[2][:line
[2].find('>')]
2630 elif line
[0] == "Call-ID":
2631 self
.sessionGuid
= line
[1].strip()
2632 elif line
[0] == "CSeq":
2633 self
.cseq
= int(line
[1].strip())
2634 elif line
[0] == "SessionID":
2635 self
.sessionID
= int(line
[1].strip())
2636 elif line
[0] == "EUF-GUID":
2637 self
.euf_guid
= line
[1].strip()
2638 elif line
[0] == "Content-Type":
2639 self
.ctype
= line
[1].strip()
2640 elif line
[0] == "Context":
2641 self
.context
= b64dec(line
[1])
2642 elif line
[0] == "Via":
2643 self
.branch
= line
[1].split(";")[1].split("=")[1].strip()
2646 log
.msg("Error parsing MSNSLP message.")
2652 s
.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self
.method
, self
.to
))
2654 if self
.status
== "200": status
= "200 OK"
2655 elif self
.status
== "603": status
= "603 Decline"
2656 s
.append("MSNSLP/1.0 %s\r\n" % status
)
2657 s
.append("To: <msnmsgr:%s>\r\n" % self
.to
)
2658 s
.append("From: <msnmsgr:%s>\r\n" % self
.fro
)
2659 s
.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % self
.branch
)
2660 s
.append("CSeq: %s \r\n" % str(self
.cseq
))
2661 s
.append("Call-ID: %s\r\n" % self
.sessionGuid
)
2662 s
.append("Max-Forwards: 0\r\n")
2663 s
.append("Content-Type: %s\r\n" % self
.ctype
)
2664 s
.append("Content-Length: %s\r\n\r\n" % len(self
.data
))
2669 """ Utility for handling the weird sequence IDs in p2p messages """
2670 def __init__(self
, baseID
=None):
2672 self
.baseID
= baseID
2674 self
.baseID
= random
.randint(1000, sys
.maxint
)
2678 return p2pseq(self
.pos
) + self
.baseID
2685 class StringBuffer(StringIO
.StringIO
):
2686 def __init__(self
, notifyFunc
=None):
2687 self
.notifyFunc
= notifyFunc
2688 StringIO
.StringIO
.__init
__(self
)
2692 self
.notifyFunc(self
.getvalue())
2693 self
.notifyFunc
= None
2694 StringIO
.StringIO
.close(self
)
2698 def __init__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
):
2701 sessionID
= random
.randint(1000, sys
.maxint
)
2703 sessionGuid
= random_guid()
2704 self
.remoteUser
= remoteUser
2705 self
.switchboard
= switchboard
2706 self
.sessionID
= sessionID
2707 self
.sessionGuid
= sessionGuid
2708 self
.seqID
= SeqID()
2712 if not self
.switchboard
: return
2713 del self
.switchboard
.slpLinks
[self
.sessionID
]
2714 self
.switchboard
= None
2715 # This is so that handleP2PMessage can still use the SLPLink
2716 # one last time, for ACKing BYEs and 601s.
2717 reactor
.callLater(0, kill
)
2719 def warn(self
, text
):
2720 log
.msg("Warning in transfer: %s %s" % (self
, text
))
2722 def sendP2PACK(self
, ackHeaders
):
2723 binaryFields
= BinaryFields()
2724 binaryFields
[0] = ackHeaders
[0]
2725 binaryFields
[1] = self
.seqID
.next()
2726 binaryFields
[3] = ackHeaders
[3]
2727 binaryFields
[5] = BinaryFields
.ACK
2728 binaryFields
[6] = ackHeaders
[1]
2729 binaryFields
[7] = ackHeaders
[6]
2730 binaryFields
[8] = ackHeaders
[3]
2731 self
.sendP2PMessage(binaryFields
, "")
2733 def sendSLPMessage(self
, cmd
, ctype
, data
, branch
=None):
2734 msg
= MSNSLPMessage()
2736 msg
.create(status
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=branch
, cseq
=1, sessionGuid
=self
.sessionGuid
)
2738 msg
.create(method
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=random_guid(), cseq
=0, sessionGuid
=self
.sessionGuid
)
2739 msg
.setData(ctype
, data
)
2741 binaryFields
= BinaryFields()
2742 binaryFields
[1] = self
.seqID
.next()
2743 binaryFields
[3] = len(msgStr
)
2744 binaryFields
[4] = binaryFields
[3]
2745 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2746 self
.sendP2PMessage(binaryFields
, msgStr
)
2748 def sendP2PMessage(self
, binaryFields
, msgStr
):
2749 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2751 message
= MSNMessage(message
=packet
)
2752 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2753 message
.setHeader("P2P-Dest", self
.remoteUser
)
2754 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2755 self
.switchboard
.sendMessage(message
)
2757 def handleSLPMessage(self
, slpMessage
):
2758 raise NotImplementedError
2764 class SLPLink_Send(SLPLink
):
2765 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2766 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2767 self
.handlePacket
= None
2769 self
.filesize
= filesize
2772 def send_dataprep(self
):
2773 if MSNP2PDEBUG
: log
.msg("send_dataprep")
2774 binaryFields
= BinaryFields()
2775 binaryFields
[0] = self
.sessionID
2776 binaryFields
[1] = self
.seqID
.next()
2779 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2781 self
.sendP2PMessage(binaryFields
, chr(0) * 4)
2783 def write(self
, data
):
2784 if MSNP2PDEBUG
: log
.msg("write")
2788 if i
+ 1202 < length
:
2789 self
._writeChunk
(data
[i
:i
+1202])
2792 self
.data
+= data
[i
:]
2793 if len(self
.data
) >= 1202:
2799 def _writeChunk(self
, chunk
):
2800 log
.msg("writing chunk")
2801 binaryFields
= BinaryFields()
2802 binaryFields
[0] = self
.sessionID
2803 if self
.offset
== 0:
2804 binaryFields
[1] = self
.seqID
.next()
2806 binaryFields
[1] = self
.seqID
.get()
2807 binaryFields
[2] = self
.offset
2808 binaryFields
[3] = self
.filesize
2809 binaryFields
[4] = len(chunk
)
2810 binaryFields
[5] = self
.dataFlag
2811 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2813 self
.offset
+= len(chunk
)
2814 self
.sendP2PMessage(binaryFields
, chunk
)
2818 self
._writeChunk
(self
.data
)
2823 # FIXME, should send 601 or something
2825 class SLPLink_FileSend(SLPLink_Send
):
2826 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
):
2827 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
)
2828 self
.dataFlag
= BinaryFields
.DATAFT
2829 # Send invite & wait for 200OK before sending dataprep
2830 context
= FileContext()
2831 context
.filename
= filename
2832 context
.filesize
= filesize
2833 data
= {"EUF-GUID" : MSN_MSNFTP_GUID
,\
2834 "SessionID": self
.sessionID
,\
2836 "Context" : context
.pack() }
2837 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2838 self
.acceptDeferred
= Deferred()
2840 def handleSLPMessage(self
, slpMessage
):
2841 if slpMessage
.status
== "200":
2842 if slpMessage
.ctype
== "application/x-msnmsgr-sessionreqbody":
2843 data
= {"Bridges" : "TRUDPv1 TCPv1",\
2845 "Conn-Type" : "Firewall",\
2846 "UPnPNat" : "false",\
2848 #"Hashed-Nonce": random_guid()}
2849 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data
)
2850 elif slpMessage
.ctype
== "application/x-msnmsgr-transrespbody":
2851 self
.acceptDeferred
.callback((True,))
2852 self
.handlePacket
= self
.wait_data_ack
2854 if slpMessage
.status
== "603":
2855 self
.acceptDeferred
.callback((False,))
2856 # SLPLink is over due to decline, error or BYE
2859 def wait_data_ack(self
, packet
):
2860 if MSNP2PDEBUG
: log
.msg("wait_data_ack")
2861 binaryFields
= BinaryFields()
2862 binaryFields
.unpackFields(packet
)
2864 if binaryFields
[5] != BinaryFields
.ACK
:
2865 self
.warn("field5," + str(binaryFields
[5]))
2868 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2869 self
.handlePacket
= None
2872 self
.handlePacket
= self
.wait_data_ack
2873 SLPLink_Send
.close(self
)
2876 class SLPLink_AvatarSend(SLPLink_Send
):
2877 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2878 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2879 self
.dataFlag
= BinaryFields
.DATA
2880 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2881 self
.send_dataprep()
2882 self
.handlePacket
= lambda packet
: None
2884 def handleSLPMessage(self
, slpMessage
):
2885 self
.killLink() # BYE or error
2888 SLPLink_Send
.close(self
)
2889 # Keep the link open to wait for a BYE
2891 class SLPLink_Receive(SLPLink
):
2892 def __init__(self
, remoteUser
, switchboard
, consumer
, context
=None, sessionID
=None, sessionGuid
=None):
2893 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2894 self
.handlePacket
= None
2895 self
.consumer
= consumer
2898 def wait_dataprep(self
, packet
):
2899 if MSNP2PDEBUG
: log
.msg("wait_dataprep")
2900 binaryFields
= BinaryFields()
2901 binaryFields
.unpackFields(packet
)
2903 if binaryFields
[3] != 4:
2904 self
.warn("field3," + str(binaryFields
[3]))
2906 if binaryFields
[4] != 4:
2907 self
.warn("field4," + str(binaryFields
[4]))
2909 # Just ignore the footer
2910 #if binaryFields[9] != 1:
2911 # self.warn("field9," + str(binaryFields[9]))
2914 self
.sendP2PACK(binaryFields
)
2915 self
.handlePacket
= self
.wait_data
2917 def wait_data(self
, packet
):
2918 if MSNP2PDEBUG
: log
.msg("wait_data")
2919 binaryFields
= BinaryFields()
2920 binaryFields
.unpackFields(packet
)
2922 if binaryFields
[5] != self
.dataFlag
:
2923 self
.warn("field5," + str(binaryFields
[5]))
2925 # Just ignore the footer
2926 #if binaryFields[9] != 1:
2927 # self.warn("field9," + str(binaryFields[9]))
2929 offset
= binaryFields
[2]
2930 total
= binaryFields
[3]
2931 length
= binaryFields
[4]
2933 data
= packet
[48:-4]
2934 if offset
!= self
.pos
:
2935 self
.warn("Received packet out of order")
2936 self
.consumer
.error()
2938 if len(data
) != length
:
2939 self
.warn("Received bad length of slp")
2940 self
.consumer
.error()
2945 self
.consumer
.write(str(data
))
2947 if self
.pos
== total
:
2948 self
.sendP2PACK(binaryFields
)
2949 self
.consumer
.close()
2950 self
.handlePacket
= None
2953 def doFinished(self
):
2954 raise NotImplementedError
2957 class SLPLink_FileReceive(SLPLink_Receive
, FileReceive
):
2958 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
, sessionID
, sessionGuid
, branch
):
2959 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=self
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2960 self
.dataFlag
= BinaryFields
.DATAFT
2961 self
.initialBranch
= branch
2962 FileReceive
.__init
__(self
, filename
, filesize
, remoteUser
)
2965 # Send a 603 decline
2966 self
.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2969 def accept(self
, consumer
):
2970 FileReceive
.accept(self
, consumer
)
2971 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2973 def handleSLPMessage(self
, slpMessage
):
2974 if slpMessage
.method
== "INVITE": # The second invite
2975 data
= {"Bridge" : "TCPv1",\
2976 "Listening" : "false",\
2977 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
2978 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
2979 self
.handlePacket
= self
.wait_data
2981 self
.killLink() # It's either a BYE or an error
2982 # FIXME, do some error handling if it was an error
2984 def doFinished(self
):
2985 #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2987 # Wait for BYE? #FIXME
2990 class SLPLink_AvatarReceive(SLPLink_Receive
):
2991 def __init__(self
, remoteUser
, switchboard
, consumer
, context
):
2992 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=consumer
, context
=context
)
2993 self
.dataFlag
= BinaryFields
.DATA
2994 data
= {"EUF-GUID" : MSN_AVATAR_GUID
,\
2995 "SessionID": self
.sessionID
,\
2997 "Context" : context
}
2998 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
3000 def handleSLPMessage(self
, slpMessage
):
3001 if slpMessage
.status
== "200":
3002 self
.handlePacket
= self
.wait_dataprep
3004 # SLPLink is over due to error or BYE
3007 def doFinished(self
):
3008 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
3010 # mapping of error codes to error messages
3013 200 : "Syntax error",
3014 201 : "Invalid parameter",
3015 205 : "Invalid user",
3016 206 : "Domain name missing",
3017 207 : "Already logged in",
3018 208 : "Invalid username",
3019 209 : "Invalid screen name",
3020 210 : "User list full",
3021 215 : "User already there",
3022 216 : "User already on list",
3023 217 : "User not online",
3024 218 : "Already in mode",
3025 219 : "User is in the opposite list",
3026 223 : "Too many groups",
3027 224 : "Invalid group",
3028 225 : "User not in group",
3029 229 : "Group name too long",
3030 230 : "Cannot remove group 0",
3031 231 : "Invalid group",
3032 280 : "Switchboard failed",
3033 281 : "Transfer to switchboard failed",
3035 300 : "Required field missing",
3036 301 : "Too many FND responses",
3037 302 : "Not logged in",
3039 402 : "Error accessing contact list",
3040 403 : "Error accessing contact list",
3042 500 : "Internal server error",
3043 501 : "Database server error",
3044 502 : "Command disabled",
3045 510 : "File operation failed",
3046 520 : "Memory allocation failed",
3047 540 : "Wrong CHL value sent to server",
3049 600 : "Server is busy",
3050 601 : "Server is unavaliable",
3051 602 : "Peer nameserver is down",
3052 603 : "Database connection failed",
3053 604 : "Server is going down",
3054 605 : "Server unavailable",
3056 707 : "Could not create connection",
3057 710 : "Invalid CVR parameters",
3058 711 : "Write is blocking",
3059 712 : "Session is overloaded",
3060 713 : "Too many active users",
3061 714 : "Too many sessions",
3062 715 : "Not expected",
3063 717 : "Bad friend file",
3064 731 : "Not expected",
3066 800 : "Requests too rapid",
3068 910 : "Server too busy",
3069 911 : "Authentication failed",
3070 912 : "Server too busy",
3071 913 : "Not allowed when offline",
3072 914 : "Server too busy",
3073 915 : "Server too busy",
3074 916 : "Server too busy",
3075 917 : "Server too busy",
3076 918 : "Server too busy",
3077 919 : "Server too busy",
3078 920 : "Not accepting new users",
3079 921 : "Server too busy",
3080 922 : "Server too busy",
3081 923 : "No parent consent",
3082 924 : "Passport account not yet verified"
3086 # mapping of status codes to readable status format
3089 STATUS_ONLINE
: "Online",
3090 STATUS_OFFLINE
: "Offline",
3091 STATUS_HIDDEN
: "Appear Offline",
3092 STATUS_IDLE
: "Idle",
3093 STATUS_AWAY
: "Away",
3094 STATUS_BUSY
: "Busy",
3095 STATUS_BRB
: "Be Right Back",
3096 STATUS_PHONE
: "On the Phone",
3097 STATUS_LUNCH
: "Out to Lunch"
3101 # mapping of list ids to list codes
3104 FORWARD_LIST
: 'fl',
3107 REVERSE_LIST
: 'rl',
3112 # mapping of list codes to list ids
3114 for id,code
in listIDToCode
.items():
3115 listCodeToID
[code
] = id