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"
98 from msnp2p
import random_guid
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
107 from twisted
.xish
.domish
import unescapeFromXml
110 from tlib
import xmlw
113 import types
, operator
, os
, sys
, base64
, random
114 from urllib
import quote
, unquote
116 MSN_PROTOCOL_VERSION
= "MSNP11 CVR0" # protocol version
117 MSN_PORT
= 1863 # default dispatch server port
118 MSN_MAX_MESSAGE
= 1664 # max message length
119 MSN_CVR_STR
= "0x040c winnt 5.1 i386 MSNMSGR 7.0.0777 msmsgs"
120 MSN_AVATAR_GUID
= "{A4268EEC-FEC5-49E5-95C3-F126696BDBF6}"
121 MSN_MSNFTP_GUID
= "{5D3E02AB-6190-11D3-BBBB-00C04F795683}"
143 STATUS_ONLINE
= 'NLN'
144 STATUS_OFFLINE
= 'FLN'
145 STATUS_HIDDEN
= 'HDN'
161 return inp
.split('=')[1]
173 userHandle
= getVal(p
)
175 screenName
= unquote(getVal(p
))
180 else: # Must be the groups
182 groups
= p
.split(',')
184 raise MSNProtocolError
, "Unknown LST/ADC response" + str(params
) # debug
186 return userHandle
, screenName
, userGuid
, lists
, groups
188 def checkParamLen(num
, expected
, cmd
, error
=None):
189 if error
== None: error
= "Invalid Number of Parameters for %s" % cmd
190 if num
!= expected
: raise MSNProtocolError
, error
192 def _parseHeader(h
, v
):
194 Split a certin number of known
195 header values with the format:
196 field1=val,field2=val,field3=val into
197 a dict mapping fields to values.
198 @param h: the header's key
199 @param v: the header's value as a string
202 if h
in ('passporturls','authentication-info','www-authenticate'):
203 v
= v
.replace('Passport1.4','').lstrip()
205 for fieldPair
in v
.split(','):
207 field
,value
= fieldPair
.split('=',1)
208 fields
[field
.lower()] = value
210 fields
[field
.lower()] = ''
214 def _parsePrimitiveHost(host
):
216 h
,p
= host
.replace('https://','').split('/',1)
220 def _login(userHandle
, passwd
, nexusServer
, cached
=0, authData
=''):
222 This function is used internally and should not ever be called
226 def _cb(server
, auth
):
227 loginFac
= ClientFactory()
228 loginFac
.protocol
= lambda : PassportLogin(cb
, userHandle
, passwd
, server
, auth
)
229 reactor
.connectSSL(_parsePrimitiveHost(server
)[0], 443, loginFac
, ClientContextFactory())
232 _cb(nexusServer
, authData
)
234 fac
= ClientFactory()
236 d
.addCallbacks(_cb
, callbackArgs
=(authData
,))
237 d
.addErrback(lambda f
: cb
.errback(f
))
238 fac
.protocol
= lambda : PassportNexus(d
, nexusServer
)
239 reactor
.connectSSL(_parsePrimitiveHost(nexusServer
)[0], 443, fac
, ClientContextFactory())
243 class PassportNexus(HTTPClient
):
246 Used to obtain the URL of a valid passport
249 This class is used internally and should
250 not be instantiated directly -- that is,
251 The passport logging in process is handled
252 transparantly by NotificationClient.
255 def __init__(self
, deferred
, host
):
256 self
.deferred
= deferred
257 self
.host
, self
.path
= _parsePrimitiveHost(host
)
259 def connectionMade(self
):
260 HTTPClient
.connectionMade(self
)
261 self
.sendCommand('GET', self
.path
)
262 self
.sendHeader('Host', self
.host
)
266 def handleHeader(self
, header
, value
):
268 self
.headers
[h
] = _parseHeader(h
, value
)
270 def handleEndHeaders(self
):
271 if self
.connected
: self
.transport
.loseConnection()
272 if not self
.headers
.has_key('passporturls') or not self
.headers
['passporturls'].has_key('dalogin'):
273 self
.deferred
.errback(failure
.Failure(failure
.DefaultException("Invalid Nexus Reply")))
275 self
.deferred
.callback('https://' + self
.headers
['passporturls']['dalogin'])
277 def handleResponse(self
, r
): pass
279 class PassportLogin(HTTPClient
):
281 This class is used internally to obtain
282 a login ticket from a passport HTTPS
283 server -- it should not be used directly.
288 def __init__(self
, deferred
, userHandle
, passwd
, host
, authData
):
289 self
.deferred
= deferred
290 self
.userHandle
= userHandle
292 self
.authData
= authData
293 self
.host
, self
.path
= _parsePrimitiveHost(host
)
295 def connectionMade(self
):
296 self
.sendCommand('GET', self
.path
)
297 self
.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
298 'sign-in=%s,pwd=%s,%s' % (quote(self
.userHandle
), self
.passwd
,self
.authData
))
299 self
.sendHeader('Host', self
.host
)
303 def handleHeader(self
, header
, value
):
305 self
.headers
[h
] = _parseHeader(h
, value
)
307 def handleEndHeaders(self
):
308 if self
._finished
: return
309 self
._finished
= 1 # I think we need this because of HTTPClient
310 if self
.connected
: self
.transport
.loseConnection()
311 authHeader
= 'authentication-info'
312 _interHeader
= 'www-authenticate'
313 if self
.headers
.has_key(_interHeader
): authHeader
= _interHeader
315 info
= self
.headers
[authHeader
]
316 status
= info
['da-status']
317 handler
= getattr(self
, 'login_%s' % (status
,), None)
320 else: raise Exception()
322 self
.deferred
.errback(failure
.Failure(e
))
324 def handleResponse(self
, r
): pass
326 def login_success(self
, info
):
327 ticket
= info
['from-pp']
328 ticket
= ticket
[1:len(ticket
)-1]
329 self
.deferred
.callback((LOGIN_SUCCESS
, ticket
))
331 def login_failed(self
, info
):
332 self
.deferred
.callback((LOGIN_FAILURE
, unquote(info
['cbtxt'])))
334 def login_redir(self
, info
):
335 self
.deferred
.callback((LOGIN_REDIRECT
, self
.headers
['location'], self
.authData
))
337 class MSNProtocolError(Exception):
339 This Exception is basically used for debugging
340 purposes, as the official MSN server should never
341 send anything _wrong_ and nobody in their right
342 mind would run their B{own} MSN server.
343 If it is raised by default command handlers
344 (handle_BLAH) the error will be logged.
351 I am the class used to represent an 'instant' message.
353 @ivar userHandle: The user handle (passport) of the sender
354 (this is only used when receiving a message)
355 @ivar screenName: The screen name of the sender (this is only used
356 when receiving a message)
357 @ivar message: The message
358 @ivar headers: The message headers
360 @ivar length: The message length (including headers and line endings)
361 @ivar ack: This variable is used to tell the server how to respond
362 once the message has been sent. If set to MESSAGE_ACK
363 (default) the server will respond with an ACK upon receiving
364 the message, if set to MESSAGE_NACK the server will respond
365 with a NACK upon failure to receive the message.
366 If set to MESSAGE_ACK_NONE the server will do nothing.
367 This is relevant for the return value of
368 SwitchboardClient.sendMessage (which will return
369 a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
370 and will fire when the respective ACK or NACK is received).
371 If set to MESSAGE_ACK_NONE sendMessage will return None.
374 MESSAGE_ACK_FAT
= 'D'
376 MESSAGE_ACK_NONE
= 'U'
380 def __init__(self
, length
=0, userHandle
="", screenName
="", message
="", specialMessage
=False):
381 self
.userHandle
= userHandle
382 self
.screenName
= screenName
383 self
.specialMessage
= specialMessage
384 self
.message
= message
385 self
.headers
= {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
389 def _calcMessageLen(self
):
391 used to calculte the number to send
392 as the message length when sending a message.
394 return reduce(operator
.add
, [len(x
[0]) + len(x
[1]) + 4 for x
in self
.headers
.items()]) + len(self
.message
) + 2
396 def setHeader(self
, header
, value
):
397 """ set the desired header """
398 self
.headers
[header
] = value
400 def getHeader(self
, header
):
402 get the desired header value
403 @raise KeyError: if no such header exists.
405 return self
.headers
[header
]
407 def hasHeader(self
, header
):
408 """ check to see if the desired header exists """
409 return self
.headers
.has_key(header
)
411 def getMessage(self
):
412 """ return the message - not including headers """
415 def setMessage(self
, message
):
416 """ set the message text """
417 self
.message
= message
422 This class represents a contact (user).
424 @ivar userGuid: The contact's user guid (unique string)
425 @ivar userHandle: The contact's user handle (passport).
426 @ivar screenName: The contact's screen name.
427 @ivar groups: A list of all the group IDs which this
429 @ivar lists: An integer representing the sum of all lists
430 that this contact belongs to.
431 @ivar caps: int, The capabilities of this client
432 @ivar msnobj: msnp2p.MSNOBJ, The MSNObject representing the contact's avatar
433 @ivar status: The contact's status code.
434 @type status: str if contact's status is known, None otherwise.
435 @ivar personal: The contact's personal message .
436 @type personal: str if contact's personal message is known, None otherwise.
438 @ivar homePhone: The contact's home phone number.
439 @type homePhone: str if known, otherwise None.
440 @ivar workPhone: The contact's work phone number.
441 @type workPhone: str if known, otherwise None.
442 @ivar mobilePhone: The contact's mobile phone number.
443 @type mobilePhone: str if known, otherwise None.
444 @ivar hasPager: Whether or not this user has a mobile pager
445 @ivar hasBlog: Whether or not this user has a MSN Spaces blog
453 def __init__(self
, userGuid
="", userHandle
="", screenName
="", lists
=0, caps
=0, msnobj
=None, groups
={}, status
=None, personal
=""):
454 self
.userGuid
= userGuid
455 self
.userHandle
= userHandle
456 self
.screenName
= screenName
460 self
.msnobjGot
= True
461 self
.groups
= [] # if applicable
462 self
.status
= status
# current status
463 self
.personal
= personal
466 self
.homePhone
= None
467 self
.workPhone
= None
468 self
.mobilePhone
= None
472 def setPhone(self
, phoneType
, value
):
474 set phone numbers/values for this specific user.
475 for phoneType check the *_PHONE constants and HAS_PAGER
478 t
= phoneType
.upper()
479 if t
== HOME_PHONE
: self
.homePhone
= value
480 elif t
== WORK_PHONE
: self
.workPhone
= value
481 elif t
== MOBILE_PHONE
: self
.mobilePhone
= value
482 elif t
== HAS_PAGER
: self
.hasPager
= value
483 elif t
== HAS_BLOG
: self
.hasBlog
= value
484 #else: raise ValueError, "Invalid Phone Type: " + t
486 def addToList(self
, listType
):
488 Update the lists attribute to
489 reflect being part of the
492 self
.lists |
= listType
494 def removeFromList(self
, listType
):
496 Update the lists attribute to
497 reflect being removed from the
500 self
.lists ^
= listType
502 class MSNContactList
:
504 This class represents a basic MSN contact list.
506 @ivar contacts: All contacts on my various lists
507 @type contacts: dict (mapping user handles to MSNContact objects)
508 @ivar groups: a mapping of group ids to group names
509 (groups can only exist on the forward list)
513 This is used only for storage and doesn't effect the
514 server's contact list.
524 def _getContactsFromList(self
, listType
):
526 Obtain all contacts which belong
527 to the given list type.
529 return dict([(uH
,obj
) for uH
,obj
in self
.contacts
.items() if obj
.lists
& listType
])
531 def addContact(self
, contact
):
535 self
.contacts
[contact
.userHandle
] = contact
537 def remContact(self
, userHandle
):
542 del self
.contacts
[userHandle
]
543 except KeyError: pass
545 def getContact(self
, userHandle
):
547 Obtain the MSNContact object
548 associated with the given
550 @return: the MSNContact object if
551 the user exists, or None.
554 return self
.contacts
[userHandle
]
558 def getBlockedContacts(self
):
560 Obtain all the contacts on my block list
562 return self
._getContactsFromList
(BLOCK_LIST
)
564 def getAuthorizedContacts(self
):
566 Obtain all the contacts on my auth list.
567 (These are contacts which I have verified
568 can view my state changes).
570 return self
._getContactsFromList
(ALLOW_LIST
)
572 def getReverseContacts(self
):
574 Get all contacts on my reverse list.
575 (These are contacts which have added me
576 to their forward list).
578 return self
._getContactsFromList
(REVERSE_LIST
)
580 def getContacts(self
):
582 Get all contacts on my forward list.
583 (These are the contacts which I have added
586 return self
._getContactsFromList
(FORWARD_LIST
)
588 def setGroup(self
, id, name
):
590 Keep a mapping from the given id
593 self
.groups
[id] = name
595 def remGroup(self
, id):
597 Removed the stored group
598 mapping for the given id.
602 except KeyError: pass
603 for c
in self
.contacts
:
604 if id in c
.groups
: c
.groups
.remove(id)
607 class MSNEventBase(LineReceiver
):
609 This class provides support for handling / dispatching events and is the
610 base class of the three main client protocols (DispatchClient,
611 NotificationClient, SwitchboardClient)
615 self
.ids
= {} # mapping of ids to Deferreds
619 self
.currentMessage
= None
621 def connectionLost(self
, reason
):
625 def connectionMade(self
):
628 def _fireCallback(self
, id, *args
):
630 Fire the callback for the given id
631 if one exists and return 1, else return false
633 if self
.ids
.has_key(id):
634 self
.ids
[id][0].callback(args
)
639 def _nextTransactionID(self
):
640 """ return a usable transaction ID """
642 if self
.currentID
> 1000: self
.currentID
= 1
643 return self
.currentID
645 def _createIDMapping(self
, data
=None):
647 return a unique transaction ID that is mapped internally to a
648 deferred .. also store arbitrary data if it is needed
650 id = self
._nextTransactionID
()
652 self
.ids
[id] = (d
, data
)
655 def checkMessage(self
, message
):
657 process received messages to check for file invitations and
658 typing notifications and other control type messages
660 raise NotImplementedError
662 def sendLine(self
, line
):
663 if LINEDEBUG
: log
.msg(">> " + line
)
664 LineReceiver
.sendLine(self
, line
)
666 def lineReceived(self
, line
):
667 if LINEDEBUG
: log
.msg("<< " + line
)
668 if self
.currentMessage
:
669 self
.currentMessage
.readPos
+= len(line
+CR
+LF
)
671 header
, value
= line
.split(':')
672 self
.currentMessage
.setHeader(header
, unquote(value
).lstrip())
675 #raise MSNProtocolError, "Invalid Message Header"
677 if line
== "" or self
.currentMessage
.specialMessage
:
679 if self
.currentMessage
.readPos
== self
.currentMessage
.length
: self
.rawDataReceived("") # :(
682 cmd
, params
= line
.split(' ', 1)
684 raise MSNProtocolError
, "Invalid Message, %s" % repr(line
)
686 if len(cmd
) != 3: raise MSNProtocolError
, "Invalid Command, %s" % repr(cmd
)
688 if self
.ids
.has_key(params
.split(' ')[0]):
689 self
.ids
[id].errback(int(cmd
))
692 else: # we received an error which doesn't map to a sent command
693 self
.gotError(int(cmd
))
696 handler
= getattr(self
, "handle_%s" % cmd
.upper(), None)
698 try: handler(params
.split(' '))
699 except MSNProtocolError
, why
: self
.gotBadLine(line
, why
)
701 self
.handle_UNKNOWN(cmd
, params
.split(' '))
703 def rawDataReceived(self
, data
):
705 self
.currentMessage
.readPos
+= len(data
)
706 diff
= self
.currentMessage
.readPos
- self
.currentMessage
.length
708 self
.currentMessage
.message
+= data
[:-diff
]
711 self
.currentMessage
.message
+= data
713 self
.currentMessage
.message
+= data
715 del self
.currentMessage
.readPos
716 m
= self
.currentMessage
717 self
.currentMessage
= None
718 if MESSAGEDEBUG
: log
.msg(m
.message
)
719 if not self
.checkMessage(m
):
720 self
.setLineMode(extra
)
723 self
.setLineMode(extra
)
725 ### protocol command handlers - no need to override these.
727 def handle_MSG(self
, params
):
728 checkParamLen(len(params
), 3, 'MSG')
730 messageLen
= int(params
[2])
731 except ValueError: raise MSNProtocolError
, "Invalid Parameter for MSG length argument"
732 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
=unquote(params
[1]))
734 def handle_UNKNOWN(self
, cmd
, params
):
735 """ implement me in subclasses if you want to handle unknown events """
736 log
.msg("Received unknown command (%s), params: %s" % (cmd
, params
))
740 def gotBadLine(self
, line
, why
):
741 """ called when a handler notifies me that this line is broken """
742 log
.msg('Error in line: %s (%s)' % (line
, why
))
744 def gotError(self
, errorCode
):
746 called when the server sends an error which is not in
747 response to a sent command (ie. it has no matching transaction ID)
749 log
.msg('Error %s' % (errorCodes
[errorCode
]))
751 class DispatchClient(MSNEventBase
):
753 This class provides support for clients connecting to the dispatch server
754 @ivar userHandle: your user handle (passport) needed before connecting.
757 # eventually this may become an attribute of the
761 def connectionMade(self
):
762 MSNEventBase
.connectionMade(self
)
763 self
.sendLine('VER %s %s' % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
765 ### protocol command handlers ( there is no need to override these )
767 def handle_VER(self
, params
):
768 versions
= params
[1:]
769 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
770 self
.transport
.loseConnection()
771 raise MSNProtocolError
, "Invalid version response"
772 id = self
._nextTransactionID
()
773 self
.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR
, self
.userHandle
))
775 def handle_CVR(self
, params
):
776 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.userHandle
))
778 def handle_XFR(self
, params
):
779 if len(params
) < 4: raise MSNProtocolError
, "Invalid number of parameters for XFR"
780 id, refType
, addr
= params
[:3]
781 # was addr a host:port pair?
783 host
, port
= addr
.split(':')
788 self
.gotNotificationReferral(host
, int(port
))
792 def gotNotificationReferral(self
, host
, port
):
794 called when we get a referral to the notification server.
796 @param host: the notification server's hostname
797 @param port: the port to connect to
802 class NotificationClient(MSNEventBase
):
804 This class provides support for clients connecting
805 to the notification server.
808 factory
= None # sssh pychecker
810 def __init__(self
, currentID
=0):
811 MSNEventBase
.__init
__(self
)
812 self
.currentID
= currentID
813 self
._state
= ['DISCONNECTED', {}]
815 self
.pingCheckTask
= None
816 self
.msnobj
= msnp2p
.MSNOBJ()
818 def _setState(self
, state
):
819 self
._state
[0] = state
822 return self
._state
[0]
824 def _getStateData(self
, key
):
825 return self
._state
[1][key
]
827 def _setStateData(self
, key
, value
):
828 self
._state
[1][key
] = value
830 def _remStateData(self
, *args
):
831 for key
in args
: del self
._state
[1][key
]
833 def connectionMade(self
):
834 MSNEventBase
.connectionMade(self
)
835 self
._setState
('CONNECTED')
836 self
.sendLine("VER %s %s" % (self
._nextTransactionID
(), MSN_PROTOCOL_VERSION
))
838 def connectionLost(self
, reason
):
839 self
._setState
('DISCONNECTED')
841 if self
.pingCheckTask
:
842 self
.pingCheckTask
.stop()
843 self
.pingCheckTask
= None
844 MSNEventBase
.connectionLost(self
, reason
)
846 def _getEmailFields(self
, message
):
847 fields
= message
.getMessage().strip().split('\n')
851 if len(a
) != 2: continue
858 def _gotInitialEmailNotification(self
, message
):
859 values
= self
._getEmailFields
(message
)
861 inboxunread
= int(values
["Inbox-Unread"])
862 foldersunread
= int(values
["Folders-Unread"])
865 if foldersunread
+ inboxunread
> 0: # For some reason MSN sends notifications about empty inboxes sometimes?
866 self
.gotInitialEmailNotification(inboxunread
, foldersunread
)
868 def _gotEmailNotification(self
, message
):
869 values
= self
._getEmailFields
(message
)
871 mailfrom
= values
["From"]
872 fromaddr
= values
["From-Addr"]
873 subject
= values
["Subject"]
874 junkbeginning
= "=?\"us-ascii\"?Q?"
876 subject
= subject
.replace(junkbeginning
, "").replace(junkend
, "").replace("_", " ")
878 # If any of the fields weren't found then it's not a big problem. We just ignore the message
880 self
.gotRealtimeEmailNotification(mailfrom
, fromaddr
, subject
)
882 def _gotMSNAlert(self
, message
):
883 notification
= xmlw
.parseText(message
.message
, beExtremelyLenient
=True)
884 siteurl
= notification
.getAttribute("siteurl")
885 notid
= notification
.getAttribute("id")
888 for e
in notification
.elements():
894 msgid
= msg
.getAttribute("id")
899 for e
in msg
.elements():
900 if e
.name
== "ACTION":
901 action
= e
.getAttribute("url")
902 if e
.name
== "SUBSCR":
903 subscr
= e
.getAttribute("url")
905 for e2
in e
.elements():
906 if e2
.name
== "TEXT":
907 bodytext
= e2
.__str__()
908 if not (action
and subscr
and bodytext
): return
910 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
911 subscrurl
= "%s¬ification_id=%s&message_id=%s&agent=messenger" % (subscr
, notid
, msgid
)
913 self
.gotMSNAlert(bodytext
, actionurl
, subscrurl
)
915 def _gotUBX(self
, message
):
916 lm
= message
.message
.lower()
917 p1
= lm
.find("<psm>") + 5
918 p2
= lm
.find("</psm>")
919 if p1
>= 0 and p2
>= 0:
920 personal
= unescapeFromXml(message
.message
[p1
:p2
])
921 msnContact
= self
.factory
.contacts
.getContact(message
.userHandle
)
922 if not msnContact
: return
923 msnContact
.personal
= personal
924 self
.contactPersonalChanged(message
.userHandle
, personal
)
926 self
.contactPersonalChanged(message
.userHandle
, '')
928 def checkMessage(self
, message
):
929 """ hook used for detecting specific notification messages """
930 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
931 if 'text/x-msmsgsprofile' in cTypes
:
932 self
.gotProfile(message
)
934 elif "text/x-msmsgsinitialemailnotification" in cTypes
:
935 self
._gotInitialEmailNotification
(message
)
937 elif "text/x-msmsgsemailnotification" in cTypes
:
938 self
._gotEmailNotification
(message
)
940 elif "NOTIFICATION" == message
.userHandle
and message
.specialMessage
== True:
941 self
._gotMSNAlert
(message
)
943 elif "UBX" == message
.screenName
and message
.specialMessage
== True:
944 self
._gotUBX
(message
)
948 ### protocol command handlers - no need to override these
950 def handle_VER(self
, params
):
951 versions
= params
[1:]
952 if versions
is None or ' '.join(versions
) != MSN_PROTOCOL_VERSION
:
953 self
.transport
.loseConnection()
954 raise MSNProtocolError
, "Invalid version response"
955 self
.sendLine("CVR %s %s %s" % (self
._nextTransactionID
(), MSN_CVR_STR
, self
.factory
.userHandle
))
957 def handle_CVR(self
, params
):
958 self
.sendLine("USR %s TWN I %s" % (self
._nextTransactionID
(), self
.factory
.userHandle
))
960 def handle_USR(self
, params
):
961 if not (4 <= len(params
) <= 6):
962 raise MSNProtocolError
, "Invalid Number of Parameters for USR"
964 mechanism
= params
[1]
965 if mechanism
== "OK":
966 self
.loggedIn(params
[2], int(params
[3]))
967 elif params
[2].upper() == "S":
968 # we need to obtain auth from a passport server
970 d
= _login(f
.userHandle
, f
.password
, f
.passportServer
, authData
=params
[3])
971 d
.addCallback(self
._passportLogin
)
972 d
.addErrback(self
._passportError
)
974 def _passportLogin(self
, result
):
975 if result
[0] == LOGIN_REDIRECT
:
976 d
= _login(self
.factory
.userHandle
, self
.factory
.password
,
977 result
[1], cached
=1, authData
=result
[2])
978 d
.addCallback(self
._passportLogin
)
979 d
.addErrback(self
._passportError
)
980 elif result
[0] == LOGIN_SUCCESS
:
981 self
.sendLine("USR %s TWN S %s" % (self
._nextTransactionID
(), result
[1]))
982 elif result
[0] == LOGIN_FAILURE
:
983 self
.loginFailure(result
[1])
985 def _passportError(self
, failure
):
986 self
.loginFailure("Exception while authenticating: %s" % failure
)
988 def handle_CHG(self
, params
):
990 if not self
._fireCallback
(id, params
[1]):
991 if self
.factory
: self
.factory
.status
= params
[1]
992 self
.statusChanged(params
[1])
994 def handle_ILN(self
, params
):
995 #checkParamLen(len(params), 6, 'ILN')
996 msnContact
= self
.factory
.contacts
.getContact(params
[2])
997 if not msnContact
: return
998 msnContact
.status
= params
[1]
999 msnContact
.screenName
= unquote(params
[3])
1000 if len(params
) > 4: msnContact
.caps
= int(params
[4])
1002 self
.handleAvatarHelper(msnContact
, params
[5])
1004 self
.handleAvatarGoneHelper(msnContact
)
1005 self
.gotContactStatus(params
[1], params
[2], unquote(params
[3]))
1007 def handleAvatarGoneHelper(self
, msnContact
):
1008 if msnContact
.msnobj
:
1009 msnContact
.msnobj
= None
1010 msnContact
.msnobjGot
= True
1011 self
.contactAvatarChanged(msnContact
.userHandle
, "")
1013 def handleAvatarHelper(self
, msnContact
, msnobjStr
):
1014 msnobj
= msnp2p
.MSNOBJ(unquote(msnobjStr
))
1015 if not msnContact
.msnobj
or msnobj
.sha1d
!= msnContact
.msnobj
.sha1d
:
1016 if msnp2p
.MSNP2P_DEBUG
: log
.msg("Updated MSNOBJ received!" + msnobjStr
)
1017 msnContact
.msnobj
= msnobj
1018 msnContact
.msnobjGot
= False
1019 self
.contactAvatarChanged(msnContact
.userHandle
, msnContact
.msnobj
.sha1d
)
1021 def handle_CHL(self
, params
):
1022 checkParamLen(len(params
), 2, 'CHL')
1023 response
= msnp11chl
.doChallenge(params
[1])
1024 self
.sendLine("QRY %s %s %s" % (self
._nextTransactionID
(), msnp11chl
.MSNP11_PRODUCT_ID
, len(response
)))
1025 self
.transport
.write(response
)
1027 def handle_QRY(self
, params
):
1030 def handle_NLN(self
, params
):
1031 if not self
.factory
: return
1032 msnContact
= self
.factory
.contacts
.getContact(params
[1])
1033 if not msnContact
: return
1034 msnContact
.status
= params
[0]
1035 msnContact
.screenName
= unquote(params
[2])
1036 if len(params
) > 3: msnContact
.caps
= int(params
[3])
1038 self
.handleAvatarHelper(msnContact
, params
[4])
1040 self
.handleAvatarGoneHelper(msnContact
)
1041 self
.contactStatusChanged(params
[0], params
[1], unquote(params
[2]))
1043 def handle_FLN(self
, params
):
1044 checkParamLen(len(params
), 1, 'FLN')
1045 msnContact
= self
.factory
.contacts
.getContact(params
[0])
1047 msnContact
.status
= STATUS_OFFLINE
1048 self
.contactOffline(params
[0])
1050 def handle_LST(self
, params
):
1051 if self
._getState
() != 'SYNC': return
1053 userHandle
, screenName
, userGuid
, lists
, groups
= getVals(params
)
1055 if not userHandle
or lists
< 1:
1056 raise MSNProtocolError
, "Unknown LST " + str(params
) # debug
1057 contact
= MSNContact(userGuid
, userHandle
, screenName
, lists
)
1058 if contact
.lists
& FORWARD_LIST
:
1059 contact
.groups
.extend(map(str, groups
))
1060 self
._getStateData
('list').addContact(contact
)
1061 self
._setStateData
('last_contact', contact
)
1062 sofar
= self
._getStateData
('lst_sofar') + 1
1063 if sofar
== self
._getStateData
('lst_reply'):
1064 # this is the best place to determine that
1065 # a syn realy has finished - msn _may_ send
1066 # BPR information for the last contact
1067 # which is unfortunate because it means
1068 # that the real end of a syn is non-deterministic.
1069 # to handle this we'll keep 'last_contact' hanging
1070 # around in the state data and update it if we need
1072 self
._setState
('SESSION')
1073 contacts
= self
._getStateData
('list')
1074 phone
= self
._getStateData
('phone')
1075 id = self
._getStateData
('synid')
1076 self
._remStateData
('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
1077 self
._fireCallback
(id, contacts
, phone
)
1079 self
._setStateData
('lst_sofar',sofar
)
1081 def handle_BLP(self
, params
):
1082 # check to see if this is in response to a SYN
1083 if self
._getState
() == 'SYNC':
1084 self
._getStateData
('list').privacy
= listCodeToID
[params
[0].lower()]
1087 self
.factory
.contacts
.privacy
= listCodeToID
[params
[1].lower()]
1088 self
._fireCallback
(id, params
[1])
1090 def handle_GTC(self
, params
):
1091 # check to see if this is in response to a SYN
1092 if self
._getState
() == 'SYNC':
1093 if params
[0].lower() == "a": self
._getStateData
('list').autoAdd
= 0
1094 elif params
[0].lower() == "n": self
._getStateData
('list').autoAdd
= 1
1095 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1098 if params
[1].lower() == "a": self
._fireCallback
(id, 0)
1099 elif params
[1].lower() == "n": self
._fireCallback
(id, 1)
1100 else: raise MSNProtocolError
, "Invalid Paramater for GTC" # debug
1102 def handle_SYN(self
, params
):
1104 self
._setStateData
('phone', []) # Always needs to be set
1105 if params
[3] == 0: # No LST will be received. New account?
1106 self
._setState
('SESSION')
1107 self
._fireCallback
(id, None, None)
1109 contacts
= MSNContactList()
1110 self
._setStateData
('list', contacts
)
1111 self
._setStateData
('lst_reply', int(params
[3]))
1112 self
._setStateData
('lsg_reply', int(params
[4]))
1113 self
._setStateData
('lst_sofar', 0)
1115 def handle_LSG(self
, params
):
1116 if self
._getState
() == 'SYNC':
1117 self
._getStateData
('list').groups
[params
[1]] = unquote(params
[0])
1119 def handle_PRP(self
, params
):
1120 if params
[1] == "MFN":
1121 self
._fireCallback
(int(params
[0]), unquote(params
[2]))
1122 elif self
._getState
() == 'SYNC':
1123 self
._getStateData
('phone').append((params
[0], unquote(params
[1])))
1125 self
._fireCallback
(int(params
[0]), int(params
[1]), unquote(params
[3]))
1127 def handle_BPR(self
, params
):
1128 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_BPR called with no contact list" # debug
1129 numParams
= len(params
)
1130 if numParams
== 2: # part of a syn
1131 self
._getStateData
('last_contact').setPhone(params
[0], unquote(params
[1]))
1132 elif numParams
== 4:
1133 self
.factory
.contacts
.version
= int(params
[0])
1134 userHandle
, phoneType
, number
= params
[1], params
[2], unquote(params
[3])
1135 self
.factory
.contacts
.getContact(userHandle
).setPhone(phoneType
, number
)
1136 self
.gotPhoneNumber(userHandle
, phoneType
, number
)
1139 def handle_ADG(self
, params
):
1140 checkParamLen(len(params
), 5, 'ADG')
1142 if not self
._fireCallback
(id, int(params
[1]), unquote(params
[2]), int(params
[3])):
1143 raise MSNProtocolError
, "ADG response does not match up to a request" # debug
1145 def handle_RMG(self
, params
):
1146 checkParamLen(len(params
), 3, 'RMG')
1148 if not self
._fireCallback
(id, int(params
[1]), int(params
[2])):
1149 raise MSNProtocolError
, "RMG response does not match up to a request" # debug
1151 def handle_REG(self
, params
):
1152 checkParamLen(len(params
), 5, 'REG')
1154 if not self
._fireCallback
(id, int(params
[1]), int(params
[2]), unquote(params
[3])):
1155 raise MSNProtocolError
, "REG response does not match up to a request" # debug
1157 def handle_ADC(self
, params
):
1158 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_ADC called with no contact list"
1159 numParams
= len(params
)
1160 if numParams
< 3 or params
[1].upper() not in ('AL','BL','RL','FL', 'PL'):
1161 raise MSNProtocolError
, "Invalid Paramaters for ADC" # debug
1163 listType
= params
[1].lower()
1164 userHandle
, screenName
, userGuid
, ignored1
, groups
= getVals(params
[2:])
1166 if groups
and listType
.upper() != FORWARD_LIST
:
1167 raise MSNProtocolError
, "Only forward list can contain groups" # debug
1169 if not self
._fireCallback
(id, listCodeToID
[listType
], userGuid
, userHandle
, screenName
):
1170 c
= self
.factory
.contacts
.getContact(userHandle
)
1172 c
= MSNContact(userGuid
=userGuid
, userHandle
=userHandle
, screenName
=screenName
)
1173 self
.factory
.contacts
.addContact(c
)
1174 c
.addToList(PENDING_LIST
)
1175 self
.userAddedMe(userGuid
, userHandle
, screenName
)
1177 def handle_REM(self
, params
):
1178 if not self
.factory
.contacts
: raise MSNProtocolError
, "handle_REM called with no contact list available!"
1179 numParams
= len(params
)
1180 if numParams
< 3 or params
[1].upper() not in ('AL','BL','FL','RL'):
1181 raise MSNProtocolError
, "Invalid Paramaters for REM" # debug
1183 listType
= params
[1].lower()
1184 userHandle
= params
[2]
1187 if params
[1] != "FL": raise MSNProtocolError
, "Only forward list can contain groups" # debug
1188 groupID
= int(params
[3])
1189 if not self
._fireCallback
(id, listCodeToID
[listType
], userHandle
, groupID
):
1190 if listType
.upper() != "RL": return
1191 c
= self
.factory
.contacts
.getContact(userHandle
)
1193 c
.removeFromList(REVERSE_LIST
)
1194 if c
.lists
== 0: self
.factory
.contacts
.remContact(c
.userHandle
)
1195 self
.userRemovedMe(userHandle
)
1197 def handle_XFR(self
, params
):
1198 checkParamLen(len(params
), 5, 'XFR')
1200 # check to see if they sent a host/port pair
1202 host
, port
= params
[2].split(':')
1207 if not self
._fireCallback
(id, host
, int(port
), params
[4]):
1208 raise MSNProtocolError
, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1210 def handle_RNG(self
, params
):
1211 checkParamLen(len(params
), 6, 'RNG')
1212 # check for host:port pair
1214 host
, port
= params
[1].split(":")
1219 self
.gotSwitchboardInvitation(int(params
[0]), host
, port
, params
[3], params
[4],
1222 def handle_NOT(self
, params
):
1223 checkParamLen(len(params
), 1, 'NOT')
1225 messageLen
= int(params
[0])
1226 except ValueError: raise MSNProtocolError
, "Invalid Parameter for NOT length argument"
1227 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
="NOTIFICATION", specialMessage
=True)
1230 def handle_UBX(self
, params
):
1231 checkParamLen(len(params
), 2, 'UBX')
1233 messageLen
= int(params
[1])
1234 except ValueError: raise MSNProtocolError
, "Invalid Parameter for UBX length argument"
1236 self
.currentMessage
= MSNMessage(length
=messageLen
, userHandle
=params
[0], screenName
="UBX", specialMessage
=True)
1239 self
.contactPersonalChanged(params
[0], '')
1241 def handle_UUX(self
, params
):
1242 checkParamLen(len(params
), 2, 'UUX')
1243 if params
[1] != '0': return
1245 self
._fireCallback
(id)
1247 def handle_OUT(self
, params
):
1248 checkParamLen(len(params
), 1, 'OUT')
1249 if params
[0] == "OTH": self
.multipleLogin()
1250 elif params
[0] == "SSD": self
.serverGoingDown()
1251 else: raise MSNProtocolError
, "Invalid Parameters received for OUT" # debug
1253 def handle_QNG(self
, params
):
1254 self
.pingCounter
= 0 # They replied to a ping. We'll forgive them for any they may have missed, because they're alive again now
1258 def pingChecker(self
):
1259 if self
.pingCounter
> 5:
1260 # The server has ignored 5 pings, lets kill the connection
1261 self
.transport
.loseConnection()
1263 self
.sendLine("PNG")
1264 self
.pingCounter
+= 1
1266 def pingCheckerStart(self
, *args
):
1267 self
.pingCheckTask
= task
.LoopingCall(self
.pingChecker
)
1268 self
.pingCheckTask
.start(PINGSPEED
)
1270 def loggedIn(self
, userHandle
, verified
):
1272 Called when the client has logged in.
1273 The default behaviour of this method is to
1274 update the factory with our screenName and
1275 to sync the contact list (factory.contacts).
1276 When this is complete self.listSynchronized
1279 @param userHandle: our userHandle
1280 @param verified: 1 if our passport has been (verified), 0 if not.
1281 (i'm not sure of the significace of this)
1285 d
.addCallback(self
.listSynchronized
)
1286 d
.addCallback(self
.pingCheckerStart
)
1288 def loginFailure(self
, message
):
1290 Called when the client fails to login.
1292 @param message: a message indicating the problem that was encountered
1296 def gotProfile(self
, message
):
1298 Called after logging in when the server sends an initial
1299 message with MSN/passport specific profile information
1300 such as country, number of kids, etc.
1301 Check the message headers for the specific values.
1303 @param message: The profile message
1307 def listSynchronized(self
, *args
):
1309 Lists are now synchronized by default upon logging in, this
1310 method is called after the synchronization has finished
1311 and the factory now has the up-to-date contacts.
1315 def contactAvatarChanged(self
, userHandle
, hash):
1317 Called when we receive the first, or a new <msnobj/> from a
1320 @param userHandle: contact who's msnobj has been changed
1321 @param hash: sha1 hash of their avatar
1324 def statusChanged(self
, statusCode
):
1326 Called when our status changes and its not in response to a
1329 @param statusCode: 3-letter status code
1333 def gotContactStatus(self
, statusCode
, userHandle
, screenName
):
1335 Called when we receive a list of statuses upon login.
1337 @param statusCode: 3-letter status code
1338 @param userHandle: the contact's user handle (passport)
1339 @param screenName: the contact's screen name
1343 def contactStatusChanged(self
, statusCode
, userHandle
, screenName
):
1345 Called when we're notified that a contact's status has changed.
1347 @param statusCode: 3-letter status code
1348 @param userHandle: the contact's user handle (passport)
1349 @param screenName: the contact's screen name
1353 def contactPersonalChanged(self
, userHandle
, personal
):
1355 Called when a contact's personal message changes.
1357 @param userHandle: the contact who changed their personal message
1358 @param personal : the new personal message
1362 def contactOffline(self
, userHandle
):
1364 Called when a contact goes offline.
1366 @param userHandle: the contact's user handle
1370 def gotMessage(self
, message
):
1372 Called when there is a message from the notification server
1373 that is not understood by default.
1375 @param message: the MSNMessage.
1379 def gotMSNAlert(self
, body
, action
, subscr
):
1381 Called when the server sends an MSN Alert (http://alerts.msn.com)
1383 @param body : the alert text
1384 @param action: a URL with more information for the user to view
1385 @param subscr: a URL the user can use to modify their alert subscription
1389 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
1391 Called when the server sends you details about your hotmail
1392 inbox. This is only ever called once, on login.
1394 @param inboxunread : the number of unread items in your inbox
1395 @param foldersunread: the number of unread items in other folders
1399 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
1401 Called when the server sends us realtime email
1402 notification. This means that you have received
1403 a new email in your hotmail inbox.
1405 @param mailfrom: the sender of the email
1406 @param fromaddr: the sender of the email (I don't know :P)
1407 @param subject : the email subject
1411 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
1413 Called when the server sends us phone details about
1414 a specific user (for example after a user is added
1415 the server will send their status, phone details etc.
1417 @param userHandle: the contact's user handle (passport)
1418 @param phoneType: the specific phoneType
1419 (*_PHONE constants or HAS_PAGER)
1420 @param number: the value/phone number.
1424 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
1426 Called when a user adds me to their list. (ie. they have been added to
1429 @param userHandle: the userHandle of the user
1430 @param screenName: the screen name of the user
1434 def userRemovedMe(self
, userHandle
):
1436 Called when a user removes us from their contact list
1437 (they are no longer on our reverseContacts list.
1439 @param userHandle: the contact's user handle (passport)
1443 def gotSwitchboardInvitation(self
, sessionID
, host
, port
,
1444 key
, userHandle
, screenName
):
1446 Called when we get an invitation to a switchboard server.
1447 This happens when a user requests a chat session with us.
1449 @param sessionID: session ID number, must be remembered for logging in
1450 @param host: the hostname of the switchboard server
1451 @param port: the port to connect to
1452 @param key: used for authorization when connecting
1453 @param userHandle: the user handle of the person who invited us
1454 @param screenName: the screen name of the person who invited us
1458 def multipleLogin(self
):
1460 Called when the server says there has been another login
1461 under our account, the server should disconnect us right away.
1465 def serverGoingDown(self
):
1467 Called when the server has notified us that it is going down for
1474 def changeStatus(self
, status
):
1476 Change my current status. This method will add
1477 a default callback to the returned Deferred
1478 which will update the status attribute of the
1481 @param status: 3-letter status code (as defined by
1482 the STATUS_* constants)
1483 @return: A Deferred, the callback of which will be
1484 fired when the server confirms the change
1485 of status. The callback argument will be
1486 a tuple with the new status code as the
1490 id, d
= self
._createIDMapping
()
1491 self
.sendLine("CHG %s %s %s %s" % (id, status
, str(MSNContact
.MSNC1 | MSNContact
.MSNC2 | MSNContact
.MSNC3 | MSNContact
.MSNC4
), quote(self
.msnobj
.text
)))
1493 self
.factory
.status
= r
[0]
1495 return d
.addCallback(_cb
)
1497 def setPrivacyMode(self
, privLevel
):
1499 Set my privacy mode on the server.
1502 This only keeps the current privacy setting on
1503 the server for later retrieval, it does not
1504 effect the way the server works at all.
1506 @param privLevel: This parameter can be true, in which
1507 case the server will keep the state as
1508 'al' which the official client interprets
1509 as -> allow messages from only users on
1510 the allow list. Alternatively it can be
1511 false, in which case the server will keep
1512 the state as 'bl' which the official client
1513 interprets as -> allow messages from all
1514 users except those on the block list.
1516 @return: A Deferred, the callback of which will be fired when
1517 the server replies with the new privacy setting.
1518 The callback argument will be a tuple, the only element
1519 of which being either 'al' or 'bl' (the new privacy setting).
1522 id, d
= self
._createIDMapping
()
1523 if privLevel
: self
.sendLine("BLP %s AL" % id)
1524 else: self
.sendLine("BLP %s BL" % id)
1529 Used for keeping an up-to-date contact list.
1530 A callback is added to the returned Deferred
1531 that updates the contact list on the factory
1532 and also sets my state to STATUS_ONLINE.
1535 This is called automatically upon signing
1536 in using the version attribute of
1537 factory.contacts, so you may want to persist
1538 this object accordingly. Because of this there
1539 is no real need to ever call this method
1542 @return: A Deferred, the callback of which will be
1543 fired when the server sends an adequate reply.
1544 The callback argument will be a tuple with two
1545 elements, the new list (MSNContactList) and
1546 your current state (a dictionary). If the version
1547 you sent _was_ the latest list version, both elements
1548 will be None. To just request the list send a version of 0.
1551 self
._setState
('SYNC')
1552 id, d
= self
._createIDMapping
(data
=None)
1553 self
._setStateData
('synid',id)
1554 self
.sendLine("SYN %s %s %s" % (id, 0, 0))
1556 self
.changeStatus(STATUS_ONLINE
)
1557 if r
[0] is not None:
1558 self
.factory
.contacts
= r
[0]
1560 return d
.addCallback(_cb
)
1562 def setPhoneDetails(self
, phoneType
, value
):
1564 Set/change my phone numbers stored on the server.
1566 @param phoneType: phoneType can be one of the following
1567 constants - HOME_PHONE, WORK_PHONE,
1568 MOBILE_PHONE, HAS_PAGER.
1569 These are pretty self-explanatory, except
1570 maybe HAS_PAGER which refers to whether or
1571 not you have a pager.
1572 @param value: for all of the *_PHONE constants the value is a
1573 phone number (str), for HAS_PAGER accepted values
1574 are 'Y' (for yes) and 'N' (for no).
1576 @return: A Deferred, the callback for which will be fired when
1577 the server confirms the change has been made. The
1578 callback argument will be a tuple with 2 elements, the
1579 first being the new list version (int) and the second
1580 being the new phone number value (str).
1582 raise "ProbablyDoesntWork"
1583 # XXX: Add a default callback which updates
1584 # factory.contacts.version and the relevant phone
1586 id, d
= self
._createIDMapping
()
1587 self
.sendLine("PRP %s %s %s" % (id, phoneType
, quote(value
)))
1590 def addListGroup(self
, name
):
1592 Used to create a new list group.
1593 A default callback is added to the
1594 returned Deferred which updates the
1595 contacts attribute of the factory.
1597 @param name: The desired name of the new group.
1599 @return: A Deferred, the callbacck for which will be called
1600 when the server clarifies that the new group has been
1601 created. The callback argument will be a tuple with 3
1602 elements: the new list version (int), the new group name
1603 (str) and the new group ID (int).
1606 raise "ProbablyDoesntWork"
1607 id, d
= self
._createIDMapping
()
1608 self
.sendLine("ADG %s %s 0" % (id, quote(name
)))
1610 if self
.factory
.contacts
:
1611 self
.factory
.contacts
.version
= r
[0]
1612 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1614 return d
.addCallback(_cb
)
1616 def remListGroup(self
, groupID
):
1618 Used to remove a list group.
1619 A default callback is added to the
1620 returned Deferred which updates the
1621 contacts attribute of the factory.
1623 @param groupID: the ID of the desired group to be removed.
1625 @return: A Deferred, the callback for which will be called when
1626 the server clarifies the deletion of the group.
1627 The callback argument will be a tuple with 2 elements:
1628 the new list version (int) and the group ID (int) of
1632 raise "ProbablyDoesntWork"
1633 id, d
= self
._createIDMapping
()
1634 self
.sendLine("RMG %s %s" % (id, groupID
))
1636 self
.factory
.contacts
.version
= r
[0]
1637 self
.factory
.contacts
.remGroup(r
[1])
1639 return d
.addCallback(_cb
)
1641 def renameListGroup(self
, groupID
, newName
):
1643 Used to rename an existing list group.
1644 A default callback is added to the returned
1645 Deferred which updates the contacts attribute
1648 @param groupID: the ID of the desired group to rename.
1649 @param newName: the desired new name for the group.
1651 @return: A Deferred, the callback for which will be called
1652 when the server clarifies the renaming.
1653 The callback argument will be a tuple of 3 elements,
1654 the new list version (int), the group id (int) and
1655 the new group name (str).
1658 raise "ProbablyDoesntWork"
1659 id, d
= self
._createIDMapping
()
1660 self
.sendLine("REG %s %s %s 0" % (id, groupID
, quote(newName
)))
1662 self
.factory
.contacts
.version
= r
[0]
1663 self
.factory
.contacts
.setGroup(r
[1], r
[2])
1665 return d
.addCallback(_cb
)
1667 def addContact(self
, listType
, userHandle
):
1669 Used to add a contact to the desired list.
1670 A default callback is added to the returned
1671 Deferred which updates the contacts attribute of
1672 the factory with the new contact information.
1673 If you are adding a contact to the forward list
1674 and you want to associate this contact with multiple
1675 groups then you will need to call this method for each
1676 group you would like to add them to, changing the groupID
1677 parameter. The default callback will take care of updating
1678 the group information on the factory's contact list.
1680 @param listType: (as defined by the *_LIST constants)
1681 @param userHandle: the user handle (passport) of the contact
1684 @return: A Deferred, the callback for which will be called when
1685 the server has clarified that the user has been added.
1686 The callback argument will be a tuple with 4 elements:
1687 the list type, the contact's user handle, the new list
1688 version, and the group id (if relevant, otherwise it
1692 id, d
= self
._createIDMapping
()
1693 try: # Make sure the contact isn't actually on the list
1694 if self
.factory
.contacts
.getContact(userHandle
).lists
& listType
: return
1695 except AttributeError: pass
1696 listType
= listIDToCode
[listType
].upper()
1697 if listType
== "FL":
1698 self
.sendLine("ADC %s %s N=%s F=%s" % (id, listType
, userHandle
, userHandle
))
1700 self
.sendLine("ADC %s %s N=%s" % (id, listType
, userHandle
))
1703 if not self
.factory
: return
1704 c
= self
.factory
.contacts
.getContact(r
[2])
1706 c
= MSNContact(userGuid
=r
[1], userHandle
=r
[2], screenName
=r
[3])
1707 #if r[3]: c.groups.append(r[3])
1710 return d
.addCallback(_cb
)
1712 def remContact(self
, listType
, userHandle
):
1714 Used to remove a contact from the desired list.
1715 A default callback is added to the returned deferred
1716 which updates the contacts attribute of the factory
1717 to reflect the new contact information.
1719 @param listType: (as defined by the *_LIST constants)
1720 @param userHandle: the user handle (passport) of the
1721 contact being removed
1723 @return: A Deferred, the callback for which will be called when
1724 the server has clarified that the user has been removed.
1725 The callback argument will be a tuple of 3 elements:
1726 the list type, the contact's user handle and the group ID
1727 (if relevant, otherwise it will be None)
1730 id, d
= self
._createIDMapping
()
1731 try: # Make sure the contact is actually on this list
1732 if not (self
.factory
.contacts
.getContact(userHandle
).lists
& listType
): return
1733 except AttributeError: return
1734 listType
= listIDToCode
[listType
].upper()
1735 if listType
== "FL":
1737 c
= self
.factory
.contacts
.getContact(userHandle
)
1738 userGuid
= c
.userGuid
1739 except AttributeError: return
1740 self
.sendLine("REM %s FL %s" % (id, userGuid
))
1742 self
.sendLine("REM %s %s %s" % (id, listType
, userHandle
))
1745 if listType
== "FL":
1746 r
= (r
[0], userHandle
, r
[2]) # make sure we always get a userHandle
1747 l
= self
.factory
.contacts
1748 c
= l
.getContact(r
[1])
1752 if group
: # they may not have been removed from the list
1753 c
.groups
.remove(group
)
1754 if c
.groups
: shouldRemove
= 0
1756 c
.removeFromList(r
[0])
1757 if c
.lists
== 0: l
.remContact(c
.userHandle
)
1759 return d
.addCallback(_cb
)
1761 def changeScreenName(self
, newName
):
1763 Used to change your current screen name.
1764 A default callback is added to the returned
1765 Deferred which updates the screenName attribute
1766 of the factory and also updates the contact list
1769 @param newName: the new screen name
1771 @return: A Deferred, the callback for which will be called
1772 when the server acknowledges the change.
1773 The callback argument will be a tuple of 1 element,
1774 the new screen name.
1777 id, d
= self
._createIDMapping
()
1778 self
.sendLine("PRP %s MFN %s" % (id, quote(newName
)))
1780 self
.factory
.screenName
= r
[0]
1782 return d
.addCallback(_cb
)
1784 def changePersonalMessage(self
, personal
):
1786 Used to change your personal message.
1788 @param personal: the new screen name
1790 @return: A Deferred, the callback for which will be called
1791 when the server acknowledges the change.
1792 The callback argument will be a tuple of 1 element,
1793 the personal message.
1796 id, d
= self
._createIDMapping
()
1799 data
= "<Data><PSM>" + personal
+ "</PSM><CurrentMedia></CurrentMedia></Data>"
1800 self
.sendLine("UUX %s %s" % (id, len(data
)))
1801 self
.transport
.write(data
)
1803 self
.factory
.personal
= personal
1805 return d
.addCallback(_cb
)
1807 def changeAvatar(self
, imageData
, push
):
1809 Used to change the avatar that other users see.
1811 @param imageData: the PNG image data to set as the avatar
1812 @param push : whether to push the update to the server
1813 (it will otherwise be sent with the next
1816 @return: If push==True, a Deferred, the callback for which
1817 will be called when the server acknowledges the change.
1818 The callback argument will be the same as for changeStatus.
1821 if self
.msnobj
and imageData
== self
.msnobj
.imageData
: return
1823 self
.msnobj
.setData(self
.factory
.userHandle
, imageData
)
1825 self
.msnobj
.setNull()
1826 if push
: return self
.changeStatus(self
.factory
.status
) # Push to server
1829 def requestSwitchboardServer(self
):
1831 Used to request a switchboard server to use for conversations.
1833 @return: A Deferred, the callback for which will be called when
1834 the server responds with the switchboard information.
1835 The callback argument will be a tuple with 3 elements:
1836 the host of the switchboard server, the port and a key
1837 used for logging in.
1840 id, d
= self
._createIDMapping
()
1841 self
.sendLine("XFR %s SB" % id)
1846 Used to log out of the notification server.
1847 After running the method the server is expected
1848 to close the connection.
1851 if self
.pingCheckTask
:
1852 self
.pingCheckTask
.stop()
1853 self
.pingCheckTask
= None
1854 self
.sendLine("OUT")
1856 class NotificationFactory(ClientFactory
):
1858 Factory for the NotificationClient protocol.
1859 This is basically responsible for keeping
1860 the state of the client and thus should be used
1861 in a 1:1 situation with clients.
1863 @ivar contacts: An MSNContactList instance reflecting
1864 the current contact list -- this is
1865 generally kept up to date by the default
1867 @ivar userHandle: The client's userHandle, this is expected
1868 to be set by the client and is used by the
1869 protocol (for logging in etc).
1870 @ivar screenName: The client's current screen-name -- this is
1871 generally kept up to date by the default
1873 @ivar password: The client's password -- this is (obviously)
1874 expected to be set by the client.
1875 @ivar passportServer: This must point to an msn passport server
1876 (the whole URL is required)
1877 @ivar status: The status of the client -- this is generally kept
1878 up to date by the default command handlers
1885 passportServer
= 'https://nexus.passport.com/rdr/pprdr.asp'
1887 protocol
= NotificationClient
1890 class SwitchboardClient(MSNEventBase
):
1892 This class provides support for clients connecting to a switchboard server.
1894 Switchboard servers are used for conversations with other people
1895 on the MSN network. This means that the number of conversations at
1896 any given time will be directly proportional to the number of
1897 connections to varioius switchboard servers.
1899 MSN makes no distinction between single and group conversations,
1900 so any number of users may be invited to join a specific conversation
1901 taking place on a switchboard server.
1903 @ivar key: authorization key, obtained when receiving
1904 invitation / requesting switchboard server.
1905 @ivar userHandle: your user handle (passport)
1906 @ivar sessionID: unique session ID, used if you are replying
1907 to a switchboard invitation
1908 @ivar reply: set this to 1 in connectionMade or before to signifiy
1909 that you are replying to a switchboard invitation.
1919 def __init__(self
, msnobj
=None):
1920 MSNEventBase
.__init
__(self
)
1921 self
.pendingUsers
= {}
1922 self
.cookies
= {'iCookies' : {}} # will maybe be moved to a factory in the future
1924 self
.msnobj
= msnobj
1926 def connectionMade(self
):
1927 MSNEventBase
.connectionMade(self
)
1930 def connectionLost(self
, reason
):
1931 self
.cookies
['iCookies'] = {}
1932 MSNEventBase
.connectionLost(self
, reason
)
1934 def _sendInit(self
):
1936 send initial data based on whether we are replying to an invitation
1939 id = self
._nextTransactionID
()
1941 self
.sendLine("USR %s %s %s" % (id, self
.userHandle
, self
.key
))
1943 self
.sendLine("ANS %s %s %s %s" % (id, self
.userHandle
, self
.key
, self
.sessionID
))
1945 def _newInvitationCookie(self
):
1947 if self
._iCookie
> 1000: self
._iCookie
= 1
1948 return self
._iCookie
1950 def _checkTyping(self
, message
, cTypes
):
1951 """ helper method for checkMessage """
1952 if 'text/x-msmsgscontrol' in cTypes
and message
.hasHeader('TypingUser'):
1953 self
.userTyping(message
)
1956 def _checkFileInvitation(self
, message
, info
):
1957 """ helper method for checkMessage """
1958 if not info
.get('Application-GUID', '').upper() == MSN_MSNFTP_GUID
: return 0
1960 cookie
= info
['Invitation-Cookie']
1961 filename
= info
['Application-File']
1962 filesize
= int(info
['Application-FileSize'])
1963 connectivity
= (info
.get('Connectivity').lower() == 'y')
1965 log
.msg('Received munged file transfer request ... ignoring.')
1968 self
.gotSendRequest(msnft
.MSNFTP_Receive(filename
, filesize
, message
.userHandle
, cookie
, connectivity
, self
))
1971 def _checkP2PMessage(self
, message
, cTypes
):
1972 """ helper method for msnslp messages (file transfer & avatars) """
1973 packet
= message
.message
1974 binaryFields
= msnp2p
.BinaryFields(packet
=packet
)
1975 if binaryFields
[0] != 0:
1976 slpLink
= self
.slpLinks
[binaryFields
[0]]
1977 if slpLink
.remoteUser
== message
.userHandle
:
1978 slpLink
.handlePacket(packet
)
1979 elif binaryFields
[5] == BinaryFields
.ACK
or binaryFields
[5] == BinaryFields
.BYEGOT
:
1980 pass # Ignore the ACKs
1982 slpMessage
= msnp2p
.MSNSLPMessage(packet
)
1984 if slpMessage
.method
== "INVITE":
1985 if slpMessage
.euf_guid
== MSN_MSNFTP_GUID
:
1986 slpLink
= msnp2p
.SLPLink_Receive(slpMessage
.fro
, slpMessage
.sessionID
, slpMessage
.sessionGuid
)
1987 context
= msnp2p
.FileContext(slpMessage
.context
)
1988 fileReceive
= msnft
.MSNP2P_Receive(context
.filename
, context
.filesize
, slpMessage
.fro
, self
)
1989 elif slpMessage
.euf_guid
== MSN_AVATAR_GUID
:
1990 slpLink
= msnp2p
.SLPLink_Send(slpMessage
.fro
, slpMessage
.sessionID
, slpMessage
.sessionGuid
)
1991 self
._sendMSNSLPResponse
(slpLink
, "200 OK")
1993 self
.slpLinks
[slpMessage
.sessionID
] = slpLink
1995 if slpMessage
.status
!= "200":
1996 for slpLink
in self
.slpLinks
:
1997 if slpLink
.sessionGuid
== slpMessage
.sessionGuid
:
1998 del self
.slpLinks
[slpLink
.sessionID
]
1999 if slpMessage
.method
!= "BYE":
2000 # Must be an error. If its a file transfer we need to signal that it failed
2001 slpLink
.transferError()
2003 slpLink
= self
.slpLinks
[slpMessage
.sessionID
]
2004 slpLink
.transferReady()
2006 # Always need to ACK these packets if we can
2007 self
._sendP
2PACK
(self
, slpLink
, binaryHeaders
)
2011 def _sendP2PACK(self
, slpLink
, ackHeaders
):
2012 binaryFields
= msnp2p
.BinaryFields()
2013 binaryFields
[1] = slpLink
.nextBaseID()
2014 binaryFields
[3] = ackHeaders
[3]
2015 binaryFields
[5] = BinaryFields
.ACK
2016 binaryFields
[6] = ackHeaders
[1]
2017 binaryFields
[7] = ackHeaders
[6]
2018 binaryFields
[8] = ackHeaders
[3]
2019 self
._sendP
2PMessage
(binaryFields
, "")
2021 def _sendMSNSLPInvite(self
, slpLink
, guid
, context
):
2022 msg
= msnp2p
.MSNSLP_Message()
2023 msg
.create(method
="INVITE", to
=slpLink
.remoteUser
, fro
=self
.userHandle
, cseq
=0, sessionGuid
=slpLink
.sessionGuid
)
2024 msg
.setData(sessionID
=slpLink
.sessionID
, appID
="1", guid
=guid
, context
=msnp2p
.b64enc(context
))
2025 self
._sendMSNSLPMessage
(slpLink
, msg
)
2027 def _sendMSNSLPResponse(self
, slpLink
, response
):
2028 msg
= msnp2p
.MSNSLPMessage()
2029 msg
.create(status
=response
, to
=slpLink
.remoteUser
, fro
=self
.userHandle
, cseq
=1, sessionGuid
=slpLink
.sessionGuid
)
2030 msg
.setData(sessionID
=slpLink
.sessionID
)
2031 self
._sendMSNSLPMessage
(slpLink
, msg
)
2033 def _sendMSNSLPMessage(self
, slpLink
, msnSlpMessage
):
2035 binaryFields
= msnp2p
.BinaryFields()
2036 binaryFields
[1] = slpLink
.nextBaseID()
2037 binaryFields
[3] = len(msgStr
)
2038 binaryFields
[4] = binaryFields
[3]
2039 binaryFields
[6] = random
.randint(0, sys
.maxint
)
2040 self
._sendP
2PMessage
(binaryFields
, msgStr
)
2042 def _sendP2PMessage(self
, binaryFields
, msgStr
):
2043 packet
= binaryFields
.packHeaders() + msgStr
+ binaryFields
.packFooter()
2045 message
= MSNMessage(message
=packet
)
2046 message
.setHeader("Content-Type", "application/x-msnmsgrp2p")
2047 message
.setHeader("P2P-Dest", handler
.to
)
2048 message
.ack
= MSNMessage
.MESSAGE_ACK_FAT
2049 self
.sendMessage(message
)
2051 def checkMessage(self
, message
):
2053 hook for detecting any notification type messages
2054 (e.g. file transfer)
2056 cTypes
= [s
.lstrip() for s
in message
.getHeader('Content-Type').split(';')]
2057 if self
._checkTyping
(message
, cTypes
): return 0
2058 if 'text/x-msmsgsinvite' in cTypes
:
2059 # header like info is sent as part of the message body.
2061 for line
in message
.message
.split('\r\n'):
2063 key
, val
= line
.split(':')
2064 info
[key
] = val
.lstrip()
2065 except ValueError: continue
2066 if self
._checkFileInvitation
(message
, info
): return 0
2067 if 'application/x-msnmsgrp2p' in cTypes
:
2068 if self
._checkP
2PMessage
(message
, cTypes
): return 0
2072 def handle_USR(self
, params
):
2073 checkParamLen(len(params
), 4, 'USR')
2074 if params
[1] == "OK":
2078 def handle_CAL(self
, params
):
2079 checkParamLen(len(params
), 3, 'CAL')
2081 if params
[1].upper() == "RINGING":
2082 self
._fireCallback
(id, int(params
[2])) # session ID as parameter
2085 def handle_JOI(self
, params
):
2086 checkParamLen(len(params
), 2, 'JOI')
2087 self
.userJoined(params
[0], unquote(params
[1]))
2089 # users participating in the current chat
2090 def handle_IRO(self
, params
):
2091 checkParamLen(len(params
), 5, 'IRO')
2092 self
.pendingUsers
[params
[3]] = unquote(params
[4])
2093 if params
[1] == params
[2]:
2094 self
.gotChattingUsers(self
.pendingUsers
)
2095 self
.pendingUsers
= {}
2097 # finished listing users
2098 def handle_ANS(self
, params
):
2099 checkParamLen(len(params
), 2, 'ANS')
2100 if params
[1] == "OK":
2103 def handle_ACK(self
, params
):
2104 checkParamLen(len(params
), 1, 'ACK')
2105 self
._fireCallback
(int(params
[0]), None)
2107 def handle_NAK(self
, params
):
2108 checkParamLen(len(params
), 1, 'NAK')
2109 self
._fireCallback
(int(params
[0]), None)
2111 def handle_BYE(self
, params
):
2112 #checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
2113 self
.userLeft(params
[0])
2119 called when all login details have been negotiated.
2120 Messages can now be sent, or new users invited.
2124 def gotChattingUsers(self
, users
):
2126 called after connecting to an existing chat session.
2128 @param users: A dict mapping user handles to screen names
2129 (current users taking part in the conversation)
2133 def userJoined(self
, userHandle
, screenName
):
2135 called when a user has joined the conversation.
2137 @param userHandle: the user handle (passport) of the user
2138 @param screenName: the screen name of the user
2142 def userLeft(self
, userHandle
):
2144 called when a user has left the conversation.
2146 @param userHandle: the user handle (passport) of the user.
2150 def gotMessage(self
, message
):
2152 called when we receive a message.
2154 @param message: the associated MSNMessage object
2158 def gotAvatarImage(self
, userHandle
, image
):
2160 called when we receive an avatar from a contact
2162 @param userHandle: the person who's avatar we have got
2163 @param image: the avatar image
2167 def gotSendRequest(self
, fileReceive
):
2169 called when we receive a file send request from a contact
2171 @param fileReceive: msnft.MSNFTReceive_Base instance
2175 def userTyping(self
, message
):
2177 called when we receive the special type of message notifying
2178 us that a user is typing a message.
2180 @param message: the associated MSNMessage object
2186 def inviteUser(self
, userHandle
):
2188 used to invite a user to the current switchboard server.
2190 @param userHandle: the user handle (passport) of the desired user.
2192 @return: A Deferred, the callback for which will be called
2193 when the server notifies us that the user has indeed
2194 been invited. The callback argument will be a tuple
2195 with 1 element, the sessionID given to the invited user.
2196 I'm not sure if this is useful or not.
2199 id, d
= self
._createIDMapping
()
2200 self
.sendLine("CAL %s %s" % (id, userHandle
))
2203 def sendMessage(self
, message
):
2205 used to send a message.
2207 @param message: the corresponding MSNMessage object.
2209 @return: Depending on the value of message.ack.
2210 If set to MSNMessage.MESSAGE_ACK or
2211 MSNMessage.MESSAGE_NACK a Deferred will be returned,
2212 the callback for which will be fired when an ACK or
2213 NACK is received - the callback argument will be
2214 (None,). If set to MSNMessage.MESSAGE_ACK_NONE then
2215 the return value is None.
2218 if message
.ack
not in ('A','N','D'): id, d
= self
._nextTransactionID
(), None
2219 else: id, d
= self
._createIDMapping
()
2220 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
2221 self
.sendLine("MSG %s %s %s" % (id, message
.ack
, message
.length
))
2222 # apparently order matters with at least MIME-Version and Content-Type
2223 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
2224 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
2225 # send the rest of the headers
2226 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
2227 self
.sendLine("%s: %s" % (header
[0], header
[1]))
2228 self
.transport
.write(CR
+LF
)
2229 self
.transport
.write(message
.message
)
2230 if MESSAGEDEBUG
: log
.msg(message
.message
)
2233 def sendAvatarRequest(self
, userHandle
, msnobj
):
2236 def sendTypingNotification(self
):
2238 used to send a typing notification. Upon receiving this
2239 message the official client will display a 'user is typing'
2240 message to all other users in the chat session for 10 seconds.
2241 The official client sends one of these every 5 seconds (I think)
2242 as long as you continue to type.
2245 m
.ack
= m
.MESSAGE_ACK_NONE
2246 m
.setHeader('Content-Type', 'text/x-msmsgscontrol')
2247 m
.setHeader('TypingUser', self
.userHandle
)
2251 def sendFileInvitation(self
, fileName
, fileSize
):
2253 send an notification that we want to send a file.
2255 @param fileName: the file name
2256 @param fileSize: the file size
2258 @return: A Deferred, the callback of which will be fired
2259 when the user responds to this invitation with an
2260 appropriate message. The callback argument will be
2261 a tuple with 3 elements, the first being 1 or 0
2262 depending on whether they accepted the transfer
2263 (1=yes, 0=no), the second being an invitation cookie
2264 to identify your follow-up responses and the third being
2265 the message 'info' which is a dict of information they
2266 sent in their reply (this doesn't really need to be used).
2267 If you wish to proceed with the transfer see the
2268 sendTransferInfo method.
2270 cookie
= self
._newInvitationCookie
()
2273 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2274 m
.message
+= 'Application-Name: File Transfer\r\n'
2275 m
.message
+= 'Application-GUID: %s\r\n' % MSN_MSNFTP_GUID
2276 m
.message
+= 'Invitation-Command: INVITE\r\n'
2277 m
.message
+= 'Invitation-Cookie: %s\r\n' % str(cookie
)
2278 m
.message
+= 'Application-File: %s\r\n' % fileName
2279 m
.message
+= 'Application-FileSize: %s\r\n\r\n' % str(fileSize
)
2280 m
.ack
= m
.MESSAGE_ACK_NONE
2282 self
.cookies
['iCookies'][cookie
] = (d
, m
)
2285 def sendTransferInfo(self
, accept
, iCookie
, authCookie
, ip
, port
):
2287 send information relating to a file transfer session.
2289 @param accept: whether or not to go ahead with the transfer
2291 @param iCookie: the invitation cookie of previous replies
2292 relating to this transfer
2293 @param authCookie: the authentication cookie obtained from
2294 an FileSend instance
2296 @param port: the port on which an FileSend protocol is listening.
2299 m
.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2300 m
.message
+= 'Invitation-Command: %s\r\n' % (accept
and 'ACCEPT' or 'CANCEL')
2301 m
.message
+= 'Invitation-Cookie: %s\r\n' % iCookie
2302 m
.message
+= 'IP-Address: %s\r\n' % ip
2303 m
.message
+= 'Port: %s\r\n' % port
2304 m
.message
+= 'AuthCookie: %s\r\n' % authCookie
2306 m
.ack
= m
.MESSAGE_NACK
2310 # mapping of error codes to error messages
2313 200 : "Syntax error",
2314 201 : "Invalid parameter",
2315 205 : "Invalid user",
2316 206 : "Domain name missing",
2317 207 : "Already logged in",
2318 208 : "Invalid username",
2319 209 : "Invalid screen name",
2320 210 : "User list full",
2321 215 : "User already there",
2322 216 : "User already on list",
2323 217 : "User not online",
2324 218 : "Already in mode",
2325 219 : "User is in the opposite list",
2326 223 : "Too many groups",
2327 224 : "Invalid group",
2328 225 : "User not in group",
2329 229 : "Group name too long",
2330 230 : "Cannot remove group 0",
2331 231 : "Invalid group",
2332 280 : "Switchboard failed",
2333 281 : "Transfer to switchboard failed",
2335 300 : "Required field missing",
2336 301 : "Too many FND responses",
2337 302 : "Not logged in",
2339 402 : "Error accessing contact list",
2340 403 : "Error accessing contact list",
2342 500 : "Internal server error",
2343 501 : "Database server error",
2344 502 : "Command disabled",
2345 510 : "File operation failed",
2346 520 : "Memory allocation failed",
2347 540 : "Wrong CHL value sent to server",
2349 600 : "Server is busy",
2350 601 : "Server is unavaliable",
2351 602 : "Peer nameserver is down",
2352 603 : "Database connection failed",
2353 604 : "Server is going down",
2354 605 : "Server unavailable",
2356 707 : "Could not create connection",
2357 710 : "Invalid CVR parameters",
2358 711 : "Write is blocking",
2359 712 : "Session is overloaded",
2360 713 : "Too many active users",
2361 714 : "Too many sessions",
2362 715 : "Not expected",
2363 717 : "Bad friend file",
2364 731 : "Not expected",
2366 800 : "Requests too rapid",
2368 910 : "Server too busy",
2369 911 : "Authentication failed",
2370 912 : "Server too busy",
2371 913 : "Not allowed when offline",
2372 914 : "Server too busy",
2373 915 : "Server too busy",
2374 916 : "Server too busy",
2375 917 : "Server too busy",
2376 918 : "Server too busy",
2377 919 : "Server too busy",
2378 920 : "Not accepting new users",
2379 921 : "Server too busy",
2380 922 : "Server too busy",
2381 923 : "No parent consent",
2382 924 : "Passport account not yet verified"
2386 # mapping of status codes to readable status format
2389 STATUS_ONLINE
: "Online",
2390 STATUS_OFFLINE
: "Offline",
2391 STATUS_HIDDEN
: "Appear Offline",
2392 STATUS_IDLE
: "Idle",
2393 STATUS_AWAY
: "Away",
2394 STATUS_BUSY
: "Busy",
2395 STATUS_BRB
: "Be Right Back",
2396 STATUS_PHONE
: "On the Phone",
2397 STATUS_LUNCH
: "Out to Lunch"
2401 # mapping of list ids to list codes
2404 FORWARD_LIST
: 'fl',
2407 REVERSE_LIST
: 'rl',
2412 # mapping of list codes to list ids
2414 for id,code
in listIDToCode
.items():
2415 listCodeToID
[code
] = id