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
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}"
121 P2PSEQ
= [-3, -2, 0, -1, 1, 2, 3, 4, 5, 6, 7, 8]
143 STATUS_ONLINE
= 'NLN'
144 STATUS_OFFLINE
= 'FLN'
145 STATUS_HIDDEN
= 'HDN'
166 return inp
.split('=')[1]
178 userHandle
= getVal(p
)
180 screenName
= unquote(getVal(p
))
185 else: # Must be the groups
187 groups
= p
.split(',')
189 raise MSNProtocolError
, "Unknown LST/ADC response" + str(params
) # debug
191 return userHandle
, screenName
, userGuid
, lists
, groups
194 """ Needed for Python 2.3 compatibility """
195 return s
+ (n
-len(s
))*c
197 if sys
.byteorder
== "little":
199 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
200 a
= array
.array("h", s
.encode("utf-16")[2:])
205 """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
206 return s
.encode("utf-16")[2:]
209 return base64
.encodestring(s
).replace("\n", "")
212 for pad
in ["", "=", "==", "A", "A=", "A=="]: # Stupid MSN client!
214 return base64
.decodestring(s
+ pad
)
217 raise ValueError("Got some very bad base64!")
220 format
= "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
223 data
.append(random
.random() * 0xAAFF + 0x1111)
228 def checkParamLen(num
, expected
, cmd
, error
=None):
229 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
230 if num
!= expected
: raise MSNProtocolError
, error
232 def _parseHeader(h
, v
):
234 Split a certin number of known
235 header values with the format:
236 field1=val,field2=val,field3=val into
237 a dict mapping fields to values.
238 @param h: the header's key
239 @param v: the header's value as a string
242 if h
in ('passporturls','authentication-info','www-authenticate'):
243 v
= v
.replace('Passport1.4','').lstrip()
245 for fieldPair
in v
.split(','):
247 field
,value
= fieldPair
.split('=',1)
248 fields
[field
.lower()] = value
250 fields
[field
.lower()] = ''
254 def _parsePrimitiveHost(host
):
256 h
,p
= host
.replace('https://','').split('/',1)
260 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
=''):
262 This function is used internally and should not ever be called
266 def _cb(server
, auth
):
267 loginFac
= ClientFactory()
268 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
269 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
272 _cb(nexusServer
, authData
)
274 fac
= ClientFactory()
276 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
277 d
.addErrback(lambda f
: cb
.errback(f
))
278 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
279 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
283 class PassportNexus(HTTPClient
):
286 Used to obtain the URL of a valid passport
289 This class is used internally and should
290 not be instantiated directly -- that is,
291 The passport logging in process is handled
292 transparantly by NotificationClient.
295 def __init__(self
, deferred
, host
):
296 self
.deferred
= deferred
297 self
.host
, self
.path
= _parsePrimitiveHost(host
)
299 def connectionMade(self
):
300 HTTPClient
.connectionMade(self
)
301 self
.sendCommand('GET', self
.path
)
302 self
.sendHeader('Host', self
.host
)
306 def handleHeader(self
, header
, value
):
308 self
.headers
[h
] = _parseHeader(h
, value
)
310 def handleEndHeaders(self
):
311 if self
.connected
: self
.transport
.loseConnection()
312 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
313 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
315 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
317 def handleResponse(self
, r
): pass
319 class PassportLogin(HTTPClient
):
321 This class is used internally to obtain
322 a login ticket from a passport HTTPS
323 server -- it should not be used directly.
328 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
329 self
.deferred
= deferred
330 self
.userHandle
= userHandle
332 self
.authData
= authData
333 self
.host
, self
.path
= _parsePrimitiveHost(host
)
335 def connectionMade(self
):
336 self
.sendCommand('GET', self
.path
)
337 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
338 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), self
.passwd
,self
.authData
))
339 self
.sendHeader('Host', self
.host
)
343 def handleHeader(self
, header
, value
):
345 self
.headers
[h
] = _parseHeader(h
, value
)
347 def handleEndHeaders(self
):
348 if self
._finished
: return
349 self
._finished
= 1 # I think we need this because of HTTPClient
350 if self
.connected
: self
.transport
.loseConnection()
351 authHeader
= 'authentication-info'
352 _interHeader
= 'www-authenticate'
353 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
355 info
= self
.headers
[authHeader
]
356 status
= info
['da-status']
357 handler
= getattr(self
, 'login_%s' % (status
,), None)
360 else: raise Exception()
362 self
.deferred
.errback(failure
.Failure(e
))
364 def handleResponse(self
, r
): pass
366 def login_success(self
, info
):
367 ticket
= info
['from-pp']
368 ticket
= ticket
[1:len(ticket
)-1]
369 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
371 def login_failed(self
, info
):
372 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
374 def login_redir(self
, info
):
375 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
377 class MSNProtocolError(Exception):
379 This Exception is basically used for debugging
380 purposes, as the official MSN server should never
381 send anything _wrong_ and nobody in their right
382 mind would run their B{own} MSN server.
383 If it is raised by default command handlers
384 (handle_BLAH) the error will be logged.
391 I am the class used to represent an 'instant' message.
393 @ivar userHandle: The user handle (passport) of the sender
394 (this is only used when receiving a message)
395 @ivar screenName: The screen name of the sender (this is only used
396 when receiving a message)
397 @ivar message: The message
398 @ivar headers: The message headers
400 @ivar length: The message length (including headers and line endings)
401 @ivar ack: This variable is used to tell the server how to respond
402 once the message has been sent. If set to MESSAGE_ACK
403 (default) the server will respond with an ACK upon receiving
404 the message, if set to MESSAGE_NACK the server will respond
405 with a NACK upon failure to receive the message.
406 If set to MESSAGE_ACK_NONE the server will do nothing.
407 This is relevant for the return value of
408 SwitchboardClient.sendMessage (which will return
409 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
410 and will fire when the respective ACK or NACK is received).
411 If set to MESSAGE_ACK_NONE sendMessage will return None.
414 MESSAGE_ACK_FAT
= 'D'
416 MESSAGE_ACK_NONE
= 'U'
420 def __init__(self
, length
=0, userHandle
="", screenName
="", message
="", specialMessage
=False):
421 self
.userHandle
= userHandle
422 self
.screenName
= screenName
423 self
.specialMessage
= specialMessage
424 self
.message
= message
425 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
429 def _calcMessageLen(self
):
431 used to calculte the number to send
432 as the message length when sending a message.
434 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
436 def setHeader(self
, header
, value
):
437 """ set the desired header """
438 self
.headers
[header
] = value
440 def getHeader(self
, header
):
442 get the desired header value
443 @raise KeyError: if no such header exists.
445 return self
.headers
[header
]
447 def hasHeader(self
, header
):
448 """ check to see if the desired header exists """
449 return self
.headers
.has_key(header
)
451 def getMessage(self
):
452 """ return the message - not including headers """
455 def setMessage(self
, message
):
456 """ set the message text """
457 self
.message
= message
462 Used to represent a MSNObject. This can be currently only be an avatar.
464 @ivar creator: The userHandle of the creator of this picture.
465 @ivar imageData: The PNG image data (only for our own avatar)
466 @ivar type: Always set to 3, for avatar.
467 @ivar size: The size of the image.
468 @ivar location: The filename of the image.
469 @ivar friendly: Unknown.
470 @ivar text: The textual representation of this MSNObject.
472 def __init__(self
, s
=""):
473 """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
479 def setData(self
, creator
, imageData
):
480 """ Set the creator and imageData for this object """
481 self
.creator
= creator
482 self
.imageData
= imageData
483 self
.size
= len(imageData
)
485 self
.location
= "TMP" + str(random
.randint(1000,9999))
486 self
.friendly
= "AAA="
487 self
.sha1d
= b64enc(sha
.sha(imageData
).digest())
501 """ Makes a textual representation of this MSNObject. Stores it in self.text """
504 h
.append(self
.creator
)
506 h
.append(str(self
.size
))
508 h
.append(str(self
.type))
510 h
.append(self
.location
)
512 h
.append(self
.friendly
)
515 sha1c
= b64enc(sha
.sha("".join(h
)).digest())
516 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
)
519 e
= xmlw
.parseText(s
, True)
520 self
.creator
= e
.getAttribute("Creator")
521 self
.size
= int(e
.getAttribute("Size"))
522 self
.type = int(e
.getAttribute("Type"))
523 self
.location
= e
.getAttribute("Location")
524 self
.friendly
= e
.getAttribute("Friendly")
525 self
.sha1d
= e
.getAttribute("SHA1D")
532 This class represents a contact (user).
534 @ivar userGuid: The contact's user guid (unique string)
535 @ivar userHandle: The contact's user handle (passport).
536 @ivar screenName: The contact's screen name.
537 @ivar groups: A list of all the group IDs which this
539 @ivar lists: An integer representing the sum of all lists
540 that this contact belongs to.
541 @ivar caps: int, The capabilities of this client
542 @ivar msnobj: The MSNObject representing the contact's avatar
543 @ivar status: The contact's status code.
544 @type status: str if contact's status is known, None otherwise.
545 @ivar personal: The contact's personal message .
546 @type personal: str if contact's personal message is known, None otherwise.
548 @ivar homePhone: The contact's home phone number.
549 @type homePhone: str if known, otherwise None.
550 @ivar workPhone: The contact's work phone number.
551 @type workPhone: str if known, otherwise None.
552 @ivar mobilePhone: The contact's mobile phone number.
553 @type mobilePhone: str if known, otherwise None.
554 @ivar hasPager: Whether or not this user has a mobile pager
555 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
563 def __init__(self
, userGuid
="", userHandle
="", screenName
="", lists
=0, caps
=0, msnobj
=None, groups
={}, status
=None, personal
=""):
564 self
.userGuid
= userGuid
565 self
.userHandle
= userHandle
566 self
.screenName
= screenName
570 self
.msnobjGot
= True
571 self
.groups
= [] # if applicable
572 self
.status
= status
# current status
573 self
.personal
= personal
576 self
.homePhone
= None
577 self
.workPhone
= None
578 self
.mobilePhone
= None
582 def setPhone(self
, phoneType
, value
):
584 set phone numbers/values for this specific user.
585 for phoneType check the *_PHONE constants and HAS_PAGER
588 t
= phoneType
.upper()
589 if t
== HOME_PHONE
: self
.homePhone
= value
590 elif t
== WORK_PHONE
: self
.workPhone
= value
591 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
592 elif t
== HAS_PAGER
: self
.hasPager
= value
593 elif t
== HAS_BLOG
: self
.hasBlog
= value
594 #else: raise ValueError, "Invalid Phone Type: " + t
596 def addToList(self
, listType
):
598 Update the lists attribute to
599 reflect being part of the
602 self
.lists |
= listType
604 def removeFromList(self
, listType
):
606 Update the lists attribute to
607 reflect being removed from the
610 self
.lists ^
= listType
612 class MSNContactList
:
614 This class represents a basic MSN contact list.
616 @ivar contacts: All contacts on my various lists
617 @type contacts: dict (mapping user handles to MSNContact objects)
618 @ivar groups: a mapping of group ids to group names
619 (groups can only exist on the forward list)
623 This is used only for storage and doesn't effect the
624 server's contact list.
634 def _getContactsFromList(self
, listType
):
636 Obtain all contacts which belong
637 to the given list type.
639 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
641 def addContact(self
, contact
):
645 self
.contacts
[contact
.userHandle
] = contact
647 def remContact(self
, userHandle
):
652 del self
.contacts
[userHandle
]
653 except KeyError: pass
655 def getContact(self
, userHandle
):
657 Obtain the MSNContact object
658 associated with the given
660 @return: the MSNContact object if
661 the user exists, or None.
664 return self
.contacts
[userHandle
]
668 def getBlockedContacts(self
):
670 Obtain all the contacts on my block list
672 return self
._getContactsFromList
(BLOCK_LIST
)
674 def getAuthorizedContacts(self
):
676 Obtain all the contacts on my auth list.
677 (These are contacts which I have verified
678 can view my state changes).
680 return self
._getContactsFromList
(ALLOW_LIST
)
682 def getReverseContacts(self
):
684 Get all contacts on my reverse list.
685 (These are contacts which have added me
686 to their forward list).
688 return self
._getContactsFromList
(REVERSE_LIST
)
690 def getContacts(self
):
692 Get all contacts on my forward list.
693 (These are the contacts which I have added
696 return self
._getContactsFromList
(FORWARD_LIST
)
698 def setGroup(self
, id, name
):
700 Keep a mapping from the given id
703 self
.groups
[id] = name
705 def remGroup(self
, id):
707 Removed the stored group
708 mapping for the given id.
712 except KeyError: pass
713 for c
in self
.contacts
:
714 if id in c
.groups
: c
.groups
.remove(id)
717 class MSNEventBase(LineReceiver
):
719 This class provides support for handling / dispatching events and is the
720 base class of the three main client protocols (DispatchClient,
721 NotificationClient, SwitchboardClient)
725 self
.ids
= {} # mapping of ids to Deferreds
729 self
.currentMessage
= None
731 def connectionLost(self
, reason
):
735 def connectionMade(self
):
738 def _fireCallback(self
, id, *args
):
740 Fire the callback for the given id
741 if one exists and return 1, else return false
743 if self
.ids
.has_key(id):
744 self
.ids
[id][0].callback(args
)
749 def _nextTransactionID(self
):
750 """ return a usable transaction ID """
752 if self
.currentID
> 1000: self
.currentID
= 1
753 return self
.currentID
755 def _createIDMapping(self
, data
=None):
757 return a unique transaction ID that is mapped internally to a
758 deferred .. also store arbitrary data if it is needed
760 id = self
._nextTransactionID
()
762 self
.ids
[id] = (d
, data
)
765 def checkMessage(self
, message
):
767 process received messages to check for file invitations and
768 typing notifications and other control type messages
770 raise NotImplementedError
772 def sendLine(self
, line
):
773 if LINEDEBUG
: log
.msg(">> " + line
)
774 LineReceiver
.sendLine(self
, line
)
776 def lineReceived(self
, line
):
777 if LINEDEBUG
: log
.msg("<< " + line
)
778 if self
.currentMessage
:
779 self
.currentMessage
.readPos
+= len(line
+"\r\n")
781 header
, value
= line
.split(':')
782 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
785 #raise MSNProtocolError, "Invalid Message Header"
787 if line
== "" or self
.currentMessage
.specialMessage
:
789 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
792 cmd
, params
= line
.split(' ', 1)
794 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
796 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
798 if self
.ids
.has_key(params
.split(' ')[0]):
799 self
.ids
[id].errback(int(cmd
))
802 else: # we received an error which doesn't map to a sent command
803 self
.gotError(int(cmd
))
806 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
808 try: handler(params
.split(' '))
809 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
811 self
.handle_UNKNOWN(cmd
, params
.split(' '))
813 def rawDataReceived(self
, data
):
815 self
.currentMessage
.readPos
+= len(data
)
816 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
818 self
.currentMessage
.message
+= data
[:-diff
]
821 self
.currentMessage
.message
+= data
823 self
.currentMessage
.message
+= data
825 del self
.currentMessage
.readPos
826 m
= self
.currentMessage
827 self
.currentMessage
= None
828 if MESSAGEDEBUG
: log
.msg(m
.message
)
829 if not self
.checkMessage(m
):
830 self
.setLineMode(extra
)
833 self
.setLineMode(extra
)
835 ### protocol command handlers - no need to override these.
837 def handle_MSG(self
, params
):
838 checkParamLen(len(params
), 3, 'MSG')
840 messageLen
= int(params
[2])
841 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
842 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
844 def handle_UNKNOWN(self
, cmd
, params
):
845 """ implement me in subclasses if you want to handle unknown events """
846 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
850 def gotBadLine(self
, line
, why
):
851 """ called when a handler notifies me that this line is broken """
852 log
.msg('Error in line: %s (%s)' % (line
, why
))
854 def gotError(self
, errorCode
):
856 called when the server sends an error which is not in
857 response to a sent command (ie. it has no matching transaction ID)
859 log
.msg('Error %s' % (errorCodes
[errorCode
]))
862 class DispatchClient(MSNEventBase
):
864 This class provides support for clients connecting to the dispatch server
865 @ivar userHandle: your user handle (passport) needed before connecting.
868 def connectionMade(self
):
869 MSNEventBase
.connectionMade(self
)
870 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
872 ### protocol command handlers ( there is no need to override these )
874 def handle_VER(self
, params
):
875 versions
= params
[1:]
876 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
877 self
.transport
.loseConnection()
878 raise MSNProtocolError
, "Invalid version response"
879 id = self
._nextTransactionID
()
880 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.factory
.userHandle
))
882 def handle_CVR(self
, params
):
883 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
885 def handle_XFR(self
, params
):
886 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
887 id, refType
, addr
= params
[:3]
888 # was addr a host:port pair?
890 host
, port
= addr
.split(':')
895 self
.gotNotificationReferral(host
, int(port
))
899 def gotNotificationReferral(self
, host
, port
):
901 called when we get a referral to the notification server.
903 @param host: the notification server's hostname
904 @param port: the port to connect to
909 class DispatchFactory(ClientFactory
):
911 This class keeps the state for the DispatchClient.
913 @ivar userHandle: the userHandle to request a notification
916 protocol
= DispatchClient
921 class NotificationClient(MSNEventBase
):
923 This class provides support for clients connecting
924 to the notification server.
927 factory
= None # sssh pychecker
929 def __init__(self
, currentID
=0):
930 MSNEventBase
.__init
__(self
)
931 self
.currentID
= currentID
932 self
._state
= ['DISCONNECTED', {}]
934 self
.pingCheckTask
= None
935 self
.msnobj
= MSNObject()
937 def _setState(self
, state
):
938 self
._state
[0] = state
941 return self
._state
[0]
943 def _getStateData(self
, key
):
944 return self
._state
[1][key
]
946 def _setStateData(self
, key
, value
):
947 self
._state
[1][key
] = value
949 def _remStateData(self
, *args
):
950 for key
in args
: del self
._state
[1][key
]
952 def connectionMade(self
):
953 MSNEventBase
.connectionMade(self
)
954 self
._setState
('CONNECTED')
955 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
957 def connectionLost(self
, reason
):
958 self
._setState
('DISCONNECTED')
960 if self
.pingCheckTask
:
961 self
.pingCheckTask
.stop()
962 self
.pingCheckTask
= None
963 MSNEventBase
.connectionLost(self
, reason
)
965 def _getEmailFields(self
, message
):
966 fields
= message
.getMessage().strip().split('\n')
970 if len(a
) != 2: continue
977 def _gotInitialEmailNotification(self
, message
):
978 values
= self
._getEmailFields
(message
)
980 inboxunread
= int(values
["Inbox-Unread"])
981 foldersunread
= int(values
["Folders-Unread"])
984 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
985 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
987 def _gotEmailNotification(self
, message
):
988 values
= self
._getEmailFields
(message
)
990 mailfrom
= values
["From"]
991 fromaddr
= values
["From-Addr"]
992 subject
= values
["Subject"]
993 junkbeginning
= "=?\"us-ascii\"?Q?"
995 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
997 # If any of the fields weren't found then it's not a big problem. We just ignore the message
999 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
1001 def _gotMSNAlert(self
, message
):
1002 notification
= xmlw
.parseText(message
.message
, beExtremelyLenient
=True)
1003 siteurl
= notification
.getAttribute("siteurl")
1004 notid
= notification
.getAttribute("id")
1007 for e
in notification
.elements():
1013 msgid
= msg
.getAttribute("id")
1018 for e
in msg
.elements():
1019 if e
.name
== "ACTION":
1020 action
= e
.getAttribute("url")
1021 if e
.name
== "SUBSCR":
1022 subscr
= e
.getAttribute("url")
1023 if e
.name
== "BODY":
1024 for e2
in e
.elements():
1025 if e2
.name
== "TEXT":
1026 bodytext
= e2
.__str__()
1027 if not (action
and subscr
and bodytext
): return
1029 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
1030 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
1032 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
1034 def _gotUBX(self
, message
):
1035 lm
= message
.message
.lower()
1036 p1
= lm
.find("<psm>") + 5
1037 p2
= lm
.find("</psm>")
1038 if p1
>= 0 and p2
>= 0:
1039 personal
= unescapeFromXml(message
.message
[p1
:p2
])
1040 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
1041 if not msnContact
: return
1042 msnContact
.personal
= personal
1043 self
.contactPersonalChanged(message
.userHandle
, personal
)
1045 self
.contactPersonalChanged(message
.userHandle
, '')
1047 def checkMessage(self
, message
):
1048 """ hook used for detecting specific notification messages """
1049 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
1050 if 'text/x-msmsgsprofile' in cTypes
:
1051 self
.gotProfile(message
)
1053 elif "text/x-msmsgsinitialemailnotification" in cTypes
:
1054 self
._gotInitialEmailNotification
(message
)
1056 elif "text/x-msmsgsemailnotification" in cTypes
:
1057 self
._gotEmailNotification
(message
)
1059 elif "NOTIFICATION" == message
.userHandle
and message
.specialMessage
== True:
1060 self
._gotMSNAlert
(message
)
1062 elif "UBX" == message
.screenName
and message
.specialMessage
== True:
1063 self
._gotUBX
(message
)
1067 ### protocol command handlers - no need to override these
1069 def handle_VER(self
, params
):
1070 versions
= params
[1:]
1071 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
1072 self
.transport
.loseConnection()
1073 raise MSNProtocolError
, "Invalid version response"
1074 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
1076 def handle_CVR(self
, params
):
1077 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
1079 def handle_USR(self
, params
):
1080 if not (4 <= len(params
) <= 6):
1081 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
1083 mechanism
= params
[1]
1084 if mechanism
== "OK":
1085 self
.loggedIn(params
[2], int(params
[3]))
1086 elif params
[2].upper() == "S":
1087 # we need to obtain auth from a passport server
1089 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3])
1090 d
.addCallback(self
._passportLogin
)
1091 d
.addErrback(self
._passportError
)
1093 def _passportLogin(self
, result
):
1094 if result
[0] == LOGIN_REDIRECT
:
1095 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
1096 result
[1], cached
=1, authData
=result
[2])
1097 d
.addCallback(self
._passportLogin
)
1098 d
.addErrback(self
._passportError
)
1099 elif result
[0] == LOGIN_SUCCESS
:
1100 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
1101 elif result
[0] == LOGIN_FAILURE
:
1102 self
.loginFailure(result
[1])
1104 def _passportError(self
, failure
):
1105 self
.loginFailure("Exception while authenticating: %s" % failure
)
1107 def handle_CHG(self
, params
):
1109 if not self
._fireCallback
(id, params
[1]):
1110 if self
.factory
: self
.factory
.status
= params
[1]
1111 self
.statusChanged(params
[1])
1113 def handle_ILN(self
, params
):
1114 #checkParamLen(len(params), 6, 'ILN')
1115 msnContact
= self
.factory
.contacts
.getContact(params
[2])
1116 if not msnContact
: return
1117 msnContact
.status
= params
[1]
1118 msnContact
.screenName
= unquote(params
[3])
1119 if len(params
) > 4: msnContact
.caps
= int(params
[4])
1121 self
.handleAvatarHelper(msnContact
, params
[5])
1123 self
.handleAvatarGoneHelper(msnContact
)
1124 self
.gotContactStatus(params
[2], params
[1], unquote(params
[3]))
1126 def handleAvatarGoneHelper(self
, msnContact
):
1127 if msnContact
.msnobj
:
1128 msnContact
.msnobj
= None
1129 msnContact
.msnobjGot
= True
1130 self
.contactAvatarChanged(msnContact
.userHandle
, "")
1132 def handleAvatarHelper(self
, msnContact
, msnobjStr
):
1133 msnobj
= MSNObject(unquote(msnobjStr
))
1134 if not msnContact
.msnobj
or msnobj
.sha1d
!= msnContact
.msnobj
.sha1d
:
1135 if MSNP2PDEBUG
: log
.msg("Updated MSNObject received!" + msnobjStr
)
1136 msnContact
.msnobj
= msnobj
1137 msnContact
.msnobjGot
= False
1138 self
.contactAvatarChanged(msnContact
.userHandle
, msnContact
.msnobj
.sha1d
)
1140 def handle_CHL(self
, params
):
1141 checkParamLen(len(params
), 2, 'CHL')
1142 response
= msnp11chl
.doChallenge(params
[1])
1143 self
.sendLine("QRY %s %s %s" % (self
._nextTransactionID
(), msnp11chl
.MSNP11_PRODUCT_ID
, len(response
)))
1144 self
.transport
.write(response
)
1146 def handle_QRY(self
, params
):
1149 def handle_NLN(self
, params
):
1150 if not self
.factory
: return
1151 msnContact
= self
.factory
.contacts
.getContact(params
[1])
1152 if not msnContact
: return
1153 msnContact
.status
= params
[0]
1154 msnContact
.screenName
= unquote(params
[2])
1155 if len(params
) > 3: msnContact
.caps
= int(params
[3])
1157 self
.handleAvatarHelper(msnContact
, params
[4])
1159 self
.handleAvatarGoneHelper(msnContact
)
1160 self
.contactStatusChanged(params
[1], params
[0], unquote(params
[2]))
1162 def handle_FLN(self
, params
):
1163 checkParamLen(len(params
), 1, 'FLN')
1164 msnContact
= self
.factory
.contacts
.getContact(params
[0])
1166 msnContact
.status
= STATUS_OFFLINE
1167 self
.contactOffline(params
[0])
1169 def handle_LST(self
, params
):
1170 if self
._getState
() != 'SYNC': return
1172 userHandle
, screenName
, userGuid
, lists
, groups
= getVals(params
)
1174 if not userHandle
or lists
< 1:
1175 raise MSNProtocolError
, "Unknown LST " + str(params
) # debug
1176 contact
= MSNContact(userGuid
, userHandle
, screenName
, lists
)
1177 if contact
.lists
& FORWARD_LIST
:
1178 contact
.groups
.extend(map(str, groups
))
1179 self
._getStateData
('list').addContact(contact
)
1180 self
._setStateData
('last_contact', contact
)
1181 sofar
= self
._getStateData
('lst_sofar') + 1
1182 if sofar
== self
._getStateData
('lst_reply'):
1183 # this is the best place to determine that
1184 # a syn realy has finished - msn _may_ send
1185 # BPR information for the last contact
1186 # which is unfortunate because it means
1187 # that the real end of a syn is non-deterministic.
1188 # to handle this we'll keep 'last_contact' hanging
1189 # around in the state data and update it if we need
1191 self
._setState
('SESSION')
1192 contacts
= self
._getStateData
('list')
1193 phone
= self
._getStateData
('phone')
1194 id = self
._getStateData
('synid')
1195 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1196 self
._fireCallback
(id, contacts
, phone
)
1198 self
._setStateData
('lst_sofar',sofar
)
1200 def handle_BLP(self
, params
):
1201 # check to see if this is in response to a SYN
1202 if self
._getState
() == 'SYNC':
1203 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
1206 self
.factory
.contacts
.privacy
= listCodeToID
[params
[1].lower()]
1207 self
._fireCallback
(id, params
[1])
1209 def handle_GTC(self
, params
):
1210 # check to see if this is in response to a SYN
1211 if self
._getState
() == 'SYNC':
1212 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
1213 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
1214 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1217 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
1218 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
1219 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1221 def handle_SYN(self
, params
):
1223 self
._setStateData
('phone', []) # Always needs to be set
1224 if params
[3] == 0: # No LST will be received. New account?
1225 self
._setState
('SESSION')
1226 self
._fireCallback
(id, None, None)
1228 contacts
= MSNContactList()
1229 self
._setStateData
('list', contacts
)
1230 self
._setStateData
('lst_reply', int(params
[3]))
1231 self
._setStateData
('lsg_reply', int(params
[4]))
1232 self
._setStateData
('lst_sofar', 0)
1234 def handle_LSG(self
, params
):
1235 if self
._getState
() == 'SYNC':
1236 self
._getStateData
('list').groups
[params
[1]] = unquote(params
[0])
1238 def handle_PRP(self
, params
):
1239 if params
[1] == "MFN":
1240 self
._fireCallback
(int(params
[0]), unquote(params
[2]))
1241 elif self
._getState
() == 'SYNC':
1242 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
1244 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
1246 def handle_BPR(self
, params
):
1247 numParams
= len(params
)
1248 if numParams
== 2: # part of a syn
1249 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
1250 elif numParams
== 4:
1251 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_BPR called with no contact list" # debug
1252 self
.factory
.contacts
.version
= int(params
[0])
1253 userHandle
, phoneType
, number
= params
[1], params
[2], unquote(params
[3])
1254 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1255 self
.gotPhoneNumber(userHandle
, phoneType
, number
)
1258 def handle_ADG(self
, params
):
1259 checkParamLen(len(params
), 5, 'ADG')
1261 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
1262 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
1264 def handle_RMG(self
, params
):
1265 checkParamLen(len(params
), 3, 'RMG')
1267 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
1268 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
1270 def handle_REG(self
, params
):
1271 checkParamLen(len(params
), 5, 'REG')
1273 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
1274 raise MSNProtocolError
, "REG response does not match up to a request" # debug
1276 def handle_ADC(self
, params
):
1277 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_ADC called with no contact list"
1278 numParams
= len(params
)
1279 if numParams
< 3 or params
[1].upper() not in ('AL','BL','RL','FL','PL'):
1280 raise MSNProtocolError
, "Invalid Paramaters for ADC" # debug
1282 listType
= params
[1].lower()
1283 userHandle
, screenName
, userGuid
, ignored1
, groups
= getVals(params
[2:])
1285 if groups
and listType
.upper() != FORWARD_LIST
:
1286 raise MSNProtocolError
, "Only forward list can contain groups" # debug
1288 if not self
._fireCallback
(id, listCodeToID
[listType
], userGuid
, userHandle
, screenName
):
1289 c
= self
.factory
.contacts
.getContact(userHandle
)
1291 c
= MSNContact(userGuid
=userGuid
, userHandle
=userHandle
, screenName
=screenName
)
1292 self
.factory
.contacts
.addContact(c
)
1293 c
.addToList(PENDING_LIST
)
1294 self
.userAddedMe(userGuid
, userHandle
, screenName
)
1296 def handle_REM(self
, params
):
1297 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_REM called with no contact list available!"
1298 numParams
= len(params
)
1299 if numParams
< 3 or params
[1].upper() not in ('AL','BL','FL','RL','PL'):
1300 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1302 listType
= params
[1].lower()
1303 userHandle
= params
[2]
1306 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1307 groupID
= int(params
[3])
1308 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, groupID
):
1309 if listType
.upper() != "RL": return
1310 c
= self
.factory
.contacts
.getContact(userHandle
)
1312 c
.removeFromList(REVERSE_LIST
)
1313 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1314 self
.userRemovedMe(userHandle
)
1316 def handle_XFR(self
, params
):
1317 checkParamLen(len(params
), 5, 'XFR')
1319 # check to see if they sent a host/port pair
1321 host
, port
= params
[2].split(':')
1326 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1327 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1329 def handle_RNG(self
, params
):
1330 checkParamLen(len(params
), 6, 'RNG')
1331 # check for host:port pair
1333 host
, port
= params
[1].split(":")
1338 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1341 def handle_NOT(self
, params
):
1342 checkParamLen(len(params
), 1, 'NOT')
1344 messageLen
= int(params
[0])
1345 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1346 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", specialMessage
=True)
1349 def handle_UBX(self
, params
):
1350 checkParamLen(len(params
), 2, 'UBX')
1352 messageLen
= int(params
[1])
1353 except ValueError: raise MSNProtocolError
, "Invalid Parameter for UBX length argument"
1355 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
="UBX", specialMessage
=True)
1358 self
.contactPersonalChanged(params
[0], '')
1360 def handle_UUX(self
, params
):
1361 checkParamLen(len(params
), 2, 'UUX')
1362 if params
[1] != '0': return
1364 self
._fireCallback
(id)
1366 def handle_OUT(self
, params
):
1367 checkParamLen(len(params
), 1, 'OUT')
1368 if params
[0] == "OTH": self
.multipleLogin()
1369 elif params
[0] == "SSD": self
.serverGoingDown()
1370 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1372 def handle_QNG(self
, params
):
1373 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1377 def pingChecker(self
):
1378 if self
.pingCounter
> 5:
1379 # The server has ignored 5 pings, lets kill the connection
1380 self
.transport
.loseConnection()
1382 self
.sendLine("PNG")
1383 self
.pingCounter
+= 1
1385 def pingCheckerStart(self
, *args
):
1386 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1387 self
.pingCheckTask
.start(PINGSPEED
)
1389 def loggedIn(self
, userHandle
, verified
):
1391 Called when the client has logged in.
1392 The default behaviour of this method is to
1393 update the factory with our screenName and
1394 to sync the contact list (factory.contacts).
1395 When this is complete self.listSynchronized
1398 @param userHandle: our userHandle
1399 @param verified: 1 if our passport has been (verified), 0 if not.
1400 (i'm not sure of the significace of this)
1404 d
.addCallback(self
.listSynchronized
)
1405 d
.addCallback(self
.pingCheckerStart
)
1407 def loginFailure(self
, message
):
1409 Called when the client fails to login.
1411 @param message: a message indicating the problem that was encountered
1415 def gotProfile(self
, message
):
1417 Called after logging in when the server sends an initial
1418 message with MSN/passport specific profile information
1419 such as country, number of kids, etc.
1420 Check the message headers for the specific values.
1422 @param message: The profile message
1426 def listSynchronized(self
, *args
):
1428 Lists are now synchronized by default upon logging in, this
1429 method is called after the synchronization has finished
1430 and the factory now has the up-to-date contacts.
1434 def contactAvatarChanged(self
, userHandle
, hash):
1436 Called when we receive the first, or a new <msnobj/> from a
1439 @param userHandle: contact who's msnobj has been changed
1440 @param hash: sha1 hash of their avatar
1443 def statusChanged(self
, statusCode
):
1445 Called when our status changes and its not in response to a
1448 @param statusCode: 3-letter status code
1452 def gotContactStatus(self
, userHandle
, statusCode
, screenName
):
1454 Called when we receive a list of statuses upon login.
1456 @param userHandle: the contact's user handle (passport)
1457 @param statusCode: 3-letter status code
1458 @param screenName: the contact's screen name
1462 def contactStatusChanged(self
, userHandle
, statusCode
, screenName
):
1464 Called when we're notified that a contact's status has changed.
1466 @param userHandle: the contact's user handle (passport)
1467 @param statusCode: 3-letter status code
1468 @param screenName: the contact's screen name
1472 def contactPersonalChanged(self
, userHandle
, personal
):
1474 Called when a contact's personal message changes.
1476 @param userHandle: the contact who changed their personal message
1477 @param personal : the new personal message
1481 def contactOffline(self
, userHandle
):
1483 Called when a contact goes offline.
1485 @param userHandle: the contact's user handle
1489 def gotMessage(self
, message
):
1491 Called when there is a message from the notification server
1492 that is not understood by default.
1494 @param message: the MSNMessage.
1498 def gotMSNAlert(self
, body
, action
, subscr
):
1500 Called when the server sends an MSN Alert (http://alerts.msn.com)
1502 @param body : the alert text
1503 @param action: a URL with more information for the user to view
1504 @param subscr: a URL the user can use to modify their alert subscription
1508 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1510 Called when the server sends you details about your hotmail
1511 inbox. This is only ever called once, on login.
1513 @param inboxunread : the number of unread items in your inbox
1514 @param foldersunread: the number of unread items in other folders
1518 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1520 Called when the server sends us realtime email
1521 notification. This means that you have received
1522 a new email in your hotmail inbox.
1524 @param mailfrom: the sender of the email
1525 @param fromaddr: the sender of the email (I don't know :P)
1526 @param subject : the email subject
1530 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1532 Called when the server sends us phone details about
1533 a specific user (for example after a user is added
1534 the server will send their status, phone details etc.
1536 @param userHandle: the contact's user handle (passport)
1537 @param phoneType: the specific phoneType
1538 (*_PHONE constants or HAS_PAGER)
1539 @param number: the value/phone number.
1543 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1545 Called when a user adds me to their list. (ie. they have been added to
1548 @param userHandle: the userHandle of the user
1549 @param screenName: the screen name of the user
1553 def userRemovedMe(self
, userHandle
):
1555 Called when a user removes us from their contact list
1556 (they are no longer on our reverseContacts list.
1558 @param userHandle: the contact's user handle (passport)
1562 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1563 key
, userHandle
, screenName
):
1565 Called when we get an invitation to a switchboard server.
1566 This happens when a user requests a chat session with us.
1568 @param sessionID: session ID number, must be remembered for logging in
1569 @param host: the hostname of the switchboard server
1570 @param port: the port to connect to
1571 @param key: used for authorization when connecting
1572 @param userHandle: the user handle of the person who invited us
1573 @param screenName: the screen name of the person who invited us
1577 def multipleLogin(self
):
1579 Called when the server says there has been another login
1580 under our account, the server should disconnect us right away.
1584 def serverGoingDown(self
):
1586 Called when the server has notified us that it is going down for
1593 def changeStatus(self
, status
):
1595 Change my current status. This method will add
1596 a default callback to the returned Deferred
1597 which will update the status attribute of the
1600 @param status: 3-letter status code (as defined by
1601 the STATUS_* constants)
1602 @return: A Deferred, the callback of which will be
1603 fired when the server confirms the change
1604 of status. The callback argument will be
1605 a tuple with the new status code as the
1609 id, d
= self
._createIDMapping
()
1610 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1612 self
.factory
.status
= r
[0]
1614 return d
.addCallback(_cb
)
1616 def setPrivacyMode(self
, privLevel
):
1618 Set my privacy mode on the server.
1621 This only keeps the current privacy setting on
1622 the server for later retrieval, it does not
1623 effect the way the server works at all.
1625 @param privLevel: This parameter can be true, in which
1626 case the server will keep the state as
1627 'al' which the official client interprets
1628 as -> allow messages from only users on
1629 the allow list. Alternatively it can be
1630 false, in which case the server will keep
1631 the state as 'bl' which the official client
1632 interprets as -> allow messages from all
1633 users except those on the block list.
1635 @return: A Deferred, the callback of which will be fired when
1636 the server replies with the new privacy setting.
1637 The callback argument will be a tuple, the only element
1638 of which being either 'al' or 'bl' (the new privacy setting).
1641 id, d
= self
._createIDMapping
()
1642 if privLevel
: self
.sendLine("BLP %s AL" % id)
1643 else: self
.sendLine("BLP %s BL" % id)
1648 Used for keeping an up-to-date contact list.
1649 A callback is added to the returned Deferred
1650 that updates the contact list on the factory
1651 and also sets my state to STATUS_ONLINE.
1654 This is called automatically upon signing
1655 in using the version attribute of
1656 factory.contacts, so you may want to persist
1657 this object accordingly. Because of this there
1658 is no real need to ever call this method
1661 @return: A Deferred, the callback of which will be
1662 fired when the server sends an adequate reply.
1663 The callback argument will be a tuple with two
1664 elements, the new list (MSNContactList) and
1665 your current state (a dictionary). If the version
1666 you sent _was_ the latest list version, both elements
1667 will be None. To just request the list send a version of 0.
1670 self
._setState
('SYNC')
1671 id, d
= self
._createIDMapping
(data
=None)
1672 self
._setStateData
('synid',id)
1673 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1675 self
.changeStatus(STATUS_ONLINE
)
1676 if r
[0] is not None:
1677 self
.factory
.contacts
= r
[0]
1679 return d
.addCallback(_cb
)
1681 def setPhoneDetails(self
, phoneType
, value
):
1683 Set/change my phone numbers stored on the server.
1685 @param phoneType: phoneType can be one of the following
1686 constants - HOME_PHONE, WORK_PHONE,
1687 MOBILE_PHONE, HAS_PAGER.
1688 These are pretty self-explanatory, except
1689 maybe HAS_PAGER which refers to whether or
1690 not you have a pager.
1691 @param value: for all of the *_PHONE constants the value is a
1692 phone number (str), for HAS_PAGER accepted values
1693 are 'Y' (for yes) and 'N' (for no).
1695 @return: A Deferred, the callback for which will be fired when
1696 the server confirms the change has been made. The
1697 callback argument will be a tuple with 2 elements, the
1698 first being the new list version (int) and the second
1699 being the new phone number value (str).
1701 raise "ProbablyDoesntWork"
1702 # XXX: Add a default callback which updates
1703 # factory.contacts.version and the relevant phone
1705 id, d
= self
._createIDMapping
()
1706 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1709 def addListGroup(self
, name
):
1711 Used to create a new list group.
1712 A default callback is added to the
1713 returned Deferred which updates the
1714 contacts attribute of the factory.
1716 @param name: The desired name of the new group.
1718 @return: A Deferred, the callbacck for which will be called
1719 when the server clarifies that the new group has been
1720 created. The callback argument will be a tuple with 3
1721 elements: the new list version (int), the new group name
1722 (str) and the new group ID (int).
1725 raise "ProbablyDoesntWork"
1726 id, d
= self
._createIDMapping
()
1727 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1729 if self
.factory
.contacts
:
1730 self
.factory
.contacts
.version
= r
[0]
1731 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1733 return d
.addCallback(_cb
)
1735 def remListGroup(self
, groupID
):
1737 Used to remove a list group.
1738 A default callback is added to the
1739 returned Deferred which updates the
1740 contacts attribute of the factory.
1742 @param groupID: the ID of the desired group to be removed.
1744 @return: A Deferred, the callback for which will be called when
1745 the server clarifies the deletion of the group.
1746 The callback argument will be a tuple with 2 elements:
1747 the new list version (int) and the group ID (int) of
1751 raise "ProbablyDoesntWork"
1752 id, d
= self
._createIDMapping
()
1753 self
.sendLine("RMG %s %s" % (id, groupID
))
1755 self
.factory
.contacts
.version
= r
[0]
1756 self
.factory
.contacts
.remGroup(r
[1])
1758 return d
.addCallback(_cb
)
1760 def renameListGroup(self
, groupID
, newName
):
1762 Used to rename an existing list group.
1763 A default callback is added to the returned
1764 Deferred which updates the contacts attribute
1767 @param groupID: the ID of the desired group to rename.
1768 @param newName: the desired new name for the group.
1770 @return: A Deferred, the callback for which will be called
1771 when the server clarifies the renaming.
1772 The callback argument will be a tuple of 3 elements,
1773 the new list version (int), the group id (int) and
1774 the new group name (str).
1777 raise "ProbablyDoesntWork"
1778 id, d
= self
._createIDMapping
()
1779 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1781 self
.factory
.contacts
.version
= r
[0]
1782 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1784 return d
.addCallback(_cb
)
1786 def addContact(self
, listType
, userHandle
):
1788 Used to add a contact to the desired list.
1789 A default callback is added to the returned
1790 Deferred which updates the contacts attribute of
1791 the factory with the new contact information.
1792 If you are adding a contact to the forward list
1793 and you want to associate this contact with multiple
1794 groups then you will need to call this method for each
1795 group you would like to add them to, changing the groupID
1796 parameter. The default callback will take care of updating
1797 the group information on the factory's contact list.
1799 @param listType: (as defined by the *_LIST constants)
1800 @param userHandle: the user handle (passport) of the contact
1803 @return: A Deferred, the callback for which will be called when
1804 the server has clarified that the user has been added.
1805 The callback argument will be a tuple with 4 elements:
1806 the list type, the contact's user handle, the new list
1807 version, and the group id (if relevant, otherwise it
1811 id, d
= self
._createIDMapping
()
1812 try: # Make sure the contact isn't actually on the list
1813 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1814 except AttributeError: pass
1815 listType
= listIDToCode
[listType
].upper()
1816 if listType
== "FL":
1817 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1819 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1822 if not self
.factory
: return
1823 c
= self
.factory
.contacts
.getContact(r
[2])
1825 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1826 self
.factory
.contacts
.addContact(c
)
1827 #if r[3]: c.groups.append(r[3])
1830 return d
.addCallback(_cb
)
1832 def remContact(self
, listType
, userHandle
):
1834 Used to remove a contact from the desired list.
1835 A default callback is added to the returned deferred
1836 which updates the contacts attribute of the factory
1837 to reflect the new contact information.
1839 @param listType: (as defined by the *_LIST constants)
1840 @param userHandle: the user handle (passport) of the
1841 contact being removed
1843 @return: A Deferred, the callback for which will be called when
1844 the server has clarified that the user has been removed.
1845 The callback argument will be a tuple of 3 elements:
1846 the list type, the contact's user handle and the group ID
1847 (if relevant, otherwise it will be None)
1850 id, d
= self
._createIDMapping
()
1851 try: # Make sure the contact is actually on this list
1852 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1853 except AttributeError: return
1854 listType
= listIDToCode
[listType
].upper()
1855 if listType
== "FL":
1857 c
= self
.factory
.contacts
.getContact(userHandle
)
1858 userGuid
= c
.userGuid
1859 except AttributeError: return
1860 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1862 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1865 if listType
== "FL":
1866 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1867 l
= self
.factory
.contacts
1868 c
= l
.getContact(r
[1])
1872 if group
: # they may not have been removed from the list
1873 c
.groups
.remove(group
)
1874 if c
.groups
: shouldRemove
= 0
1876 c
.removeFromList(r
[0])
1877 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1879 return d
.addCallback(_cb
)
1881 def changeScreenName(self
, newName
):
1883 Used to change your current screen name.
1884 A default callback is added to the returned
1885 Deferred which updates the screenName attribute
1886 of the factory and also updates the contact list
1889 @param newName: the new screen name
1891 @return: A Deferred, the callback for which will be called
1892 when the server acknowledges the change.
1893 The callback argument will be a tuple of 1 element,
1894 the new screen name.
1897 id, d
= self
._createIDMapping
()
1898 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1900 self
.factory
.screenName
= r
[0]
1902 return d
.addCallback(_cb
)
1904 def changePersonalMessage(self
, personal
):
1906 Used to change your personal message.
1908 @param personal: the new screen name
1910 @return: A Deferred, the callback for which will be called
1911 when the server acknowledges the change.
1912 The callback argument will be a tuple of 1 element,
1913 the personal message.
1916 id, d
= self
._createIDMapping
()
1919 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1920 self
.sendLine("UUX %s %s" % (id, len(data
)))
1921 self
.transport
.write(data
)
1923 self
.factory
.personal
= personal
1925 return d
.addCallback(_cb
)
1927 def changeAvatar(self
, imageData
, push
):
1929 Used to change the avatar that other users see.
1931 @param imageData: the PNG image data to set as the avatar
1932 @param push : whether to push the update to the server
1933 (it will otherwise be sent with the next
1936 @return: If push==True, a Deferred, the callback for which
1937 will be called when the server acknowledges the change.
1938 The callback argument will be the same as for changeStatus.
1941 if self
.msnobj
and imageData
== self
.msnobj
.imageData
: return
1943 self
.msnobj
.setData(self
.factory
.userHandle
, imageData
)
1945 self
.msnobj
.setNull()
1946 if push
: return self
.changeStatus(self
.factory
.status
) # Push to server
1949 def requestSwitchboardServer(self
):
1951 Used to request a switchboard server to use for conversations.
1953 @return: A Deferred, the callback for which will be called when
1954 the server responds with the switchboard information.
1955 The callback argument will be a tuple with 3 elements:
1956 the host of the switchboard server, the port and a key
1957 used for logging in.
1960 id, d
= self
._createIDMapping
()
1961 self
.sendLine("XFR %s SB" % id)
1966 Used to log out of the notification server.
1967 After running the method the server is expected
1968 to close the connection.
1971 if self
.pingCheckTask
:
1972 self
.pingCheckTask
.stop()
1973 self
.pingCheckTask
= None
1974 self
.sendLine("OUT")
1975 self
.transport
.loseConnection()
1977 class NotificationFactory(ClientFactory
):
1979 Factory for the NotificationClient protocol.
1980 This is basically responsible for keeping
1981 the state of the client and thus should be used
1982 in a 1:1 situation with clients.
1984 @ivar contacts: An MSNContactList instance reflecting
1985 the current contact list -- this is
1986 generally kept up to date by the default
1988 @ivar userHandle: The client's userHandle, this is expected
1989 to be set by the client and is used by the
1990 protocol (for logging in etc).
1991 @ivar screenName: The client's current screen-name -- this is
1992 generally kept up to date by the default
1994 @ivar password: The client's password -- this is (obviously)
1995 expected to be set by the client.
1996 @ivar passportServer: This must point to an msn passport server
1997 (the whole URL is required)
1998 @ivar status: The status of the client -- this is generally kept
1999 up to date by the default command handlers
2006 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
2008 protocol
= NotificationClient
2011 class SwitchboardClient(MSNEventBase
):
2013 This class provides support for clients connecting to a switchboard server.
2015 Switchboard servers are used for conversations with other people
2016 on the MSN network. This means that the number of conversations at
2017 any given time will be directly proportional to the number of
2018 connections to varioius switchboard servers.
2020 MSN makes no distinction between single and group conversations,
2021 so any number of users may be invited to join a specific conversation
2022 taking place on a switchboard server.
2024 @ivar key: authorization key, obtained when receiving
2025 invitation / requesting switchboard server.
2026 @ivar userHandle: your user handle (passport)
2027 @ivar sessionID: unique session ID, used if you are replying
2028 to a switchboard invitation
2029 @ivar reply: set this to 1 in connectionMade or before to signifiy
2030 that you are replying to a switchboard invitation.
2031 @ivar msnobj: the MSNObject for the user's avatar. So that the
2032 switchboard can distribute it to anyone who asks.
2044 MSNEventBase
.__init
__(self
)
2045 self
.pendingUsers
= {}
2046 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
2049 def connectionMade(self
):
2050 MSNEventBase
.connectionMade(self
)
2053 def connectionLost(self
, reason
):
2054 self
.cookies
['iCookies'] = {}
2055 MSNEventBase
.connectionLost(self
, reason
)
2057 def _sendInit(self
):
2059 send initial data based on whether we are replying to an invitation
2062 id = self
._nextTransactionID
()
2064 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
2066 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
2068 def _newInvitationCookie(self
):
2070 if self
._iCookie
> 1000: self
._iCookie
= 1
2071 return self
._iCookie
2073 def _checkTyping(self
, message
, cTypes
):
2074 """ helper method for checkMessage """
2075 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
2076 self
.gotContactTyping(message
)
2079 def _checkFileInvitation(self
, message
, info
):
2080 """ helper method for checkMessage """
2081 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
2083 cookie
= info
['Invitation-Cookie']
2084 filename
= info
['Application-File']
2085 filesize
= int(info
['Application-FileSize'])
2086 connectivity
= (info
.get('Connectivity').lower() == 'y')
2088 log
.msg('Received munged file transfer request ... ignoring.')
2090 raise NotImplementedError
2091 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
2094 def _handleP2PMessage(self
, message
):
2095 """ helper method for msnslp messages (file transfer & avatars) """
2096 if not message
.getHeader("P2P-Dest") == self
.userHandle
: return
2097 packet
= message
.message
2098 binaryFields
= BinaryFields(packet
=packet
)
2099 if binaryFields
[0] != 0:
2100 slpLink
= self
.slpLinks
.get(binaryFields
[0])
2102 # Link has been killed. Ignore
2104 if slpLink
.remoteUser
== message
.userHandle
:
2105 slpLink
.handlePacket(packet
)
2106 if binaryFields
[5] == BinaryFields
.ACK
or binaryFields
[5] == BinaryFields
.BYEGOT
:
2107 pass # Ignore the ACKs to SLP messages
2109 slpMessage
= MSNSLPMessage(packet
)
2111 # Always try and give a slpMessage to a slpLink first.
2112 # If none can be found, and it was INVITE, then create
2113 # one to handle the session.
2114 for slpLink
in self
.slpLinks
.values():
2115 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
2116 slpLink
.handleSLPMessage(slpMessage
)
2119 slpLink
= None # Was not handled
2121 if not slpLink
and slpMessage
.method
== "INVITE":
2122 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
2123 context
= FileContext(slpMessage
.context
)
2124 slpLink
= SLPLink_FileReceive(remoteUser
=slpMessage
.fro
, switchboard
=self
, filename
=context
.filename
, filesize
=context
.filesize
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
, branch
=slpMessage
.branch
)
2125 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2126 self
.gotFileReceive(slpLink
)
2127 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
2128 # Check that we have an avatar to send
2130 slpLink
= SLPLink_AvatarSend(remoteUser
=slpMessage
.fro
, switchboard
=self
, filesize
=self
.msnobj
.size
, sessionID
=slpMessage
.sessionID
, sessionGuid
=slpMessage
.sessionGuid
)
2131 slpLink
.write(self
.msnobj
.imageData
)
2134 # They shouldn't have sent a request if we have
2135 # no avatar. So we'll just ignore them.
2136 # FIXME We should really send an error
2139 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
2141 # Always need to ACK these packets if we can
2142 slpLink
.sendP2PACK(binaryFields
)
2145 def checkMessage(self
, message
):
2147 hook for detecting any notification type messages
2148 (e.g. file transfer)
2150 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2151 if self
._checkTyping
(message
, cTypes
): return 0
2152 if 'text/x-msmsgsinvite' in cTypes
:
2153 # header like info is sent as part of the message body.
2155 for line
in message
.message
.split('\r\n'):
2157 key
, val
= line
.split(':')
2158 info
[key
] = val
.lstrip()
2159 except ValueError: continue
2160 if self
._checkFileInvitation
(message
, info
): return 0
2161 elif 'application/x-msnmsgrp2p' in cTypes
:
2162 self
._handleP
2PMessage
(message
)
2167 def handle_USR(self
, params
):
2168 checkParamLen(len(params
), 4, 'USR')
2169 if params
[1] == "OK":
2173 def handle_CAL(self
, params
):
2174 checkParamLen(len(params
), 3, 'CAL')
2176 if params
[1].upper() == "RINGING":
2177 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2180 def handle_JOI(self
, params
):
2181 checkParamLen(len(params
), 2, 'JOI')
2182 self
.userJoined(params
[0], unquote(params
[1]))
2184 # users participating in the current chat
2185 def handle_IRO(self
, params
):
2186 checkParamLen(len(params
), 5, 'IRO')
2187 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2188 if params
[1] == params
[2]:
2189 self
.gotChattingUsers(self
.pendingUsers
)
2190 self
.pendingUsers
= {}
2192 # finished listing users
2193 def handle_ANS(self
, params
):
2194 checkParamLen(len(params
), 2, 'ANS')
2195 if params
[1] == "OK":
2198 def handle_ACK(self
, params
):
2199 checkParamLen(len(params
), 1, 'ACK')
2200 self
._fireCallback
(int(params
[0]), None)
2202 def handle_NAK(self
, params
):
2203 checkParamLen(len(params
), 1, 'NAK')
2204 self
._fireCallback
(int(params
[0]), None)
2206 def handle_BYE(self
, params
):
2207 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2208 self
.userLeft(params
[0])
2214 called when all login details have been negotiated.
2215 Messages can now be sent, or new users invited.
2219 def gotChattingUsers(self
, users
):
2221 called after connecting to an existing chat session.
2223 @param users: A dict mapping user handles to screen names
2224 (current users taking part in the conversation)
2228 def userJoined(self
, userHandle
, screenName
):
2230 called when a user has joined the conversation.
2232 @param userHandle: the user handle (passport) of the user
2233 @param screenName: the screen name of the user
2237 def userLeft(self
, userHandle
):
2239 called when a user has left the conversation.
2241 @param userHandle: the user handle (passport) of the user.
2245 def gotMessage(self
, message
):
2247 called when we receive a message.
2249 @param message: the associated MSNMessage object
2253 def gotFileReceive(self
, fileReceive
):
2255 called when we receive a file send request from a contact
2257 @param fileReceive: msnft.MSNFTReceive_Base instance
2262 def gotSendRequest(self
, fileReceive
):
2264 called when we receive a file send request from a contact
2266 @param fileReceive: msnft.MSNFTReceive_Base instance
2270 def gotContactTyping(self
, message
):
2272 called when we receive the special type of message notifying
2273 us that a contact is typing a message.
2275 @param message: the associated MSNMessage object
2281 def inviteUser(self
, userHandle
):
2283 used to invite a user to the current switchboard server.
2285 @param userHandle: the user handle (passport) of the desired user.
2287 @return: A Deferred, the callback for which will be called
2288 when the server notifies us that the user has indeed
2289 been invited. The callback argument will be a tuple
2290 with 1 element, the sessionID given to the invited user.
2291 I'm not sure if this is useful or not.
2294 id, d
= self
._createIDMapping
()
2295 self
.sendLine("CAL %s %s" % (id, userHandle
))
2298 def sendMessage(self
, message
):
2300 used to send a message.
2302 @param message: the corresponding MSNMessage object.
2304 @return: Depending on the value of message.ack.
2305 If set to MSNMessage.MESSAGE_ACK or
2306 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2307 the callback for which will be fired when an ACK or
2308 NACK is received - the callback argument will be
2309 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2310 the return value is None.
2313 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2314 else: id, d
= self
._createIDMapping
()
2315 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2316 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2317 # apparently order matters with at least MIME-Version and Content-Type
2318 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
2319 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
2320 # send the rest of the headers
2321 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
2322 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2323 self
.transport
.write("\r\n")
2324 self
.transport
.write(message
.message
)
2325 if MESSAGEDEBUG
: log
.msg(message
.message
)
2328 def sendAvatarRequest(self
, msnContact
):
2330 used to request an avatar from a user in this switchboard
2333 @param msnContact: the msnContact object to request an avatar for
2335 @return: A Deferred, the callback for which will be called
2336 when the avatar transfer succeeds.
2337 The callback argument will be a tuple with one element,
2338 the PNG avatar data.
2340 if not msnContact
.msnobj
: return
2342 def bufferClosed(data
):
2344 buffer = StringBuffer(bufferClosed
)
2345 slpLink
= SLPLink_AvatarReceive(remoteUser
=msnContact
.userHandle
, switchboard
=self
, consumer
=buffer, context
=msnContact
.msnobj
.text
)
2346 slpLink
.avatarDataBuffer
= buffer
2347 self
.slpLinks
[slpLink
.sessionID
] = slpLink
2350 def sendFile(self
, msnContact
, filename
, filesize
):
2352 used to send a file to a contact.
2354 @param msnContact: the MSNContact object to send a file to.
2355 @param filename: the name of the file to send.
2356 @param filesize: the size of the file to send.
2358 @return: (fileSend, d) A FileSend object and a Deferred.
2359 The Deferred will pass one argument in a tuple,
2360 whether or not the transfer is accepted. If you
2361 receive a True, then you can call write() on the
2362 fileSend object to send your file. Call close()
2363 when the file is done.
2364 NOTE: You MUST write() exactly as much as you
2365 declare in filesize.
2367 if not msnContact
.userHandle
: return
2368 # FIXME, check msnContact.caps to see if we should use old-style
2369 fileSend
= SLPLink_FileSend(remoteUser
=msnContact
.userHandle
, switchboard
=self
, filename
=filename
, filesize
=filesize
)
2370 self
.slpLinks
[fileSend
.sessionID
] = fileSend
2371 return fileSend
, fileSend
.acceptDeferred
2373 def sendTypingNotification(self
):
2375 Used to send a typing notification. Upon receiving this
2376 message the official client will display a 'user is typing'
2377 message to all other users in the chat session for 10 seconds.
2378 You should send one of these every 5 seconds as long as the
2382 m
.ack
= m
.MESSAGE_ACK_NONE
2383 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2384 m
.setHeader('TypingUser', self
.userHandle
)
2388 def sendFileInvitation(self
, fileName
, fileSize
):
2390 send an notification that we want to send a file.
2392 @param fileName: the file name
2393 @param fileSize: the file size
2395 @return: A Deferred, the callback of which will be fired
2396 when the user responds to this invitation with an
2397 appropriate message. The callback argument will be
2398 a tuple with 3 elements, the first being 1 or 0
2399 depending on whether they accepted the transfer
2400 (1=yes, 0=no), the second being an invitation cookie
2401 to identify your follow-up responses and the third being
2402 the message 'info' which is a dict of information they
2403 sent in their reply (this doesn't really need to be used).
2404 If you wish to proceed with the transfer see the
2405 sendTransferInfo method.
2407 cookie
= self
._newInvitationCookie
()
2410 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2411 m
.message
+= 'Application-Name: File Transfer\r\n'
2412 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2413 m
.message
+= 'Invitation-Command: INVITE\r\n'
2414 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2415 m
.message
+= 'Application-File: %s\r\n' % fileName
2416 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2417 m
.ack
= m
.MESSAGE_ACK_NONE
2419 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2422 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2424 send information relating to a file transfer session.
2426 @param accept: whether or not to go ahead with the transfer
2428 @param iCookie: the invitation cookie of previous replies
2429 relating to this transfer
2430 @param authCookie: the authentication cookie obtained from
2431 an FileSend instance
2433 @param port: the port on which an FileSend protocol is listening.
2436 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2437 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2438 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2439 m
.message
+= 'IP-Address: %s\r\n' % ip
2440 m
.message
+= 'Port: %s\r\n' % port
2441 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2443 m
.ack
= m
.MESSAGE_NACK
2448 def __init__(self
, filename
, filesize
, userHandle
):
2449 self
.consumer
= None
2450 self
.finished
= False
2453 self
.filename
, self
.filesize
, self
.userHandle
= filename
, filesize
, userHandle
2456 raise NotImplementedError
2458 def accept(self
, consumer
):
2459 if self
.consumer
: raise "AlreadyAccepted"
2460 self
.consumer
= consumer
2461 for data
in self
.buffer:
2462 self
.consumer
.write(data
)
2465 self
.consumer
.close()
2467 self
.consumer
.error()
2469 def write(self
, data
):
2470 if self
.error
or self
.finished
:
2471 raise IOError, "Attempt to write in an invalid state"
2473 self
.consumer
.write(data
)
2475 self
.buffer.append(data
)
2478 self
.finished
= True
2480 self
.consumer
.close()
2483 """ Represents the Context field for P2P file transfers """
2484 def __init__(self
, data
=""):
2492 if MSNP2PDEBUG
: print "FileContext packing:", self
.filename
, self
.filesize
2493 data
= struct
.pack("<LLQL", 638, 0x03, self
.filesize
, 0x01)
2494 data
= data
[:-1] # Uck, weird, but it works
2495 data
+= utf16net(self
.filename
)
2496 data
= ljust(data
, 570, '\0')
2497 data
+= struct
.pack("<L", 0xFFFFFFFF)
2498 data
= ljust(data
, 638, '\0')
2501 def parse(self
, packet
):
2502 self
.filesize
= struct
.unpack("<Q", packet
[8:16])[0]
2503 chunk
= packet
[19:540]
2504 chunk
= chunk
[:chunk
.find('\x00\x00')]
2505 self
.filename
= unicode((codecs
.BOM_UTF16_BE
+ chunk
).decode("utf-16"))
2506 if MSNP2PDEBUG
: print "FileContext parsed:", self
.filesize
, self
.filename
2510 """ Utility class for the binary header & footer in p2p messages """
2519 def __init__(self
, fields
=None, packet
=None):
2521 self
.fields
= fields
2523 self
.fields
= [0] * 10
2525 self
.unpackFields(packet
)
2527 def __getitem__(self
, key
):
2528 return self
.fields
[key
]
2530 def __setitem__(self
, key
, value
):
2531 self
.fields
[key
] = value
2533 def unpackFields(self
, packet
):
2534 self
.fields
= struct
.unpack("<LLQQLLLLQ", packet
[0:48])
2535 self
.fields
+= struct
.unpack(">L", packet
[len(packet
)-4:])
2537 print "Unpacked fields:",
2538 for i
in self
.fields
:
2542 def packHeaders(self
):
2543 f
= tuple(self
.fields
)
2545 print "Packed fields:",
2546 for i
in self
.fields
:
2549 return struct
.pack("<LLQQLLLLQ", f
[0], f
[1], f
[2], f
[3], f
[4], f
[5], f
[6], f
[7], f
[8])
2551 def packFooter(self
):
2552 return struct
.pack(">L", self
.fields
[9])
2555 class MSNSLPMessage
:
2556 """ Representation of a single MSNSLP message """
2557 def __init__(self
, packet
=None):
2564 self
.sessionGuid
= ""
2565 self
.sessionID
= None
2567 self
.data
= "\r\n" + chr(0)
2571 def create(self
, method
=None, status
=None, to
=None, fro
=None, branch
=None, cseq
=0, sessionGuid
=None, data
=None):
2572 self
.method
= method
2573 self
.status
= status
2576 self
.branch
= branch
2578 self
.sessionGuid
= sessionGuid
2579 if data
: self
.data
= data
2581 def setData(self
, ctype
, data
):
2584 order
= ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
2586 if key
== "Context" and data
.has_key(key
):
2587 s
.append("Context: %s\r\n" % b64enc(data
[key
]))
2588 elif data
.has_key(key
):
2589 s
.append("%s: %s\r\n" % (key
, str(data
[key
])))
2590 s
.append("\r\n"+chr(0))
2592 self
.data
= "".join(s
)
2596 if s
.find("MSNSLP/1.0") < 0: return
2598 lines
= s
.split("\r\n")
2600 # Get the MSNSLP method or status
2601 msnslp
= lines
[0].split(" ")
2602 if MSNP2PDEBUG
: print "Parsing MSNSLPMessage", len(s
), s
2603 if msnslp
[0] in ("INVITE", "BYE"):
2604 self
.method
= msnslp
[0].strip()
2606 self
.status
= msnslp
[1].strip()
2608 lines
.remove(lines
[0])
2611 line
= line
.split(":")
2612 if len(line
) < 1: continue
2614 if len(line
) > 2 and line
[0] == "To":
2615 self
.to
= line
[2][:line
[2].find('>')]
2616 elif len(line
) > 2 and line
[0] == "From":
2617 self
.fro
= line
[2][:line
[2].find('>')]
2618 elif line
[0] == "Call-ID":
2619 self
.sessionGuid
= line
[1].strip()
2620 elif line
[0] == "CSeq":
2621 self
.cseq
= int(line
[1].strip())
2622 elif line
[0] == "SessionID":
2623 self
.sessionID
= int(line
[1].strip())
2624 elif line
[0] == "EUF-GUID":
2625 self
.euf_guid
= line
[1].strip()
2626 elif line
[0] == "Content-Type":
2627 self
.ctype
= line
[1].strip()
2628 elif line
[0] == "Context":
2629 self
.context
= b64dec(line
[1])
2630 elif line
[0] == "Via":
2631 self
.branch
= line
[1].split(";")[1].split("=")[1].strip()
2634 print "Error parsing MSNSLP message."
2640 s
.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self
.method
, self
.to
))
2642 if self
.status
== "200": status
= "200 OK"
2643 elif self
.status
== "603": status
= "603 Decline"
2644 s
.append("MSNSLP/1.0 %s\r\n" % status
)
2645 s
.append("To: <msnmsgr:%s>\r\n" % self
.to
)
2646 s
.append("From: <msnmsgr:%s>\r\n" % self
.fro
)
2647 s
.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % self
.branch
)
2648 s
.append("CSeq: %s \r\n" % str(self
.cseq
))
2649 s
.append("Call-ID: %s\r\n" % self
.sessionGuid
)
2650 s
.append("Max-Forwards: 0\r\n")
2651 s
.append("Content-Type: %s\r\n" % self
.ctype
)
2652 s
.append("Content-Length: %s\r\n\r\n" % len(self
.data
))
2657 """ Utility for handling the weird sequence IDs in p2p messages """
2658 def __init__(self
, baseID
=None):
2660 self
.baseID
= baseID
2662 self
.baseID
= random
.randint(1000, sys
.maxint
)
2666 return P2PSEQ
[self
.pos
] + self
.baseID
2673 class StringBuffer(StringIO
.StringIO
):
2674 def __init__(self
, notifyFunc
=None):
2675 self
.notifyFunc
= notifyFunc
2676 StringIO
.StringIO
.__init
__(self
)
2680 self
.notifyFunc(self
.getvalue())
2681 self
.notifyFunc
= None
2682 StringIO
.StringIO
.close(self
)
2686 def __init__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
):
2689 sessionID
= random
.randint(1000, sys
.maxint
)
2691 sessionGuid
= random_guid()
2692 self
.remoteUser
= remoteUser
2693 self
.switchboard
= switchboard
2694 self
.sessionID
= sessionID
2695 self
.sessionGuid
= sessionGuid
2696 self
.seqID
= SeqID()
2700 if not self
.switchboard
: return
2701 del self
.switchboard
.slpLinks
[self
.sessionID
]
2702 self
.switchboard
= None
2703 # This is so that handleP2PMessage can still use the SLPLink
2704 # one last time, for ACKing BYEs and 601s.
2705 reactor
.callLater(0, kill
)
2707 def warn(self
, text
):
2709 print "Warning in transfer: ", self
, text
2711 def sendP2PACK(self
, ackHeaders
):
2712 binaryFields
= BinaryFields()
2713 binaryFields
[0] = ackHeaders
[0]
2714 binaryFields
[1] = self
.seqID
.next()
2715 binaryFields
[3] = ackHeaders
[3]
2716 binaryFields
[5] = BinaryFields
.ACK
2717 binaryFields
[6] = ackHeaders
[1]
2718 binaryFields
[7] = ackHeaders
[6]
2719 binaryFields
[8] = ackHeaders
[3]
2720 self
.sendP2PMessage(binaryFields
, "")
2722 def sendSLPMessage(self
, cmd
, ctype
, data
, branch
=None):
2723 msg
= MSNSLPMessage()
2725 msg
.create(status
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=branch
, cseq
=1, sessionGuid
=self
.sessionGuid
)
2727 msg
.create(method
=cmd
, to
=self
.remoteUser
, fro
=self
.switchboard
.userHandle
, branch
=random_guid(), cseq
=0, sessionGuid
=self
.sessionGuid
)
2728 msg
.setData(ctype
, data
)
2730 binaryFields
= BinaryFields()
2731 binaryFields
[1] = self
.seqID
.next()
2732 binaryFields
[3] = len(msgStr
)
2733 binaryFields
[4] = binaryFields
[3]
2734 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2735 self
.sendP2PMessage(binaryFields
, msgStr
)
2737 def sendP2PMessage(self
, binaryFields
, msgStr
):
2738 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2740 message
= MSNMessage(message
=packet
)
2741 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2742 message
.setHeader("P2P-Dest", self
.remoteUser
)
2743 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2744 self
.switchboard
.sendMessage(message
)
2746 def handleSLPMessage(self
, slpMessage
):
2747 raise NotImplementedError
2753 class SLPLink_Send(SLPLink
):
2754 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2755 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2756 self
.handlePacket
= None
2758 self
.filesize
= filesize
2761 def send_dataprep(self
):
2762 if MSNP2PDEBUG
: print "send_dataprep"
2763 binaryFields
= BinaryFields()
2764 binaryFields
[0] = self
.sessionID
2765 binaryFields
[1] = self
.seqID
.next()
2768 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2770 self
.sendP2PMessage(binaryFields
, chr(0) * 4)
2772 def write(self
, data
):
2773 if MSNP2PDEBUG
: print "write"
2777 if i
+ 1202 < length
:
2778 self
._writeChunk
(data
[i
:i
+1202])
2781 self
.data
+= data
[i
:]
2782 if len(self
.data
) >= 1202:
2788 def _writeChunk(self
, chunk
):
2789 print "writing chunk"
2790 binaryFields
= BinaryFields()
2791 binaryFields
[0] = self
.sessionID
2792 if self
.offset
== 0:
2793 binaryFields
[1] = self
.seqID
.next()
2795 binaryFields
[1] = self
.seqID
.get()
2796 binaryFields
[2] = self
.offset
2797 binaryFields
[3] = self
.filesize
2798 binaryFields
[4] = len(chunk
)
2799 binaryFields
[5] = self
.dataFlag
2800 binaryFields
[6] = random
.randint(1000, sys
.maxint
)
2802 self
.offset
+= len(chunk
)
2803 self
.sendP2PMessage(binaryFields
, chunk
)
2807 self
._writeChunk
(self
.data
)
2812 # FIXME, should send 601 or something
2814 class SLPLink_FileSend(SLPLink_Send
):
2815 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
):
2816 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
)
2817 self
.dataFlag
= BinaryFields
.DATAFT
2818 # Send invite & wait for 200OK before sending dataprep
2819 context
= FileContext()
2820 context
.filename
= filename
2821 context
.filesize
= filesize
2822 data
= {"EUF-GUID" : MSN_MSNFTP_GUID
,\
2823 "SessionID": self
.sessionID
,\
2825 "Context" : context
.pack() }
2826 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2827 self
.acceptDeferred
= Deferred()
2829 def handleSLPMessage(self
, slpMessage
):
2830 if slpMessage
.status
== "200":
2831 if slpMessage
.ctype
== "application/x-msnmsgr-sessionreqbody":
2832 data
= {"Bridges" : "TRUDPv1 TCPv1",\
2834 "Conn-Type" : "Firewall",\
2835 "UPnPNat" : "false",\
2837 #"Hashed-Nonce": random_guid()}
2838 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data
)
2839 elif slpMessage
.ctype
== "application/x-msnmsgr-transrespbody":
2840 self
.acceptDeferred
.callback((True,))
2841 self
.handlePacket
= self
.wait_data_ack
2843 if slpMessage
.status
== "603":
2844 self
.acceptDeferred
.callback((False,))
2845 # SLPLink is over due to decline, error or BYE
2848 def wait_data_ack(self
, packet
):
2849 if MSNP2PDEBUG
: print "wait_data_ack"
2850 binaryFields
= BinaryFields()
2851 binaryFields
.unpackFields(packet
)
2853 if binaryFields
[5] != BinaryFields
.ACK
:
2854 self
.warn("field5," + str(binaryFields
[5]))
2857 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2858 self
.handlePacket
= None
2861 self
.handlePacket
= self
.wait_data_ack
2862 SLPLink_Send
.close(self
)
2865 class SLPLink_AvatarSend(SLPLink_Send
):
2866 def __init__(self
, remoteUser
, switchboard
, filesize
, sessionID
=None, sessionGuid
=None):
2867 SLPLink_Send
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, filesize
=filesize
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2868 self
.dataFlag
= BinaryFields
.DATA
2869 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
})
2870 self
.send_dataprep()
2871 self
.handlePacket
= lambda packet
: None
2873 def handleSLPMessage(self
, slpMessage
):
2874 self
.killLink() # BYE or error
2877 SLPLink_Send
.close(self
)
2878 # Keep the link open to wait for a BYE
2880 class SLPLink_Receive(SLPLink
):
2881 def __init__(self
, remoteUser
, switchboard
, consumer
, context
=None, sessionID
=None, sessionGuid
=None):
2882 SLPLink
.__init
__(self
, remoteUser
, switchboard
, sessionID
, sessionGuid
)
2883 self
.handlePacket
= None
2884 self
.consumer
= consumer
2887 def wait_dataprep(self
, packet
):
2888 if MSNP2PDEBUG
: print "wait_dataprep"
2889 binaryFields
= BinaryFields()
2890 binaryFields
.unpackFields(packet
)
2892 if binaryFields
[3] != 4:
2893 self
.warn("field3," + str(binaryFields
[3]))
2895 if binaryFields
[4] != 4:
2896 self
.warn("field4," + str(binaryFields
[4]))
2898 if binaryFields
[9] != 1:
2899 self
.warn("field9," + str(binaryFields
[9]))
2902 self
.sendP2PACK(binaryFields
)
2903 self
.handlePacket
= self
.wait_data
2905 def wait_data(self
, packet
):
2906 if MSNP2PDEBUG
: print "wait_data"
2907 binaryFields
= BinaryFields()
2908 binaryFields
.unpackFields(packet
)
2910 if binaryFields
[5] != self
.dataFlag
:
2911 self
.warn("field5," + str(binaryFields
[5]))
2913 if binaryFields
[9] != 1:
2914 self
.warn("field9," + str(binaryFields
[9]))
2916 offset
= binaryFields
[2]
2917 total
= binaryFields
[3]
2918 length
= binaryFields
[4]
2920 data
= packet
[48:-4]
2921 if offset
!= self
.pos
:
2922 self
.warn("Received packet out of order")
2923 self
.consumer
.error()
2925 if len(data
) != length
:
2926 self
.warn("Received bad length of slp")
2927 self
.consumer
.error()
2932 self
.consumer
.write(data
)
2934 if self
.pos
== total
:
2935 self
.sendP2PACK(binaryFields
)
2936 self
.consumer
.close()
2937 self
.handlePacket
= None
2940 def doFinished(self
):
2941 raise NotImplementedError
2944 class SLPLink_FileReceive(SLPLink_Receive
, FileReceive
):
2945 def __init__(self
, remoteUser
, switchboard
, filename
, filesize
, sessionID
, sessionGuid
, branch
):
2946 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=self
, sessionID
=sessionID
, sessionGuid
=sessionGuid
)
2947 self
.dataFlag
= BinaryFields
.DATAFT
2948 self
.initialBranch
= branch
2949 FileReceive
.__init
__(self
, filename
, filesize
, remoteUser
)
2952 # Send a 603 decline
2953 self
.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2956 def accept(self
, consumer
):
2957 FileReceive
.accept(self
, consumer
)
2958 self
.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self
.sessionID
}, branch
=self
.initialBranch
)
2960 def handleSLPMessage(self
, slpMessage
):
2961 if slpMessage
.method
== "INVITE": # The second invite
2962 data
= {"Bridge" : "TCPv1",\
2963 "Listening" : "false",\
2964 "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
2965 self
.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data
, branch
=slpMessage
.branch
)
2966 self
.handlePacket
= self
.wait_data
2968 self
.killLink() # It's either a BYE or an error
2969 # FIXME, do some error handling if it was an error
2971 def doFinished(self
):
2972 #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2974 # Wait for BYE? #FIXME
2977 class SLPLink_AvatarReceive(SLPLink_Receive
):
2978 def __init__(self
, remoteUser
, switchboard
, consumer
, context
):
2979 SLPLink_Receive
.__init
__(self
, remoteUser
=remoteUser
, switchboard
=switchboard
, consumer
=consumer
, context
=context
)
2980 self
.dataFlag
= BinaryFields
.DATA
2981 data
= {"EUF-GUID" : MSN_AVATAR_GUID
,\
2982 "SessionID": self
.sessionID
,\
2984 "Context" : context
}
2985 self
.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data
)
2987 def handleSLPMessage(self
, slpMessage
):
2988 if slpMessage
.status
== "200":
2989 self
.handlePacket
= self
.wait_dataprep
2991 # SLPLink is over due to error or BYE
2994 def doFinished(self
):
2995 self
.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
2997 # mapping of error codes to error messages
3000 200 : "Syntax error",
3001 201 : "Invalid parameter",
3002 205 : "Invalid user",
3003 206 : "Domain name missing",
3004 207 : "Already logged in",
3005 208 : "Invalid username",
3006 209 : "Invalid screen name",
3007 210 : "User list full",
3008 215 : "User already there",
3009 216 : "User already on list",
3010 217 : "User not online",
3011 218 : "Already in mode",
3012 219 : "User is in the opposite list",
3013 223 : "Too many groups",
3014 224 : "Invalid group",
3015 225 : "User not in group",
3016 229 : "Group name too long",
3017 230 : "Cannot remove group 0",
3018 231 : "Invalid group",
3019 280 : "Switchboard failed",
3020 281 : "Transfer to switchboard failed",
3022 300 : "Required field missing",
3023 301 : "Too many FND responses",
3024 302 : "Not logged in",
3026 402 : "Error accessing contact list",
3027 403 : "Error accessing contact list",
3029 500 : "Internal server error",
3030 501 : "Database server error",
3031 502 : "Command disabled",
3032 510 : "File operation failed",
3033 520 : "Memory allocation failed",
3034 540 : "Wrong CHL value sent to server",
3036 600 : "Server is busy",
3037 601 : "Server is unavaliable",
3038 602 : "Peer nameserver is down",
3039 603 : "Database connection failed",
3040 604 : "Server is going down",
3041 605 : "Server unavailable",
3043 707 : "Could not create connection",
3044 710 : "Invalid CVR parameters",
3045 711 : "Write is blocking",
3046 712 : "Session is overloaded",
3047 713 : "Too many active users",
3048 714 : "Too many sessions",
3049 715 : "Not expected",
3050 717 : "Bad friend file",
3051 731 : "Not expected",
3053 800 : "Requests too rapid",
3055 910 : "Server too busy",
3056 911 : "Authentication failed",
3057 912 : "Server too busy",
3058 913 : "Not allowed when offline",
3059 914 : "Server too busy",
3060 915 : "Server too busy",
3061 916 : "Server too busy",
3062 917 : "Server too busy",
3063 918 : "Server too busy",
3064 919 : "Server too busy",
3065 920 : "Not accepting new users",
3066 921 : "Server too busy",
3067 922 : "Server too busy",
3068 923 : "No parent consent",
3069 924 : "Passport account not yet verified"
3073 # mapping of status codes to readable status format
3076 STATUS_ONLINE
: "Online",
3077 STATUS_OFFLINE
: "Offline",
3078 STATUS_HIDDEN
: "Appear Offline",
3079 STATUS_IDLE
: "Idle",
3080 STATUS_AWAY
: "Away",
3081 STATUS_BUSY
: "Busy",
3082 STATUS_BRB
: "Be Right Back",
3083 STATUS_PHONE
: "On the Phone",
3084 STATUS_LUNCH
: "Out to Lunch"
3088 # mapping of list ids to list codes
3091 FORWARD_LIST
: 'fl',
3094 REVERSE_LIST
: 'rl',
3099 # mapping of list codes to list ids
3101 for id,code
in listIDToCode
.items():
3102 listCodeToID
[code
] = id