]>
code.delx.au - pymsnt/blob - src/tlib/msn.py
9dc1e5040f7e55a13de6eec1552cf683adfd5073
1 # Twisted, the Framework of Your Internet
2 # Copyright (C) 2001-2002 Matthew W. Lefkowitz
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of version 2.1 of the GNU Lesser General Public
6 # License as published by the Free Software Foundation.
8 # This library is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 # Lesser General Public License for more details.
13 # You should have received a copy of the GNU Lesser General Public
14 # License along with this library; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 MSNP8 Protocol (client only) - semi-experimental
23 This module provides support for clients using the MSN Protocol (MSNP8).
24 There are basically 3 servers involved in any MSN session:
28 The DispatchClient class handles connections to the
29 dispatch server, which basically delegates users to a
30 suitable notification server.
32 You will want to subclass this and handle the gotNotificationReferral
35 I{Notification Server}
37 The NotificationClient class handles connections to the
38 notification server, which acts as a session server
39 (state updates, message negotiation etc...)
43 The SwitchboardClient handles connections to switchboard
44 servers which are used to conduct conversations with other users.
46 There are also two classes (FileSend and FileReceive) used
49 Clients handle events in two ways.
51 - each client request requiring a response will return a Deferred,
52 the callback for same will be fired when the server sends the
54 - Events which are not in response to any client request have
55 respective methods which should be overridden and handled in
58 Most client request callbacks require more than one argument,
59 and since Deferreds can only pass the callback one result,
60 most of the time the callback argument will be a tuple of
61 values (documented in the respective request method).
62 To make reading/writing code easier, callbacks can be defined in
63 a number of ways to handle this 'cleanly'. One way would be to
64 define methods like: def callBack(self, (arg1, arg2, arg)): ...
65 another way would be to do something like:
66 d.addCallback(lambda result: myCallback(*result)).
68 If the server sends an error response to a client request,
69 the errback of the corresponding Deferred will be called,
70 the argument being the corresponding error code.
73 Due to the lack of an official spec for MSNP8, extra checking
74 than may be deemed necessary often takes place considering the
75 server is never 'wrong'. Thus, if gotBadLine (in any of the 3
76 main clients) is called, or an MSNProtocolError is raised, it's
77 probably a good idea to submit a bug report. ;)
78 Use of this module requires that PyOpenSSL is installed.
82 - check message hooks with invalid x-msgsinvite messages.
86 @author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
89 from __future__
import nested_scopes
92 from twisted
.protocols
.basic
import LineReceiver
95 if(utils
.checkTwisted()):
96 from twisted
.web
.http
import HTTPClient
98 from twisted
.protocols
.http
import HTTPClient
99 from proxy
import proxy_connect_ssl
102 from twisted
.internet
import reactor
, task
103 from twisted
.internet
.defer
import Deferred
104 from twisted
.internet
.protocol
import ClientFactory
105 from twisted
.internet
.ssl
import ClientContextFactory
106 from twisted
.python
import failure
, log
109 import types
, operator
, os
, md5
110 from random
import randint
111 from urllib
import quote
, unquote
113 MSN_PROTOCOL_VERSION
= "MSNP8 CVR0" # protocol version
114 MSN_PORT
= 1863 # default dispatch server port
115 MSN_MAX_MESSAGE
= 1664 # max message length
116 MSN_CHALLENGE_STR
= "Q1P7W2E4J9R8U3S5" # used for server challenges
117 MSN_CVR_STR
= "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(
137 STATUS_ONLINE
= 'NLN'
138 STATUS_OFFLINE
= 'FLN'
139 STATUS_HIDDEN
= 'HDN'
152 def checkParamLen(num
, expected
, cmd
, error
=None):
153 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
154 if num
!= expected
: raise MSNProtocolError
, error
156 def _parseHeader(h
, v
):
158 Split a certin number of known
159 header values with the format:
160 field1=val,field2=val,field3=val into
161 a dict mapping fields to values.
162 @param h: the header's key
163 @param v: the header's value as a string
166 if h
in ('passporturls','authentication-info','www-authenticate'):
167 v
= v
.replace('Passport1.4','').lstrip()
169 for fieldPair
in v
.split(','):
171 field
,value
= fieldPair
.split('=',1)
172 fields
[field
.lower()] = value
174 fields
[field
.lower()] = ''
178 def _parsePrimitiveHost(host
):
180 h
,p
= host
.replace('https://','').split('/',1)
184 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
='', proxy
=None, proxyport
=None):
186 This function is used internally and should not ever be called
190 def _cb(server
, auth
):
191 loginFac
= ClientFactory()
192 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
193 if(proxy
and proxyport
):
194 proxy_connect_ssl(proxy
, proxyport
, _parsePrimitiveHost(server
)[0], 443, loginFac
)
196 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
199 _cb(nexusServer
, authData
)
201 fac
= ClientFactory()
203 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
204 d
.addErrback(lambda f
: cb
.errback(f
))
205 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
206 if(proxy
and proxyport
):
207 proxy_connect_ssl(proxy
, proxyport
, _parsePrimitiveHost(nexusServer
)[0], 443, fac
)
209 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
213 class PassportNexus(HTTPClient
):
216 Used to obtain the URL of a valid passport
219 This class is used internally and should
220 not be instantiated directly -- that is,
221 The passport logging in process is handled
222 transparantly by NotificationClient.
225 def __init__(self
, deferred
, host
):
226 self
.deferred
= deferred
227 self
.host
, self
.path
= _parsePrimitiveHost(host
)
229 def connectionMade(self
):
230 HTTPClient
.connectionMade(self
)
231 self
.sendCommand('GET', self
.path
)
232 self
.sendHeader('Host', self
.host
)
236 def handleHeader(self
, header
, value
):
238 self
.headers
[h
] = _parseHeader(h
, value
)
240 def handleEndHeaders(self
):
241 if self
.connected
: self
.transport
.loseConnection()
242 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
243 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
245 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
247 def handleResponse(self
, r
): pass
249 class PassportLogin(HTTPClient
):
251 This class is used internally to obtain
252 a login ticket from a passport HTTPS
253 server -- it should not be used directly.
258 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
259 self
.deferred
= deferred
260 self
.userHandle
= userHandle
262 self
.authData
= authData
263 self
.host
, self
.path
= _parsePrimitiveHost(host
)
265 def connectionMade(self
):
266 self
.sendCommand('GET', self
.path
)
267 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
268 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), self
.passwd
,self
.authData
))
269 self
.sendHeader('Host', self
.host
)
273 def handleHeader(self
, header
, value
):
275 self
.headers
[h
] = _parseHeader(h
, value
)
277 def handleEndHeaders(self
):
278 if self
._finished
: return
279 self
._finished
= 1 # I think we need this because of HTTPClient
280 if self
.connected
: self
.transport
.loseConnection()
281 authHeader
= 'authentication-info'
282 _interHeader
= 'www-authenticate'
283 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
285 info
= self
.headers
[authHeader
]
286 status
= info
['da-status']
287 handler
= getattr(self
, 'login_%s' % (status
,), None)
290 else: raise Exception()
292 self
.deferred
.errback(failure
.Failure(e
))
294 def handleResponse(self
, r
): pass
296 def login_success(self
, info
):
297 ticket
= info
['from-pp']
298 ticket
= ticket
[1:len(ticket
)-1]
299 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
301 def login_failed(self
, info
):
302 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
304 def login_redir(self
, info
):
305 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
307 class MSNProtocolError(Exception):
309 This Exception is basically used for debugging
310 purposes, as the official MSN server should never
311 send anything _wrong_ and nobody in their right
312 mind would run their B{own} MSN server.
313 If it is raised by default command handlers
314 (handle_BLAH) the error will be logged.
321 I am the class used to represent an 'instant' message.
323 @ivar userHandle: The user handle (passport) of the sender
324 (this is only used when receiving a message)
325 @ivar screenName: The screen name of the sender (this is only used
326 when receiving a message)
327 @ivar message: The message
328 @ivar headers: The message headers
330 @ivar length: The message length (including headers and line endings)
331 @ivar ack: This variable is used to tell the server how to respond
332 once the message has been sent. If set to MESSAGE_ACK
333 (default) the server will respond with an ACK upon receiving
334 the message, if set to MESSAGE_NACK the server will respond
335 with a NACK upon failure to receive the message.
336 If set to MESSAGE_ACK_NONE the server will do nothing.
337 This is relevant for the return value of
338 SwitchboardClient.sendMessage (which will return
339 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
340 and will fire when the respective ACK or NACK is received).
341 If set to MESSAGE_ACK_NONE sendMessage will return None.
345 MESSAGE_ACK_NONE
= 'U'
349 def __init__(self
, length
=0, userHandle
="", screenName
="", message
=""):
350 self
.userHandle
= userHandle
351 self
.screenName
= screenName
352 self
.message
= message
353 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
357 def _calcMessageLen(self
):
359 used to calculte the number to send
360 as the message length when sending a message.
362 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
364 def setHeader(self
, header
, value
):
365 """ set the desired header """
366 self
.headers
[header
] = value
368 def getHeader(self
, header
):
370 get the desired header value
371 @raise KeyError: if no such header exists.
373 return self
.headers
[header
]
375 def hasHeader(self
, header
):
376 """ check to see if the desired header exists """
377 return self
.headers
.has_key(header
)
379 def getMessage(self
):
380 """ return the message - not including headers """
383 def setMessage(self
, message
):
384 """ set the message text """
385 self
.message
= message
390 This class represents a contact (user).
392 @ivar userHandle: The contact's user handle (passport).
393 @ivar screenName: The contact's screen name.
394 @ivar groups: A list of all the group IDs which this
396 @ivar lists: An integer representing the sum of all lists
397 that this contact belongs to.
398 @ivar status: The contact's status code.
399 @type status: str if contact's status is known, None otherwise.
401 @ivar homePhone: The contact's home phone number.
402 @type homePhone: str if known, otherwise None.
403 @ivar workPhone: The contact's work phone number.
404 @type workPhone: str if known, otherwise None.
405 @ivar mobilePhone: The contact's mobile phone number.
406 @type mobilePhone: str if known, otherwise None.
407 @ivar hasPager: Whether or not this user has a mobile pager
411 def __init__(self
, userHandle
="", screenName
="", lists
=0, groups
=[], status
=None):
412 self
.userHandle
= userHandle
413 self
.screenName
= screenName
415 self
.groups
= [] # if applicable
416 self
.status
= status
# current status
419 self
.homePhone
= None
420 self
.workPhone
= None
421 self
.mobilePhone
= None
424 def setPhone(self
, phoneType
, value
):
426 set phone numbers/values for this specific user.
427 for phoneType check the *_PHONE constants and HAS_PAGER
430 t
= phoneType
.upper()
431 if t
== HOME_PHONE
: self
.homePhone
= value
432 elif t
== WORK_PHONE
: self
.workPhone
= value
433 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
434 elif t
== HAS_PAGER
: self
.hasPager
= value
435 else: raise ValueError, "Invalid Phone Type"
437 def addToList(self
, listType
):
439 Update the lists attribute to
440 reflect being part of the
443 self
.lists |
= listType
445 def removeFromList(self
, listType
):
447 Update the lists attribute to
448 reflect being removed from the
451 self
.lists ^
= listType
453 class MSNContactList
:
455 This class represents a basic MSN contact list.
457 @ivar contacts: All contacts on my various lists
458 @type contacts: dict (mapping user handles to MSNContact objects)
459 @ivar version: The current contact list version (used for list syncing)
460 @ivar groups: a mapping of group ids to group names
461 (groups can only exist on the forward list)
465 This is used only for storage and doesn't effect the
466 server's contact list.
476 def _getContactsFromList(self
, listType
):
478 Obtain all contacts which belong
479 to the given list type.
481 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
483 def addContact(self
, contact
):
487 self
.contacts
[contact
.userHandle
] = contact
489 def remContact(self
, userHandle
):
494 del self
.contacts
[userHandle
]
495 except KeyError: pass
497 def getContact(self
, userHandle
):
499 Obtain the MSNContact object
500 associated with the given
502 @return: the MSNContact object if
503 the user exists, or None.
506 return self
.contacts
[userHandle
]
510 def getBlockedContacts(self
):
512 Obtain all the contacts on my block list
514 return self
._getContactsFromList
(BLOCK_LIST
)
516 def getAuthorizedContacts(self
):
518 Obtain all the contacts on my auth list.
519 (These are contacts which I have verified
520 can view my state changes).
522 return self
._getContactsFromList
(ALLOW_LIST
)
524 def getReverseContacts(self
):
526 Get all contacts on my reverse list.
527 (These are contacts which have added me
528 to their forward list).
530 return self
._getContactsFromList
(REVERSE_LIST
)
532 def getContacts(self
):
534 Get all contacts on my forward list.
535 (These are the contacts which I have added
538 return self
._getContactsFromList
(FORWARD_LIST
)
540 def setGroup(self
, id, name
):
542 Keep a mapping from the given id
545 self
.groups
[id] = name
547 def remGroup(self
, id):
549 Removed the stored group
550 mapping for the given id.
554 except KeyError: pass
555 for c
in self
.contacts
:
556 if id in c
.groups
: c
.groups
.remove(id)
559 class MSNEventBase(LineReceiver
):
561 This class provides support for handling / dispatching events and is the
562 base class of the three main client protocols (DispatchClient,
563 NotificationClient, SwitchboardClient)
567 self
.ids
= {} # mapping of ids to Deferreds
571 self
.currentMessage
= None
573 def connectionLost(self
, reason
):
577 def connectionMade(self
):
580 def _fireCallback(self
, id, *args
):
582 Fire the callback for the given id
583 if one exists and return 1, else return false
585 if self
.ids
.has_key(id):
586 self
.ids
[id][0].callback(args
)
591 def _nextTransactionID(self
):
592 """ return a usable transaction ID """
594 if self
.currentID
> 1000: self
.currentID
= 1
595 return self
.currentID
597 def _createIDMapping(self
, data
=None):
599 return a unique transaction ID that is mapped internally to a
600 deferred .. also store arbitrary data if it is needed
602 id = self
._nextTransactionID
()
604 self
.ids
[id] = (d
, data
)
607 def checkMessage(self
, message
):
609 process received messages to check for file invitations and
610 typing notifications and other control type messages
612 raise NotImplementedError
614 def sendLine(self
, line
):
615 if(LINEDEBUG
): print ">> " + line
616 LineReceiver
.sendLine(self
, line
)
618 def lineReceived(self
, line
):
619 if(LINEDEBUG
): print "<< " + line
620 if self
.currentMessage
:
621 self
.currentMessage
.readPos
+= len(line
+CR
+LF
)
623 header
, value
= line
.split(':')
624 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
627 #raise MSNProtocolError, "Invalid Message Header"
629 if line
== "" or self
.currentMessage
.userHandle
== "NOTIFICATION":
631 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
634 cmd
, params
= line
.split(' ', 1)
636 #raise MSNProtocolError, "Invalid Message, %s" % repr(line)
637 cmd
= line
.strip() # The QNG command has no parameters.
640 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
642 if self
.ids
.has_key(params
.split(' ')[0]):
643 self
.ids
[id].errback(int(cmd
))
646 else: # we received an error which doesn't map to a sent command
647 self
.gotError(int(cmd
))
650 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
652 try: handler(params
.split(' '))
653 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
655 self
.handle_UNKNOWN(cmd
, params
.split(' '))
657 def rawDataReceived(self
, data
):
659 self
.currentMessage
.readPos
+= len(data
)
660 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
662 self
.currentMessage
.message
+= data
[:-diff
]
665 self
.currentMessage
.message
+= data
667 self
.currentMessage
.message
+= data
669 del self
.currentMessage
.readPos
670 m
= self
.currentMessage
671 self
.currentMessage
= None
672 if not self
.checkMessage(m
):
673 self
.setLineMode(extra
)
675 self
.setLineMode(extra
)
678 ### protocol command handlers - no need to override these.
680 def handle_MSG(self
, params
):
681 checkParamLen(len(params
), 3, 'MSG')
683 messageLen
= int(params
[2])
684 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
685 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
687 def handle_UNKNOWN(self
, cmd
, params
):
688 """ implement me in subclasses if you want to handle unknown events """
689 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
693 def gotMessage(self
, message
):
695 called when we receive a message - override in notification
696 and switchboard clients
698 raise NotImplementedError
700 def gotBadLine(self
, line
, why
):
701 """ called when a handler notifies me that this line is broken """
702 log
.msg('Error in line: %s (%s)' % (line
, why
))
704 def gotError(self
, errorCode
):
706 called when the server sends an error which is not in
707 response to a sent command (ie. it has no matching transaction ID)
709 log
.msg('Error %s' % (errorCodes
[errorCode
]))
711 class DispatchClient(MSNEventBase
):
713 This class provides support for clients connecting to the dispatch server
714 @ivar userHandle: your user handle (passport) needed before connecting.
717 # eventually this may become an attribute of the
721 def connectionMade(self
):
722 MSNEventBase
.connectionMade(self
)
723 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
725 ### protocol command handlers ( there is no need to override these )
727 def handle_VER(self
, params
):
728 versions
= params
[1:]
729 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
730 self
.transport
.loseConnection()
731 raise MSNProtocolError
, "Invalid version response"
732 id = self
._nextTransactionID
()
733 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.userHandle
))
735 def handle_CVR(self
, params
):
736 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.userHandle
))
738 def handle_XFR(self
, params
):
739 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
740 id, refType
, addr
= params
[:3]
741 # was addr a host:port pair?
743 host
, port
= addr
.split(':')
748 self
.gotNotificationReferral(host
, int(port
))
752 def gotNotificationReferral(self
, host
, port
):
754 called when we get a referral to the notification server.
756 @param host: the notification server's hostname
757 @param port: the port to connect to
762 class NotificationClient(MSNEventBase
):
764 This class provides support for clients connecting
765 to the notification server.
768 factory
= None # sssh pychecker
770 def __init__(self
, currentID
=0, proxy
=None, proxyport
=None):
771 MSNEventBase
.__init
__(self
)
772 self
.currentID
= currentID
773 self
._state
= ['DISCONNECTED', {}]
774 self
.proxy
, self
.proxyport
= proxy
, proxyport
776 self
.pingCheckTask
= None
778 def _setState(self
, state
):
779 self
._state
[0] = state
782 return self
._state
[0]
784 def _getStateData(self
, key
):
785 return self
._state
[1][key
]
787 def _setStateData(self
, key
, value
):
788 self
._state
[1][key
] = value
790 def _remStateData(self
, *args
):
791 for key
in args
: del self
._state
[1][key
]
793 def connectionMade(self
):
794 MSNEventBase
.connectionMade(self
)
795 self
._setState
('CONNECTED')
796 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
798 def connectionLost(self
, reason
):
799 self
._setState
('DISCONNECTED')
801 if(self
.pingCheckTask
):
802 self
.pingCheckTask
.stop()
803 self
.pingCheckTask
= None
804 MSNEventBase
.connectionLost(self
, reason
)
806 def checkMessage(self
, message
):
807 """ hook used for detecting specific notification messages """
808 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
809 if 'text/x-msmsgsprofile' in cTypes
:
810 self
.gotProfile(message
)
814 ### protocol command handlers - no need to override these
816 def handle_VER(self
, params
):
817 versions
= params
[1:]
818 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
819 self
.transport
.loseConnection()
820 raise MSNProtocolError
, "Invalid version response"
821 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
823 def handle_CVR(self
, params
):
824 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
826 def handle_USR(self
, params
):
827 if len(params
) != 4 and len(params
) != 6:
828 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
830 mechanism
= params
[1]
831 if mechanism
== "OK":
832 self
.loggedIn(params
[2], unquote(params
[3]), int(params
[4]))
833 elif params
[2].upper() == "S":
834 # we need to obtain auth from a passport server
836 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3], proxy
=self
.proxy
, proxyport
=self
.proxyport
)
837 d
.addCallback(self
._passportLogin
)
838 d
.addErrback(self
._passportError
)
840 def _passportLogin(self
, result
):
841 if result
[0] == LOGIN_REDIRECT
:
842 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
843 result
[1], cached
=1, authData
=result
[2], proxy
=self
.proxy
, proxyport
=self
.proxyport
)
844 d
.addCallback(self
._passportLogin
)
845 d
.addErrback(self
._passportError
)
846 elif result
[0] == LOGIN_SUCCESS
:
847 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
848 elif result
[0] == LOGIN_FAILURE
:
849 self
.loginFailure(result
[1])
851 def _passportError(self
, failure
):
852 self
.loginFailure("Exception while authenticating: %s" % failure
)
854 def handle_CHG(self
, params
):
855 checkParamLen(len(params
), 3, 'CHG')
857 if not self
._fireCallback
(id, params
[1]):
858 self
.statusChanged(params
[1])
860 def handle_ILN(self
, params
):
861 checkParamLen(len(params
), 5, 'ILN')
862 self
.gotContactStatus(params
[1], params
[2], unquote(params
[3]))
864 def handle_CHL(self
, params
):
865 checkParamLen(len(params
), 2, 'CHL')
866 self
.sendLine("QRY %s msmsgs@msnmsgr.com 32" % self
._nextTransactionID
())
867 self
.transport
.write(md5
.md5(params
[1] + MSN_CHALLENGE_STR
).hexdigest())
869 def handle_QRY(self
, params
):
872 def handle_NLN(self
, params
):
873 checkParamLen(len(params
), 4, 'NLN')
874 self
.contactStatusChanged(params
[0], params
[1], unquote(params
[2]))
876 def handle_FLN(self
, params
):
877 checkParamLen(len(params
), 1, 'FLN')
878 self
.contactOffline(params
[0])
880 def handle_LST(self
, params
):
881 # support no longer exists for manually
882 # requesting lists - why do I feel cleaner now?
883 if self
._getState
() != 'SYNC': return
884 contact
= MSNContact(userHandle
=params
[0], screenName
=unquote(params
[1]),
885 lists
=int(params
[2]))
886 if contact
.lists
& FORWARD_LIST
:
887 contact
.groups
.extend(map(int, params
[3].split(',')))
888 self
._getStateData
('list').addContact(contact
)
889 self
._setStateData
('last_contact', contact
)
890 sofar
= self
._getStateData
('lst_sofar') + 1
891 if sofar
== self
._getStateData
('lst_reply'):
892 # this is the best place to determine that
893 # a syn realy has finished - msn _may_ send
894 # BPR information for the last contact
895 # which is unfortunate because it means
896 # that the real end of a syn is non-deterministic.
897 # to handle this we'll keep 'last_contact' hanging
898 # around in the state data and update it if we need
900 self
._setState
('SESSION')
901 contacts
= self
._getStateData
('list')
902 phone
= self
._getStateData
('phone')
903 id = self
._getStateData
('synid')
904 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
905 self
._fireCallback
(id, contacts
, phone
)
907 self
._setStateData
('lst_sofar',sofar
)
909 def handle_BLP(self
, params
):
910 # check to see if this is in response to a SYN
911 if self
._getState
() == 'SYNC':
912 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
915 self
._fireCallback
(id, int(params
[1]), listCodeToID
[params
[2].lower()])
917 def handle_GTC(self
, params
):
918 # check to see if this is in response to a SYN
919 if self
._getState
() == 'SYNC':
920 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
921 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
922 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
925 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
926 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
927 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
929 def handle_SYN(self
, params
):
932 self
._setState
('SESSION')
933 self
._fireCallback
(id, None, None)
935 contacts
= MSNContactList()
936 contacts
.version
= int(params
[1])
937 self
._setStateData
('list', contacts
)
938 self
._setStateData
('lst_reply', int(params
[2]))
939 self
._setStateData
('lsg_reply', int(params
[3]))
940 self
._setStateData
('lst_sofar', 0)
941 self
._setStateData
('phone', [])
943 def handle_LSG(self
, params
):
944 if self
._getState
() == 'SYNC':
945 self
._getStateData
('list').groups
[int(params
[0])] = unquote(params
[1])
947 # Please see the comment above the requestListGroups / requestList methods
948 # regarding support for this
951 # self._getStateData('groups').append((int(params[4]), unquote(params[5])))
952 # if params[3] == params[4]: # this was the last group
953 # self._fireCallback(int(params[0]), self._getStateData('groups'), int(params[1]))
954 # self._remStateData('groups')
956 def handle_PRP(self
, params
):
957 if self
._getState
() == 'SYNC':
958 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
960 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
962 def handle_BPR(self
, params
):
963 numParams
= len(params
)
964 if numParams
== 2: # part of a syn
965 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
967 self
.gotPhoneNumber(int(params
[0]), params
[1], params
[2], unquote(params
[3]))
969 def handle_ADG(self
, params
):
970 checkParamLen(len(params
), 5, 'ADG')
972 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
973 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
975 def handle_RMG(self
, params
):
976 checkParamLen(len(params
), 3, 'RMG')
978 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
979 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
981 def handle_REG(self
, params
):
982 checkParamLen(len(params
), 5, 'REG')
984 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
985 raise MSNProtocolError
, "REG response does not match up to a request" # debug
987 def handle_ADD(self
, params
):
988 numParams
= len(params
)
989 if numParams
< 5 or params
[1].upper() not in ('AL','BL','RL','FL'):
990 raise MSNProtocolError
, "Invalid Paramaters for ADD" # debug
992 listType
= params
[1].lower()
993 listVer
= int(params
[2])
994 userHandle
= params
[3]
996 if numParams
== 6: # they sent a group id
997 if params
[1].upper() != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
998 groupID
= int(params
[5])
999 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, listVer
, groupID
):
1000 self
.userAddedMe(userHandle
, unquote(params
[4]), listVer
)
1002 def handle_REM(self
, params
):
1003 numParams
= len(params
)
1004 if numParams
< 4 or params
[1].upper() not in ('AL','BL','FL','RL'):
1005 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1007 listType
= params
[1].lower()
1008 listVer
= int(params
[2])
1009 userHandle
= params
[3]
1012 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1013 groupID
= int(params
[4])
1014 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, listVer
, groupID
):
1015 if listType
.upper() == "RL": self
.userRemovedMe(userHandle
, listVer
)
1017 def handle_REA(self
, params
):
1018 checkParamLen(len(params
), 4, 'REA')
1020 self
._fireCallback
(id, int(params
[1]), unquote(params
[3]))
1022 def handle_XFR(self
, params
):
1023 checkParamLen(len(params
), 5, 'XFR')
1025 # check to see if they sent a host/port pair
1027 host
, port
= params
[2].split(':')
1032 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1033 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1035 def handle_RNG(self
, params
):
1036 checkParamLen(len(params
), 6, 'RNG')
1037 # check for host:port pair
1039 host
, port
= params
[1].split(":")
1044 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1047 def handle_NOT(self
, params
):
1048 checkParamLen(len(params
), 1, 'NOT')
1050 messageLen
= int(params
[0])
1051 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1052 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", screenName
="NOTIFICATION")
1056 def handle_OUT(self
, params
):
1057 checkParamLen(len(params
), 1, 'OUT')
1058 if params
[0] == "OTH": self
.multipleLogin()
1059 elif params
[0] == "SSD": self
.serverGoingDown()
1060 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1062 def handle_QNG(self
, params
):
1063 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1067 def pingChecker(self
):
1068 if(self
.pingCounter
> 5):
1069 # The server has ignored 5 pings, lets kill the connection
1070 self
.transport
.loseConnection()
1072 self
.sendLine("PNG")
1073 self
.pingCounter
+= 1
1075 def pingCheckerStart(self
, *args
):
1076 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1077 self
.pingCheckTask
.start(50.0)
1079 def loggedIn(self
, userHandle
, screenName
, verified
):
1081 Called when the client has logged in.
1082 The default behaviour of this method is to
1083 update the factory with our screenName and
1084 to sync the contact list (factory.contacts).
1085 When this is complete self.listSynchronized
1088 @param userHandle: our userHandle
1089 @param screenName: our screenName
1090 @param verified: 1 if our passport has been (verified), 0 if not.
1091 (i'm not sure of the significace of this)
1094 self
.factory
.screenName
= screenName
1095 listVersion
= self
.factory
.initialListVersion
1096 if self
.factory
.contacts
: listVersion
= self
.factory
.contacts
.version
1097 d
= self
.syncList(listVersion
)
1098 d
.addCallback(self
.listSynchronized
)
1099 d
.addCallback(self
.pingCheckerStart
)
1101 def loginFailure(self
, message
):
1103 Called when the client fails to login.
1105 @param message: a message indicating the problem that was encountered
1109 def gotProfile(self
, message
):
1111 Called after logging in when the server sends an initial
1112 message with MSN/passport specific profile information
1113 such as country, number of kids, etc.
1114 Check the message headers for the specific values.
1116 @param message: The profile message
1120 def listSynchronized(self
, *args
):
1122 Lists are now synchronized by default upon logging in, this
1123 method is called after the synchronization has finished
1124 and the factory now has the up-to-date contacts.
1128 def statusChanged(self
, statusCode
):
1130 Called when our status changes and it isn't in response to
1131 a client command. By default we will update the status
1132 attribute of the factory.
1134 @param statusCode: 3-letter status code
1136 self
.factory
.status
= statusCode
1138 def gotContactStatus(self
, statusCode
, userHandle
, screenName
):
1140 Called after loggin in when the server sends status of online contacts.
1141 By default we will update the status attribute and screenName of the
1142 contact stored on the factory.
1144 @param statusCode: 3-letter status code
1145 @param userHandle: the contact's user handle (passport)
1146 @param screenName: the contact's screen name
1148 msnContact
= self
.factory
.contacts
.getContact(userHandle
)
1150 msnContact
= MSNContact()
1151 msnContact
.addToList(FORWARD_LIST
)
1152 self
.factory
.contacts
.addContact(msnContact
)
1153 msnContact
.status
= statusCode
1154 msnContact
.screenName
= screenName
1156 def contactStatusChanged(self
, statusCode
, userHandle
, screenName
):
1158 Called when we're notified that a contact's status has changed.
1159 By default we will update the status attribute and screenName
1160 of the contact stored on the factory.
1162 @param statusCode: 3-letter status code
1163 @param userHandle: the contact's user handle (passport)
1164 @param screenName: the contact's screen name
1166 msnContact
= self
.factory
.contacts
.getContact(userHandle
)
1168 msnContact
= MSNContact()
1169 self
.factory
.contacts
.addContact(msnContact
)
1170 msnContact
.status
= statusCode
1171 msnContact
.screenName
= screenName
1173 def contactOffline(self
, userHandle
):
1175 Called when a contact goes offline. By default this method
1176 will update the status attribute of the contact stored
1179 @param userHandle: the contact's user handle
1181 msnContact
= self
.factory
.contacts
.getContact(userHandle
)
1183 msnContact
.status
= STATUS_OFFLINE
1185 def gotPhoneNumber(self
, listVersion
, userHandle
, phoneType
, number
):
1187 Called when the server sends us phone details about
1188 a specific user (for example after a user is added
1189 the server will send their status, phone details etc.
1190 By default we will update the list version for the
1191 factory's contact list and update the phone details
1192 for the specific user.
1194 @param listVersion: the new list version
1195 @param userHandle: the contact's user handle (passport)
1196 @param phoneType: the specific phoneType
1197 (*_PHONE constants or HAS_PAGER)
1198 @param number: the value/phone number.
1200 if not self
.factory
.contacts
: return
1201 self
.factory
.contacts
.version
= listVersion
1202 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1204 def userAddedMe(self
, userHandle
, screenName
, listVersion
):
1206 Called when a user adds me to their list. (ie. they have been added to
1207 the reverse list. By default this method will update the version of
1208 the factory's contact list -- that is, if the contact already exists
1209 it will update the associated lists attribute, otherwise it will create
1210 a new MSNContact object and store it.
1212 @param userHandle: the userHandle of the user
1213 @param screenName: the screen name of the user
1214 @param listVersion: the new list version
1215 @type listVersion: int
1217 if not self
.factory
.contacts
: return
1218 self
.factory
.contacts
.version
= listVersion
1219 c
= self
.factory
.contacts
.getContact(userHandle
)
1221 c
= MSNContact(userHandle
=userHandle
, screenName
=screenName
)
1222 self
.factory
.contacts
.addContact(c
)
1223 c
.addToList(REVERSE_LIST
)
1225 def userRemovedMe(self
, userHandle
, listVersion
):
1227 Called when a user removes us from their contact list
1228 (they are no longer on our reverseContacts list.
1229 By default this method will update the version of
1230 the factory's contact list -- that is, the user will
1231 be removed from the reverse list and if they are no longer
1232 part of any lists they will be removed from the contact
1235 @param userHandle: the contact's user handle (passport)
1236 @param listVersion: the new list version
1238 if not self
.factory
.contacts
: return
1239 self
.factory
.contacts
.version
= listVersion
1240 c
= self
.factory
.contacts
.getContact(userHandle
)
1242 c
.removeFromList(REVERSE_LIST
)
1243 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1245 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1246 key
, userHandle
, screenName
):
1248 Called when we get an invitation to a switchboard server.
1249 This happens when a user requests a chat session with us.
1251 @param sessionID: session ID number, must be remembered for logging in
1252 @param host: the hostname of the switchboard server
1253 @param port: the port to connect to
1254 @param key: used for authorization when connecting
1255 @param userHandle: the user handle of the person who invited us
1256 @param screenName: the screen name of the person who invited us
1260 def multipleLogin(self
):
1262 Called when the server says there has been another login
1263 under our account, the server should disconnect us right away.
1267 def serverGoingDown(self
):
1269 Called when the server has notified us that it is going down for
1276 def changeStatus(self
, status
):
1278 Change my current status. This method will add
1279 a default callback to the returned Deferred
1280 which will update the status attribute of the
1283 @param status: 3-letter status code (as defined by
1284 the STATUS_* constants)
1285 @return: A Deferred, the callback of which will be
1286 fired when the server confirms the change
1287 of status. The callback argument will be
1288 a tuple with the new status code as the
1292 id, d
= self
._createIDMapping
()
1293 self
.sendLine("CHG %s %s" % (id, status
))
1295 if self
.factory
: self
.factory
.status
= r
[0]
1297 return d
.addCallback(_cb
)
1299 # I am no longer supporting the process of manually requesting
1300 # lists or list groups -- as far as I can see this has no use
1301 # if lists are synchronized and updated correctly, which they
1302 # should be. If someone has a specific justified need for this
1303 # then please contact me and i'll re-enable/fix support for it.
1305 #def requestList(self, listType):
1307 # request the desired list type
1309 # @param listType: (as defined by the *_LIST constants)
1310 # @return: A Deferred, the callback of which will be
1311 # fired when the list has been retrieved.
1312 # The callback argument will be a tuple with
1313 # the only element being a list of MSNContact
1316 # # this doesn't need to ever be used if syncing of the lists takes place
1317 # # i.e. please don't use it!
1318 # warnings.warn("Please do not use this method - use the list syncing process instead")
1319 # id, d = self._createIDMapping()
1320 # self.sendLine("LST %s %s" % (id, listIDToCode[listType].upper()))
1321 # self._setStateData('list',[])
1324 def setPrivacyMode(self
, privLevel
):
1326 Set my privacy mode on the server.
1329 This only keeps the current privacy setting on
1330 the server for later retrieval, it does not
1331 effect the way the server works at all.
1333 @param privLevel: This parameter can be true, in which
1334 case the server will keep the state as
1335 'al' which the official client interprets
1336 as -> allow messages from only users on
1337 the allow list. Alternatively it can be
1338 false, in which case the server will keep
1339 the state as 'bl' which the official client
1340 interprets as -> allow messages from all
1341 users except those on the block list.
1343 @return: A Deferred, the callback of which will be fired when
1344 the server replies with the new privacy setting.
1345 The callback argument will be a tuple, the 2 elements
1346 of which being the list version and either 'al'
1347 or 'bl' (the new privacy setting).
1350 id, d
= self
._createIDMapping
()
1351 if privLevel
: self
.sendLine("BLP %s AL" % id)
1352 else: self
.sendLine("BLP %s BL" % id)
1355 def syncList(self
, version
):
1357 Used for keeping an up-to-date contact list.
1358 A callback is added to the returned Deferred
1359 that updates the contact list on the factory
1360 and also sets my state to STATUS_ONLINE.
1363 This is called automatically upon signing
1364 in using the version attribute of
1365 factory.contacts, so you may want to persist
1366 this object accordingly. Because of this there
1367 is no real need to ever call this method
1370 @param version: The current known list version
1372 @return: A Deferred, the callback of which will be
1373 fired when the server sends an adequate reply.
1374 The callback argument will be a tuple with two
1375 elements, the new list (MSNContactList) and
1376 your current state (a dictionary). If the version
1377 you sent _was_ the latest list version, both elements
1378 will be None. To just request the list send a version of 0.
1381 self
._setState
('SYNC')
1382 id, d
= self
._createIDMapping
(data
=str(version
))
1383 self
._setStateData
('synid',id)
1384 self
.sendLine("SYN %s %s" % (id, version
))
1386 self
.changeStatus(STATUS_ONLINE
)
1387 if r
[0] is not None:
1388 self
.factory
.contacts
= r
[0]
1390 return d
.addCallback(_cb
)
1393 # I am no longer supporting the process of manually requesting
1394 # lists or list groups -- as far as I can see this has no use
1395 # if lists are synchronized and updated correctly, which they
1396 # should be. If someone has a specific justified need for this
1397 # then please contact me and i'll re-enable/fix support for it.
1399 #def requestListGroups(self):
1401 # Request (forward) list groups.
1403 # @return: A Deferred, the callback for which will be called
1404 # when the server responds with the list groups.
1405 # The callback argument will be a tuple with two elements,
1406 # a dictionary mapping group IDs to group names and the
1407 # current list version.
1410 # # this doesn't need to be used if syncing of the lists takes place (which it SHOULD!)
1411 # # i.e. please don't use it!
1412 # warnings.warn("Please do not use this method - use the list syncing process instead")
1413 # id, d = self._createIDMapping()
1414 # self.sendLine("LSG %s" % id)
1415 # self._setStateData('groups',{})
1418 def setPhoneDetails(self
, phoneType
, value
):
1420 Set/change my phone numbers stored on the server.
1422 @param phoneType: phoneType can be one of the following
1423 constants - HOME_PHONE, WORK_PHONE,
1424 MOBILE_PHONE, HAS_PAGER.
1425 These are pretty self-explanatory, except
1426 maybe HAS_PAGER which refers to whether or
1427 not you have a pager.
1428 @param value: for all of the *_PHONE constants the value is a
1429 phone number (str), for HAS_PAGER accepted values
1430 are 'Y' (for yes) and 'N' (for no).
1432 @return: A Deferred, the callback for which will be fired when
1433 the server confirms the change has been made. The
1434 callback argument will be a tuple with 2 elements, the
1435 first being the new list version (int) and the second
1436 being the new phone number value (str).
1438 # XXX: Add a default callback which updates
1439 # factory.contacts.version and the relevant phone
1441 id, d
= self
._createIDMapping
()
1442 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1445 def addListGroup(self
, name
):
1447 Used to create a new list group.
1448 A default callback is added to the
1449 returned Deferred which updates the
1450 contacts attribute of the factory.
1452 @param name: The desired name of the new group.
1454 @return: A Deferred, the callbacck for which will be called
1455 when the server clarifies that the new group has been
1456 created. The callback argument will be a tuple with 3
1457 elements: the new list version (int), the new group name
1458 (str) and the new group ID (int).
1461 id, d
= self
._createIDMapping
()
1462 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1464 self
.factory
.contacts
.version
= r
[0]
1465 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1467 return d
.addCallback(_cb
)
1469 def remListGroup(self
, groupID
):
1471 Used to remove a list group.
1472 A default callback is added to the
1473 returned Deferred which updates the
1474 contacts attribute of the factory.
1476 @param groupID: the ID of the desired group to be removed.
1478 @return: A Deferred, the callback for which will be called when
1479 the server clarifies the deletion of the group.
1480 The callback argument will be a tuple with 2 elements:
1481 the new list version (int) and the group ID (int) of
1485 id, d
= self
._createIDMapping
()
1486 self
.sendLine("RMG %s %s" % (id, groupID
))
1488 self
.factory
.contacts
.version
= r
[0]
1489 self
.factory
.contacts
.remGroup(r
[1])
1491 return d
.addCallback(_cb
)
1493 def renameListGroup(self
, groupID
, newName
):
1495 Used to rename an existing list group.
1496 A default callback is added to the returned
1497 Deferred which updates the contacts attribute
1500 @param groupID: the ID of the desired group to rename.
1501 @param newName: the desired new name for the group.
1503 @return: A Deferred, the callback for which will be called
1504 when the server clarifies the renaming.
1505 The callback argument will be a tuple of 3 elements,
1506 the new list version (int), the group id (int) and
1507 the new group name (str).
1510 id, d
= self
._createIDMapping
()
1511 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1513 self
.factory
.contacts
.version
= r
[0]
1514 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1516 return d
.addCallback(_cb
)
1518 def addContact(self
, listType
, userHandle
, groupID
=0):
1520 Used to add a contact to the desired list.
1521 A default callback is added to the returned
1522 Deferred which updates the contacts attribute of
1523 the factory with the new contact information.
1524 If you are adding a contact to the forward list
1525 and you want to associate this contact with multiple
1526 groups then you will need to call this method for each
1527 group you would like to add them to, changing the groupID
1528 parameter. The default callback will take care of updating
1529 the group information on the factory's contact list.
1531 @param listType: (as defined by the *_LIST constants)
1532 @param userHandle: the user handle (passport) of the contact
1534 @param groupID: the group ID for which to associate this contact
1535 with. (default 0 - default group). Groups are only
1536 valid for FORWARD_LIST.
1538 @return: A Deferred, the callback for which will be called when
1539 the server has clarified that the user has been added.
1540 The callback argument will be a tuple with 4 elements:
1541 the list type, the contact's user handle, the new list
1542 version, and the group id (if relevant, otherwise it
1546 id, d
= self
._createIDMapping
()
1547 listType
= listIDToCode
[listType
].upper()
1548 if listType
== "FL":
1549 self
.sendLine("ADD %s FL %s %s %s" % (id, userHandle
, userHandle
, groupID
))
1551 self
.sendLine("ADD %s %s %s %s" % (id, listType
, userHandle
, userHandle
))
1554 self
.factory
.contacts
.version
= r
[2]
1555 c
= self
.factory
.contacts
.getContact(r
[1])
1557 c
= MSNContact(userHandle
=r
[1])
1558 if r
[3]: c
.groups
.append(r
[3])
1561 return d
.addCallback(_cb
)
1563 def remContact(self
, listType
, userHandle
, groupID
=0):
1565 Used to remove a contact from the desired list.
1566 A default callback is added to the returned deferred
1567 which updates the contacts attribute of the factory
1568 to reflect the new contact information. If you are
1569 removing from the forward list then you will need to
1570 supply a groupID, if the contact is in more than one
1571 group then they will only be removed from this group
1572 and not the entire forward list, but if this is their
1573 only group they will be removed from the whole list.
1575 @param listType: (as defined by the *_LIST constants)
1576 @param userHandle: the user handle (passport) of the
1577 contact being removed
1578 @param groupID: the ID of the group to which this contact
1579 belongs (only relevant for FORWARD_LIST,
1582 @return: A Deferred, the callback for which will be called when
1583 the server has clarified that the user has been removed.
1584 The callback argument will be a tuple of 4 elements:
1585 the list type, the contact's user handle, the new list
1586 version, and the group id (if relevant, otherwise it will
1590 id, d
= self
._createIDMapping
()
1591 listType
= listIDToCode
[listType
].upper()
1592 if listType
== "FL":
1593 self
.sendLine("REM %s FL %s %s" % (id, userHandle
, groupID
))
1595 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1598 l
= self
.factory
.contacts
1600 c
= l
.getContact(r
[1])
1604 if group
: # they may not have been removed from the list
1605 c
.groups
.remove(group
)
1606 if c
.groups
: shouldRemove
= 0
1608 c
.removeFromList(r
[0])
1609 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1611 return d
.addCallback(_cb
)
1613 def changeScreenName(self
, newName
):
1615 Used to change your current screen name.
1616 A default callback is added to the returned
1617 Deferred which updates the screenName attribute
1618 of the factory and also updates the contact list
1621 @param newName: the new screen name
1623 @return: A Deferred, the callback for which will be called
1624 when the server sends an adequate reply.
1625 The callback argument will be a tuple of 2 elements:
1626 the new list version and the new screen name.
1629 id, d
= self
._createIDMapping
()
1630 self
.sendLine("REA %s %s %s" % (id, self
.factory
.userHandle
, quote(newName
)))
1632 if(self
.factory
.contacts
): self
.factory
.contacts
.version
= r
[0]
1633 self
.factory
.screenName
= r
[1]
1635 return d
.addCallback(_cb
)
1637 def requestSwitchboardServer(self
):
1639 Used to request a switchboard server to use for conversations.
1641 @return: A Deferred, the callback for which will be called when
1642 the server responds with the switchboard information.
1643 The callback argument will be a tuple with 3 elements:
1644 the host of the switchboard server, the port and a key
1645 used for logging in.
1648 id, d
= self
._createIDMapping
()
1649 self
.sendLine("XFR %s SB" % id)
1654 Used to log out of the notification server.
1655 After running the method the server is expected
1656 to close the connection.
1659 if(self
.pingCheckTask
):
1660 self
.pingCheckTask
.stop()
1661 self
.pingCheckTask
= None
1662 self
.sendLine("OUT")
1664 class NotificationFactory(ClientFactory
):
1666 Factory for the NotificationClient protocol.
1667 This is basically responsible for keeping
1668 the state of the client and thus should be used
1669 in a 1:1 situation with clients.
1671 @ivar contacts: An MSNContactList instance reflecting
1672 the current contact list -- this is
1673 generally kept up to date by the default
1675 @ivar userHandle: The client's userHandle, this is expected
1676 to be set by the client and is used by the
1677 protocol (for logging in etc).
1678 @ivar screenName: The client's current screen-name -- this is
1679 generally kept up to date by the default
1681 @ivar password: The client's password -- this is (obviously)
1682 expected to be set by the client.
1683 @ivar passportServer: This must point to an msn passport server
1684 (the whole URL is required)
1685 @ivar status: The status of the client -- this is generally kept
1686 up to date by the default command handlers
1693 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
1695 protocol
= NotificationClient
1696 initialListVersion
= 0
1699 # XXX: A lot of the state currently kept in
1700 # instances of SwitchboardClient is likely to
1701 # be moved into a factory at some stage in the
1704 class SwitchboardClient(MSNEventBase
):
1706 This class provides support for clients connecting to a switchboard server.
1708 Switchboard servers are used for conversations with other people
1709 on the MSN network. This means that the number of conversations at
1710 any given time will be directly proportional to the number of
1711 connections to varioius switchboard servers.
1713 MSN makes no distinction between single and group conversations,
1714 so any number of users may be invited to join a specific conversation
1715 taking place on a switchboard server.
1717 @ivar key: authorization key, obtained when receiving
1718 invitation / requesting switchboard server.
1719 @ivar userHandle: your user handle (passport)
1720 @ivar sessionID: unique session ID, used if you are replying
1721 to a switchboard invitation
1722 @ivar reply: set this to 1 in connectionMade or before to signifiy
1723 that you are replying to a switchboard invitation.
1734 MSNEventBase
.__init
__(self
)
1735 self
.pendingUsers
= {}
1736 self
.cookies
= {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
1738 def connectionMade(self
):
1739 MSNEventBase
.connectionMade(self
)
1742 def connectionLost(self
, reason
):
1743 self
.cookies
['iCookies'] = {}
1744 self
.cookies
['external'] = {}
1745 MSNEventBase
.connectionLost(self
, reason
)
1747 def _sendInit(self
):
1749 send initial data based on whether we are replying to an invitation
1752 id = self
._nextTransactionID
()
1754 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
1756 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
1758 def _newInvitationCookie(self
):
1760 if self
._iCookie
> 1000: self
._iCookie
= 1
1761 return self
._iCookie
1763 def _checkTyping(self
, message
, cTypes
):
1764 """ helper method for checkMessage """
1765 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
1766 self
.userTyping(message
)
1769 def _checkFileInvitation(self
, message
, info
):
1770 """ helper method for checkMessage """
1771 if not info
.get('Application-Name', '').lower() == 'file transfer': return 0
1773 cookie
= info
['Invitation-Cookie']
1774 fileName
= info
['Application-File']
1775 fileSize
= int(info
['Application-FileSize'])
1777 log
.msg('Received munged file transfer request ... ignoring.')
1779 self
.gotSendRequest(fileName
, fileSize
, cookie
, message
)
1782 def _checkFileResponse(self
, message
, info
):
1783 """ helper method for checkMessage """
1785 cmd
= info
['Invitation-Command'].upper()
1786 cookie
= info
['Invitation-Cookie']
1787 except KeyError: return 0
1788 accept
= (cmd
== 'ACCEPT') and 1 or 0
1789 requested
= self
.cookies
['iCookies'].get(cookie
)
1790 if not requested
: return 1
1791 requested
[0].callback((accept
, cookie
, info
))
1792 del self
.cookies
['iCookies'][cookie
]
1795 def _checkFileInfo(self
, message
, info
):
1796 """ helper method for checkMessage """
1798 ip
= info
['IP-Address']
1799 iCookie
= info
['Invitation-Cookie']
1800 aCookie
= info
['AuthCookie']
1801 cmd
= info
['Invitation-Command'].upper()
1802 port
= int(info
['Port'])
1803 except KeyError: return 0
1804 accept
= (cmd
== 'ACCEPT') and 1 or 0
1805 requested
= self
.cookies
['external'].get(iCookie
)
1806 if not requested
: return 1 # we didn't ask for this
1807 requested
[0].callback((accept
, ip
, port
, aCookie
, info
))
1808 del self
.cookies
['external'][iCookie
]
1811 def checkMessage(self
, message
):
1813 hook for detecting any notification type messages
1814 (e.g. file transfer)
1816 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
1817 if self
._checkTyping
(message
, cTypes
): return 0
1818 if 'text/x-msmsgsinvite' in cTypes
:
1819 # header like info is sent as part of the message body.
1821 for line
in message
.message
.split('\r\n'):
1823 key
, val
= line
.split(':')
1824 info
[key
] = val
.lstrip()
1825 except ValueError: continue
1826 if self
._checkFileInvitation
(message
, info
) or self
._checkFileInfo
(message
, info
) or self
._checkFileResponse
(message
, info
): return 0
1830 def handle_USR(self
, params
):
1831 checkParamLen(len(params
), 4, 'USR')
1832 if params
[1] == "OK":
1836 def handle_CAL(self
, params
):
1837 checkParamLen(len(params
), 3, 'CAL')
1839 if params
[1].upper() == "RINGING":
1840 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
1843 def handle_JOI(self
, params
):
1844 checkParamLen(len(params
), 2, 'JOI')
1845 self
.userJoined(params
[0], unquote(params
[1]))
1847 # users participating in the current chat
1848 def handle_IRO(self
, params
):
1849 checkParamLen(len(params
), 5, 'IRO')
1850 self
.pendingUsers
[params
[3]] = unquote(params
[4])
1851 if params
[1] == params
[2]:
1852 self
.gotChattingUsers(self
.pendingUsers
)
1853 self
.pendingUsers
= {}
1855 # finished listing users
1856 def handle_ANS(self
, params
):
1857 checkParamLen(len(params
), 2, 'ANS')
1858 if params
[1] == "OK":
1861 def handle_ACK(self
, params
):
1862 checkParamLen(len(params
), 1, 'ACK')
1863 self
._fireCallback
(int(params
[0]), None)
1865 def handle_NAK(self
, params
):
1866 checkParamLen(len(params
), 1, 'NAK')
1867 self
._fireCallback
(int(params
[0]), None)
1869 def handle_BYE(self
, params
):
1870 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
1871 self
.userLeft(params
[0])
1877 called when all login details have been negotiated.
1878 Messages can now be sent, or new users invited.
1882 def gotChattingUsers(self
, users
):
1884 called after connecting to an existing chat session.
1886 @param users: A dict mapping user handles to screen names
1887 (current users taking part in the conversation)
1891 def userJoined(self
, userHandle
, screenName
):
1893 called when a user has joined the conversation.
1895 @param userHandle: the user handle (passport) of the user
1896 @param screenName: the screen name of the user
1900 def userLeft(self
, userHandle
):
1902 called when a user has left the conversation.
1904 @param userHandle: the user handle (passport) of the user.
1908 def gotMessage(self
, message
):
1910 called when we receive a message.
1912 @param message: the associated MSNMessage object
1916 def userTyping(self
, message
):
1918 called when we receive the special type of message notifying
1919 us that a user is typing a message.
1921 @param message: the associated MSNMessage object
1925 def gotSendRequest(self
, fileName
, fileSize
, iCookie
, message
):
1927 called when a contact is trying to send us a file.
1928 To accept or reject this transfer see the
1929 fileInvitationReply method.
1931 @param fileName: the name of the file
1932 @param fileSize: the size of the file
1933 @param iCookie: the invitation cookie, used so the client can
1934 match up your reply with this request.
1935 @param message: the MSNMessage object which brought about this
1936 invitation (it may contain more information)
1942 def inviteUser(self
, userHandle
):
1944 used to invite a user to the current switchboard server.
1946 @param userHandle: the user handle (passport) of the desired user.
1948 @return: A Deferred, the callback for which will be called
1949 when the server notifies us that the user has indeed
1950 been invited. The callback argument will be a tuple
1951 with 1 element, the sessionID given to the invited user.
1952 I'm not sure if this is useful or not.
1955 id, d
= self
._createIDMapping
()
1956 self
.sendLine("CAL %s %s" % (id, userHandle
))
1959 def sendMessage(self
, message
):
1961 used to send a message.
1963 @param message: the corresponding MSNMessage object.
1965 @return: Depending on the value of message.ack.
1966 If set to MSNMessage.MESSAGE_ACK or
1967 MSNMessage.MESSAGE_NACK a Deferred will be returned,
1968 the callback for which will be fired when an ACK or
1969 NACK is received - the callback argument will be
1970 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
1971 the return value is None.
1974 if message
.ack
not in ('A','N'): id, d
= self
._nextTransactionID
(), None
1975 else: id, d
= self
._createIDMapping
()
1976 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
1977 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
1978 # apparently order matters with at least MIME-Version and Content-Type
1979 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
1980 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
1981 # send the rest of the headers
1982 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
1983 self
.sendLine("%s: %s" % (header
[0], header
[1]))
1984 self
.transport
.write(CR
+LF
)
1985 self
.transport
.write(message
.message
)
1988 def sendTypingNotification(self
):
1990 used to send a typing notification. Upon receiving this
1991 message the official client will display a 'user is typing'
1992 message to all other users in the chat session for 10 seconds.
1993 The official client sends one of these every 5 seconds (I think)
1994 as long as you continue to type.
1997 m
.ack
= m
.MESSAGE_ACK_NONE
1998 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
1999 m
.setHeader('TypingUser', self
.userHandle
)
2003 def sendFileInvitation(self
, fileName
, fileSize
):
2005 send an notification that we want to send a file.
2007 @param fileName: the file name
2008 @param fileSize: the file size
2010 @return: A Deferred, the callback of which will be fired
2011 when the user responds to this invitation with an
2012 appropriate message. The callback argument will be
2013 a tuple with 3 elements, the first being 1 or 0
2014 depending on whether they accepted the transfer
2015 (1=yes, 0=no), the second being an invitation cookie
2016 to identify your follow-up responses and the third being
2017 the message 'info' which is a dict of information they
2018 sent in their reply (this doesn't really need to be used).
2019 If you wish to proceed with the transfer see the
2020 sendTransferInfo method.
2022 cookie
= self
._newInvitationCookie
()
2025 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2026 m
.message
+= 'Application-Name: File Transfer\r\n'
2027 m
.message
+= 'Application-GUID: {5D3E02AB-6190-11d3-BBBB-00C04F795683}\r\n'
2028 m
.message
+= 'Invitation-Command: INVITE\r\n'
2029 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2030 m
.message
+= 'Application-File: %s\r\n' % fileName
2031 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2032 m
.ack
= m
.MESSAGE_ACK_NONE
2034 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2037 def fileInvitationReply(self
, iCookie
, accept
=1):
2039 used to reply to a file transfer invitation.
2041 @param iCookie: the invitation cookie of the initial invitation
2042 @param accept: whether or not you accept this transfer,
2043 1 = yes, 0 = no, default = 1.
2045 @return: A Deferred, the callback for which will be fired when
2046 the user responds with the transfer information.
2047 The callback argument will be a tuple with 5 elements,
2048 whether or not they wish to proceed with the transfer
2049 (1=yes, 0=no), their ip, the port, the authentication
2050 cookie (see FileReceive/FileSend) and the message
2051 info (dict) (in case they send extra header-like info
2052 like Internal-IP, this doesn't necessarily need to be
2053 used). If you wish to proceed with the transfer see
2058 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2059 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2060 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(iCookie
)
2061 if not accept
: m
.message
+= 'Cancel-Code: REJECT\r\n'
2062 m
.message
+= 'Launch-Application: FALSE\r\n'
2063 m
.message
+= 'Request-Data: IP-Address:\r\n'
2065 m
.ack
= m
.MESSAGE_ACK_NONE
2067 self
.cookies
['external'][iCookie
] = (d
, m
)
2070 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2072 send information relating to a file transfer session.
2074 @param accept: whether or not to go ahead with the transfer
2076 @param iCookie: the invitation cookie of previous replies
2077 relating to this transfer
2078 @param authCookie: the authentication cookie obtained from
2079 an FileSend instance
2081 @param port: the port on which an FileSend protocol is listening.
2084 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2085 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2086 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2087 m
.message
+= 'IP-Address: %s\r\n' % ip
2088 m
.message
+= 'Port: %s\r\n' % port
2089 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2091 m
.ack
= m
.MESSAGE_NACK
2094 class FileReceive(LineReceiver
):
2096 This class provides support for receiving files from contacts.
2098 @ivar fileSize: the size of the receiving file. (you will have to set this)
2099 @ivar connected: true if a connection has been established.
2100 @ivar completed: true if the transfer is complete.
2101 @ivar bytesReceived: number of bytes (of the file) received.
2102 This does not include header data.
2105 def __init__(self
, auth
, myUserHandle
, file, directory
="", overwrite
=0):
2107 @param auth: auth string received in the file invitation.
2108 @param myUserHandle: your userhandle.
2109 @param file: A string or file object represnting the file
2111 @param directory: optional parameter specifiying the directory.
2112 Defaults to the current directory.
2113 @param overwrite: if true and a file of the same name exists on
2114 your system, it will be overwritten. (0 by default)
2117 self
.myUserHandle
= myUserHandle
2121 self
.directory
= directory
2122 self
.bytesReceived
= 0
2123 self
.overwrite
= overwrite
2125 # used for handling current received state
2126 self
.state
= 'CONNECTING'
2127 self
.segmentLength
= 0
2130 if isinstance(file, types
.StringType
):
2131 path
= os
.path
.join(directory
, file)
2132 if os
.path
.exists(path
) and not self
.overwrite
:
2133 log
.msg('File already exists...')
2134 raise IOError, "File Exists" # is this all we should do here?
2135 self
.file = open(os
.path
.join(directory
, file), 'wb')
2139 def connectionMade(self
):
2141 self
.state
= 'INHEADER'
2142 self
.sendLine('VER MSNFTP')
2144 def connectionLost(self
, reason
):
2148 def parseHeader(self
, header
):
2149 """ parse the header of each 'message' to obtain the segment length """
2151 if ord(header
[0]) != 0: # they requested that we close the connection
2152 self
.transport
.loseConnection()
2155 extra
, factor
= header
[1:]
2157 # munged header, ending transfer
2158 self
.transport
.loseConnection()
2161 factor
= ord(factor
)
2162 return factor
* 256 + extra
2164 def lineReceived(self
, line
):
2166 if len(temp
) == 1: params
= []
2167 else: params
= temp
[1:]
2169 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
2170 if handler
: handler(params
) # try/except
2171 else: self
.handle_UNKNOWN(cmd
, params
)
2173 def rawDataReceived(self
, data
):
2174 bufferLen
= len(self
.buffer)
2175 if self
.state
== 'INHEADER':
2177 self
.buffer += data
[:delim
]
2178 if len(self
.buffer) == 3:
2179 self
.segmentLength
= self
.parseHeader(self
.buffer)
2180 if not self
.segmentLength
: return # hrm
2182 self
.state
= 'INSEGMENT'
2183 extra
= data
[delim
:]
2184 if len(extra
) > 0: self
.rawDataReceived(extra
)
2187 elif self
.state
== 'INSEGMENT':
2188 dataSeg
= data
[:(self
.segmentLength
-bufferLen
)]
2189 self
.buffer += dataSeg
2190 self
.bytesReceived
+= len(dataSeg
)
2191 if len(self
.buffer) == self
.segmentLength
:
2192 self
.gotSegment(self
.buffer)
2194 if self
.bytesReceived
== self
.fileSize
:
2198 self
.sendLine("BYE 16777989")
2200 self
.state
= 'INHEADER'
2201 extra
= data
[(self
.segmentLength
-bufferLen
):]
2202 if len(extra
) > 0: self
.rawDataReceived(extra
)
2205 def handle_VER(self
, params
):
2206 checkParamLen(len(params
), 1, 'VER')
2207 if params
[0].upper() == "MSNFTP":
2208 self
.sendLine("USR %s %s" % (self
.myUserHandle
, self
.auth
))
2210 log
.msg('they sent the wrong version, time to quit this transfer')
2211 self
.transport
.loseConnection()
2213 def handle_FIL(self
, params
):
2214 checkParamLen(len(params
), 1, 'FIL')
2216 self
.fileSize
= int(params
[0])
2217 except ValueError: # they sent the wrong file size - probably want to log this
2218 self
.transport
.loseConnection()
2221 self
.sendLine("TFR")
2223 def handle_UNKNOWN(self
, cmd
, params
):
2224 log
.msg('received unknown command (%s), params: %s' % (cmd
, params
))
2226 def gotSegment(self
, data
):
2227 """ called when a segment (block) of data arrives. """
2228 self
.file.write(data
)
2230 class FileSend(LineReceiver
):
2232 This class provides support for sending files to other contacts.
2234 @ivar bytesSent: the number of bytes that have currently been sent.
2235 @ivar completed: true if the send has completed.
2236 @ivar connected: true if a connection has been established.
2237 @ivar targetUser: the target user (contact).
2238 @ivar segmentSize: the segment (block) size.
2239 @ivar auth: the auth cookie (number) to use when sending the
2243 def __init__(self
, file):
2245 @param file: A string or file object represnting the file to send.
2248 if isinstance(file, types
.StringType
):
2249 self
.file = open(file, 'rb')
2257 self
.targetUser
= None
2258 self
.segmentSize
= 2045
2259 self
.auth
= randint(0, 2**30)
2260 self
._pendingSend
= None # :(
2262 def connectionMade(self
):
2265 def connectionLost(self
, reason
):
2266 if self
._pendingSend
:
2267 self
._pendingSend
.cancel()
2268 self
._pendingSend
= None
2272 def lineReceived(self
, line
):
2274 if len(temp
) == 1: params
= []
2275 else: params
= temp
[1:]
2277 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
2278 if handler
: handler(params
)
2279 else: self
.handle_UNKNOWN(cmd
, params
)
2281 def handle_VER(self
, params
):
2282 checkParamLen(len(params
), 1, 'VER')
2283 if params
[0].upper() == "MSNFTP":
2284 self
.sendLine("VER MSNFTP")
2285 else: # they sent some weird version during negotiation, i'm quitting.
2286 self
.transport
.loseConnection()
2288 def handle_USR(self
, params
):
2289 checkParamLen(len(params
), 2, 'USR')
2290 self
.targetUser
= params
[0]
2291 if self
.auth
== int(params
[1]):
2292 self
.sendLine("FIL %s" % (self
.fileSize
))
2293 else: # they failed the auth test, disconnecting.
2294 self
.transport
.loseConnection()
2296 def handle_TFR(self
, params
):
2297 checkParamLen(len(params
), 0, 'TFR')
2298 # they are ready for me to start sending
2301 def handle_BYE(self
, params
):
2302 self
.completed
= (self
.bytesSent
== self
.fileSize
)
2303 self
.transport
.loseConnection()
2305 def handle_CCL(self
, params
):
2306 self
.completed
= (self
.bytesSent
== self
.fileSize
)
2307 self
.transport
.loseConnection()
2309 def handle_UNKNOWN(self
, cmd
, params
): log
.msg('received unknown command (%s), params: %s' % (cmd
, params
))
2311 def makeHeader(self
, size
):
2312 """ make the appropriate header given a specific segment size. """
2313 quotient
, remainder
= divmod(size
, 256)
2314 return chr(0) + chr(remainder
) + chr(quotient
)
2317 """ send a segment of data """
2318 if not self
.connected
:
2319 self
._pendingSend
= None
2320 return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
2321 data
= self
.file.read(self
.segmentSize
)
2323 dataSize
= len(data
)
2324 header
= self
.makeHeader(dataSize
)
2325 self
.transport
.write(header
+ data
)
2326 self
.bytesSent
+= dataSize
2327 self
._pendingSend
= reactor
.callLater(0, self
.sendPart
)
2329 self
._pendingSend
= None
2332 # mapping of error codes to error messages
2335 200 : "Syntax error",
2336 201 : "Invalid parameter",
2337 205 : "Invalid user",
2338 206 : "Domain name missing",
2339 207 : "Already logged in",
2340 208 : "Invalid username",
2341 209 : "Invalid screen name",
2342 210 : "User list full",
2343 215 : "User already there",
2344 216 : "User already on list",
2345 217 : "User not online",
2346 218 : "Already in mode",
2347 219 : "User is in the opposite list",
2348 223 : "Too many groups",
2349 224 : "Invalid group",
2350 225 : "User not in group",
2351 229 : "Group name too long",
2352 230 : "Cannot remove group 0",
2353 231 : "Invalid group",
2354 280 : "Switchboard failed",
2355 281 : "Transfer to switchboard failed",
2357 300 : "Required field missing",
2358 301 : "Too many FND responses",
2359 302 : "Not logged in",
2361 500 : "Internal server error",
2362 501 : "Database server error",
2363 502 : "Command disabled",
2364 510 : "File operation failed",
2365 520 : "Memory allocation failed",
2366 540 : "Wrong CHL value sent to server",
2368 600 : "Server is busy",
2369 601 : "Server is unavaliable",
2370 602 : "Peer nameserver is down",
2371 603 : "Database connection failed",
2372 604 : "Server is going down",
2373 605 : "Server unavailable",
2375 707 : "Could not create connection",
2376 710 : "Invalid CVR parameters",
2377 711 : "Write is blocking",
2378 712 : "Session is overloaded",
2379 713 : "Too many active users",
2380 714 : "Too many sessions",
2381 715 : "Not expected",
2382 717 : "Bad friend file",
2383 731 : "Not expected",
2385 800 : "Requests too rapid",
2387 910 : "Server too busy",
2388 911 : "Authentication failed",
2389 912 : "Server too busy",
2390 913 : "Not allowed when offline",
2391 914 : "Server too busy",
2392 915 : "Server too busy",
2393 916 : "Server too busy",
2394 917 : "Server too busy",
2395 918 : "Server too busy",
2396 919 : "Server too busy",
2397 920 : "Not accepting new users",
2398 921 : "Server too busy",
2399 922 : "Server too busy",
2400 923 : "No parent consent",
2401 924 : "Passport account not yet verified"
2405 # mapping of status codes to readable status format
2408 STATUS_ONLINE
: "Online",
2409 STATUS_OFFLINE
: "Offline",
2410 STATUS_HIDDEN
: "Appear Offline",
2411 STATUS_IDLE
: "Idle",
2412 STATUS_AWAY
: "Away",
2413 STATUS_BUSY
: "Busy",
2414 STATUS_BRB
: "Be Right Back",
2415 STATUS_PHONE
: "On the Phone",
2416 STATUS_LUNCH
: "Out to Lunch"
2420 # mapping of list ids to list codes
2423 FORWARD_LIST
: 'fl',
2430 # mapping of list codes to list ids
2432 for id,code
in listIDToCode
.items():
2433 listCodeToID
[code
] = id