1
# -*- test-case-name: twisted.words.test -*-
2
# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
MSNP8 Protocol (client only) - semi-experimental
8
This module provides support for clients using the MSN Protocol (MSNP8).
9
There are basically 3 servers involved in any MSN session:
13
The DispatchClient class handles connections to the
14
dispatch server, which basically delegates users to a
15
suitable notification server.
17
You will want to subclass this and handle the gotNotificationReferral
20
I{Notification Server}
22
The NotificationClient class handles connections to the
23
notification server, which acts as a session server
24
(state updates, message negotiation etc...)
28
The SwitchboardClient handles connections to switchboard
29
servers which are used to conduct conversations with other users.
31
There are also two classes (FileSend and FileReceive) used
34
Clients handle events in two ways.
36
- each client request requiring a response will return a Deferred,
37
the callback for same will be fired when the server sends the
39
- Events which are not in response to any client request have
40
respective methods which should be overridden and handled in
43
Most client request callbacks require more than one argument,
44
and since Deferreds can only pass the callback one result,
45
most of the time the callback argument will be a tuple of
46
values (documented in the respective request method).
47
To make reading/writing code easier, callbacks can be defined in
48
a number of ways to handle this 'cleanly'. One way would be to
49
define methods like: def callBack(self, (arg1, arg2, arg)): ...
50
another way would be to do something like:
51
d.addCallback(lambda result: myCallback(*result)).
53
If the server sends an error response to a client request,
54
the errback of the corresponding Deferred will be called,
55
the argument being the corresponding error code.
58
Due to the lack of an official spec for MSNP8, extra checking
59
than may be deemed necessary often takes place considering the
60
server is never 'wrong'. Thus, if gotBadLine (in any of the 3
61
main clients) is called, or an MSNProtocolError is raised, it's
62
probably a good idea to submit a bug report. ;)
63
Use of this module requires that PyOpenSSL is installed.
67
- check message hooks with invalid x-msgsinvite messages.
74
import types, operator, os
75
from random import randint
76
from urllib import quote, unquote
78
from twisted.python import failure, log
79
from twisted.python.hashlib import md5
80
from twisted.internet import reactor
81
from twisted.internet.defer import Deferred
82
from twisted.internet.protocol import ClientFactory
84
from twisted.internet.ssl import ClientContextFactory
86
ClientContextFactory = None
87
from twisted.protocols.basic import LineReceiver
88
from twisted.web.http import HTTPClient
91
MSN_PROTOCOL_VERSION = "MSNP8 CVR0" # protocol version
92
MSN_PORT = 1863 # default dispatch server port
93
MSN_MAX_MESSAGE = 1664 # max message length
94
MSN_CHALLENGE_STR = "Q1P7W2E4J9R8U3S5" # used for server challenges
95
MSN_CVR_STR = "0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS" # :(
115
STATUS_ONLINE = 'NLN'
116
STATUS_OFFLINE = 'FLN'
117
STATUS_HIDDEN = 'HDN'
128
def checkParamLen(num, expected, cmd, error=None):
130
error = "Invalid Number of Parameters for %s" % cmd
132
raise MSNProtocolError, error
134
def _parseHeader(h, v):
136
Split a certin number of known
137
header values with the format:
138
field1=val,field2=val,field3=val into
139
a dict mapping fields to values.
140
@param h: the header's key
141
@param v: the header's value as a string
144
if h in ('passporturls','authentication-info','www-authenticate'):
145
v = v.replace('Passport1.4','').lstrip()
147
for fieldPair in v.split(','):
149
field,value = fieldPair.split('=',1)
150
fields[field.lower()] = value
152
fields[field.lower()] = ''
157
def _parsePrimitiveHost(host):
159
h,p = host.replace('https://','').split('/',1)
163
def _login(userHandle, passwd, nexusServer, cached=0, authData=''):
165
This function is used internally and should not ever be called
169
def _cb(server, auth):
170
loginFac = ClientFactory()
171
loginFac.protocol = lambda : PassportLogin(cb, userHandle, passwd, server, auth)
172
reactor.connectSSL(_parsePrimitiveHost(server)[0], 443, loginFac, ClientContextFactory())
175
_cb(nexusServer, authData)
177
fac = ClientFactory()
179
d.addCallbacks(_cb, callbackArgs=(authData,))
180
d.addErrback(lambda f: cb.errback(f))
181
fac.protocol = lambda : PassportNexus(d, nexusServer)
182
reactor.connectSSL(_parsePrimitiveHost(nexusServer)[0], 443, fac, ClientContextFactory())
186
class PassportNexus(HTTPClient):
189
Used to obtain the URL of a valid passport
192
This class is used internally and should
193
not be instantiated directly -- that is,
194
The passport logging in process is handled
195
transparantly by NotificationClient.
198
def __init__(self, deferred, host):
199
self.deferred = deferred
200
self.host, self.path = _parsePrimitiveHost(host)
202
def connectionMade(self):
203
HTTPClient.connectionMade(self)
204
self.sendCommand('GET', self.path)
205
self.sendHeader('Host', self.host)
209
def handleHeader(self, header, value):
211
self.headers[h] = _parseHeader(h, value)
213
def handleEndHeaders(self):
215
self.transport.loseConnection()
216
if not self.headers.has_key('passporturls') or not self.headers['passporturls'].has_key('dalogin'):
217
self.deferred.errback(failure.Failure(failure.DefaultException("Invalid Nexus Reply")))
218
self.deferred.callback('https://' + self.headers['passporturls']['dalogin'])
220
def handleResponse(self, r):
223
class PassportLogin(HTTPClient):
225
This class is used internally to obtain
226
a login ticket from a passport HTTPS
227
server -- it should not be used directly.
232
def __init__(self, deferred, userHandle, passwd, host, authData):
233
self.deferred = deferred
234
self.userHandle = userHandle
236
self.authData = authData
237
self.host, self.path = _parsePrimitiveHost(host)
239
def connectionMade(self):
240
self.sendCommand('GET', self.path)
241
self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
242
'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
243
self.sendHeader('Host', self.host)
247
def handleHeader(self, header, value):
249
self.headers[h] = _parseHeader(h, value)
251
def handleEndHeaders(self):
254
self._finished = 1 # I think we need this because of HTTPClient
256
self.transport.loseConnection()
257
authHeader = 'authentication-info'
258
_interHeader = 'www-authenticate'
259
if self.headers.has_key(_interHeader):
260
authHeader = _interHeader
262
info = self.headers[authHeader]
263
status = info['da-status']
264
handler = getattr(self, 'login_%s' % (status,), None)
270
self.deferred.errback(failure.Failure(e))
272
def handleResponse(self, r):
275
def login_success(self, info):
276
ticket = info['from-pp']
277
ticket = ticket[1:len(ticket)-1]
278
self.deferred.callback((LOGIN_SUCCESS, ticket))
280
def login_failed(self, info):
281
self.deferred.callback((LOGIN_FAILURE, unquote(info['cbtxt'])))
283
def login_redir(self, info):
284
self.deferred.callback((LOGIN_REDIRECT, self.headers['location'], self.authData))
287
class MSNProtocolError(Exception):
289
This Exception is basically used for debugging
290
purposes, as the official MSN server should never
291
send anything _wrong_ and nobody in their right
292
mind would run their B{own} MSN server.
293
If it is raised by default command handlers
294
(handle_BLAH) the error will be logged.
299
class MSNCommandFailed(Exception):
301
The server said that the command failed.
304
def __init__(self, errorCode):
305
self.errorCode = errorCode
308
return ("Command failed: %s (error code %d)"
309
% (errorCodes[self.errorCode], self.errorCode))
314
I am the class used to represent an 'instant' message.
316
@ivar userHandle: The user handle (passport) of the sender
317
(this is only used when receiving a message)
318
@ivar screenName: The screen name of the sender (this is only used
319
when receiving a message)
320
@ivar message: The message
321
@ivar headers: The message headers
323
@ivar length: The message length (including headers and line endings)
324
@ivar ack: This variable is used to tell the server how to respond
325
once the message has been sent. If set to MESSAGE_ACK
326
(default) the server will respond with an ACK upon receiving
327
the message, if set to MESSAGE_NACK the server will respond
328
with a NACK upon failure to receive the message.
329
If set to MESSAGE_ACK_NONE the server will do nothing.
330
This is relevant for the return value of
331
SwitchboardClient.sendMessage (which will return
332
a Deferred if ack is set to either MESSAGE_ACK or MESSAGE_NACK
333
and will fire when the respective ACK or NACK is received).
334
If set to MESSAGE_ACK_NONE sendMessage will return None.
338
MESSAGE_ACK_NONE = 'U'
342
def __init__(self, length=0, userHandle="", screenName="", message=""):
343
self.userHandle = userHandle
344
self.screenName = screenName
345
self.message = message
346
self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
350
def _calcMessageLen(self):
352
used to calculte the number to send
353
as the message length when sending a message.
355
return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
357
def setHeader(self, header, value):
358
""" set the desired header """
359
self.headers[header] = value
361
def getHeader(self, header):
363
get the desired header value
364
@raise KeyError: if no such header exists.
366
return self.headers[header]
368
def hasHeader(self, header):
369
""" check to see if the desired header exists """
370
return self.headers.has_key(header)
372
def getMessage(self):
373
""" return the message - not including headers """
376
def setMessage(self, message):
377
""" set the message text """
378
self.message = message
383
This class represents a contact (user).
385
@ivar userHandle: The contact's user handle (passport).
386
@ivar screenName: The contact's screen name.
387
@ivar groups: A list of all the group IDs which this
389
@ivar lists: An integer representing the sum of all lists
390
that this contact belongs to.
391
@ivar status: The contact's status code.
392
@type status: str if contact's status is known, None otherwise.
394
@ivar homePhone: The contact's home phone number.
395
@type homePhone: str if known, otherwise None.
396
@ivar workPhone: The contact's work phone number.
397
@type workPhone: str if known, otherwise None.
398
@ivar mobilePhone: The contact's mobile phone number.
399
@type mobilePhone: str if known, otherwise None.
400
@ivar hasPager: Whether or not this user has a mobile pager
404
def __init__(self, userHandle="", screenName="", lists=0, groups=[], status=None):
405
self.userHandle = userHandle
406
self.screenName = screenName
408
self.groups = [] # if applicable
409
self.status = status # current status
412
self.homePhone = None
413
self.workPhone = None
414
self.mobilePhone = None
417
def setPhone(self, phoneType, value):
419
set phone numbers/values for this specific user.
420
for phoneType check the *_PHONE constants and HAS_PAGER
423
t = phoneType.upper()
425
self.homePhone = value
426
elif t == WORK_PHONE:
427
self.workPhone = value
428
elif t == MOBILE_PHONE:
429
self.mobilePhone = value
431
self.hasPager = value
433
raise ValueError, "Invalid Phone Type"
435
def addToList(self, listType):
437
Update the lists attribute to
438
reflect being part of the
441
self.lists |= listType
443
def removeFromList(self, listType):
445
Update the lists attribute to
446
reflect being removed from the
449
self.lists ^= listType
451
class MSNContactList:
453
This class represents a basic MSN contact list.
455
@ivar contacts: All contacts on my various lists
456
@type contacts: dict (mapping user handles to MSNContact objects)
457
@ivar version: The current contact list version (used for list syncing)
458
@ivar groups: a mapping of group ids to group names
459
(groups can only exist on the forward list)
463
This is used only for storage and doesn't effect the
464
server's contact list.
474
def _getContactsFromList(self, listType):
476
Obtain all contacts which belong
477
to the given list type.
479
return dict([(uH,obj) for uH,obj in self.contacts.items() if obj.lists & listType])
481
def addContact(self, contact):
485
self.contacts[contact.userHandle] = contact
487
def remContact(self, userHandle):
492
del self.contacts[userHandle]
496
def getContact(self, userHandle):
498
Obtain the MSNContact object
499
associated with the given
501
@return: the MSNContact object if
502
the user exists, or None.
505
return self.contacts[userHandle]
509
def getBlockedContacts(self):
511
Obtain all the contacts on my block list
513
return self._getContactsFromList(BLOCK_LIST)
515
def getAuthorizedContacts(self):
517
Obtain all the contacts on my auth list.
518
(These are contacts which I have verified
519
can view my state changes).
521
return self._getContactsFromList(ALLOW_LIST)
523
def getReverseContacts(self):
525
Get all contacts on my reverse list.
526
(These are contacts which have added me
527
to their forward list).
529
return self._getContactsFromList(REVERSE_LIST)
531
def getContacts(self):
533
Get all contacts on my forward list.
534
(These are the contacts which I have added
537
return self._getContactsFromList(FORWARD_LIST)
539
def setGroup(self, id, name):
541
Keep a mapping from the given id
544
self.groups[id] = name
546
def remGroup(self, id):
548
Removed the stored group
549
mapping for the given id.
555
for c in self.contacts:
560
class MSNEventBase(LineReceiver):
562
This class provides support for handling / dispatching events and is the
563
base class of the three main client protocols (DispatchClient,
564
NotificationClient, SwitchboardClient)
568
self.ids = {} # mapping of ids to Deferreds
572
self.currentMessage = None
574
def connectionLost(self, reason):
578
def connectionMade(self):
581
def _fireCallback(self, id, *args):
583
Fire the callback for the given id
584
if one exists and return 1, else return false
586
if self.ids.has_key(id):
587
self.ids[id][0].callback(args)
592
def _nextTransactionID(self):
593
""" return a usable transaction ID """
595
if self.currentID > 1000:
597
return self.currentID
599
def _createIDMapping(self, data=None):
601
return a unique transaction ID that is mapped internally to a
602
deferred .. also store arbitrary data if it is needed
604
id = self._nextTransactionID()
606
self.ids[id] = (d, data)
609
def checkMessage(self, message):
611
process received messages to check for file invitations and
612
typing notifications and other control type messages
614
raise NotImplementedError
616
def lineReceived(self, line):
617
if self.currentMessage:
618
self.currentMessage.readPos += len(line+CR+LF)
621
if self.currentMessage.readPos == self.currentMessage.length:
622
self.rawDataReceived("") # :(
625
header, value = line.split(':')
627
raise MSNProtocolError, "Invalid Message Header"
628
self.currentMessage.setHeader(header, unquote(value).lstrip())
631
cmd, params = line.split(' ', 1)
633
raise MSNProtocolError, "Invalid Message, %s" % repr(line)
636
raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
639
id = int(params.split()[0])
641
self.ids[id][0].errback(MSNCommandFailed(errorCode))
644
else: # we received an error which doesn't map to a sent command
645
self.gotError(errorCode)
648
handler = getattr(self, "handle_%s" % cmd.upper(), None)
651
handler(params.split())
652
except MSNProtocolError, why:
653
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 += data
669
del self.currentMessage.readPos
670
m = self.currentMessage
671
self.currentMessage = None
672
self.setLineMode(extra)
673
if not self.checkMessage(m):
677
### protocol command handlers - no need to override these.
679
def handle_MSG(self, params):
680
checkParamLen(len(params), 3, 'MSG')
682
messageLen = int(params[2])
684
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]))
713
class DispatchClient(MSNEventBase):
715
This class provides support for clients connecting to the dispatch server
716
@ivar userHandle: your user handle (passport) needed before connecting.
719
# eventually this may become an attribute of the
723
def connectionMade(self):
724
MSNEventBase.connectionMade(self)
725
self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
727
### protocol command handlers ( there is no need to override these )
729
def handle_VER(self, params):
730
id = self._nextTransactionID()
731
self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
733
def handle_CVR(self, params):
734
self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
736
def handle_XFR(self, params):
738
raise MSNProtocolError, "Invalid number of parameters for XFR"
739
id, refType, addr = params[:3]
740
# was addr a host:port pair?
742
host, port = addr.split(':')
747
self.gotNotificationReferral(host, int(port))
751
def gotNotificationReferral(self, host, port):
753
called when we get a referral to the notification server.
755
@param host: the notification server's hostname
756
@param port: the port to connect to
761
class NotificationClient(MSNEventBase):
763
This class provides support for clients connecting
764
to the notification server.
767
factory = None # sssh pychecker
769
def __init__(self, currentID=0):
770
MSNEventBase.__init__(self)
771
self.currentID = currentID
772
self._state = ['DISCONNECTED', {}]
774
def _setState(self, state):
775
self._state[0] = state
778
return self._state[0]
780
def _getStateData(self, key):
781
return self._state[1][key]
783
def _setStateData(self, key, value):
784
self._state[1][key] = value
786
def _remStateData(self, *args):
788
del self._state[1][key]
790
def connectionMade(self):
791
MSNEventBase.connectionMade(self)
792
self._setState('CONNECTED')
793
self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
795
def connectionLost(self, reason):
796
self._setState('DISCONNECTED')
798
MSNEventBase.connectionLost(self, reason)
800
def checkMessage(self, message):
801
""" hook used for detecting specific notification messages """
802
cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
803
if 'text/x-msmsgsprofile' in cTypes:
804
self.gotProfile(message)
808
### protocol command handlers - no need to override these
810
def handle_VER(self, params):
811
id = self._nextTransactionID()
812
self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.factory.userHandle))
814
def handle_CVR(self, params):
815
self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
817
def handle_USR(self, params):
818
if len(params) != 4 and len(params) != 6:
819
raise MSNProtocolError, "Invalid Number of Parameters for USR"
821
mechanism = params[1]
822
if mechanism == "OK":
823
self.loggedIn(params[2], unquote(params[3]), int(params[4]))
824
elif params[2].upper() == "S":
825
# we need to obtain auth from a passport server
827
d = _login(f.userHandle, f.password, f.passportServer, authData=params[3])
828
d.addCallback(self._passportLogin)
829
d.addErrback(self._passportError)
831
def _passportLogin(self, result):
832
if result[0] == LOGIN_REDIRECT:
833
d = _login(self.factory.userHandle, self.factory.password,
834
result[1], cached=1, authData=result[2])
835
d.addCallback(self._passportLogin)
836
d.addErrback(self._passportError)
837
elif result[0] == LOGIN_SUCCESS:
838
self.sendLine("USR %s TWN S %s" % (self._nextTransactionID(), result[1]))
839
elif result[0] == LOGIN_FAILURE:
840
self.loginFailure(result[1])
842
def _passportError(self, failure):
843
self.loginFailure("Exception while authenticating: %s" % failure)
845
def handle_CHG(self, params):
846
checkParamLen(len(params), 3, 'CHG')
848
if not self._fireCallback(id, params[1]):
849
self.statusChanged(params[1])
851
def handle_ILN(self, params):
852
checkParamLen(len(params), 5, 'ILN')
853
self.gotContactStatus(params[1], params[2], unquote(params[3]))
855
def handle_CHL(self, params):
856
checkParamLen(len(params), 2, 'CHL')
857
self.sendLine("QRY %s msmsgs@msnmsgr.com 32" % self._nextTransactionID())
858
self.transport.write(md5(params[1] + MSN_CHALLENGE_STR).hexdigest())
860
def handle_QRY(self, params):
863
def handle_NLN(self, params):
864
checkParamLen(len(params), 4, 'NLN')
865
self.contactStatusChanged(params[0], params[1], unquote(params[2]))
867
def handle_FLN(self, params):
868
checkParamLen(len(params), 1, 'FLN')
869
self.contactOffline(params[0])
871
def handle_LST(self, params):
872
# support no longer exists for manually
873
# requesting lists - why do I feel cleaner now?
874
if self._getState() != 'SYNC':
876
contact = MSNContact(userHandle=params[0], screenName=unquote(params[1]),
877
lists=int(params[2]))
878
if contact.lists & FORWARD_LIST:
879
contact.groups.extend(map(int, params[3].split(',')))
880
self._getStateData('list').addContact(contact)
881
self._setStateData('last_contact', contact)
882
sofar = self._getStateData('lst_sofar') + 1
883
if sofar == self._getStateData('lst_reply'):
884
# this is the best place to determine that
885
# a syn realy has finished - msn _may_ send
886
# BPR information for the last contact
887
# which is unfortunate because it means
888
# that the real end of a syn is non-deterministic.
889
# to handle this we'll keep 'last_contact' hanging
890
# around in the state data and update it if we need
892
self._setState('SESSION')
893
contacts = self._getStateData('list')
894
phone = self._getStateData('phone')
895
id = self._getStateData('synid')
896
self._remStateData('lst_reply', 'lsg_reply', 'lst_sofar', 'phone', 'synid', 'list')
897
self._fireCallback(id, contacts, phone)
899
self._setStateData('lst_sofar',sofar)
901
def handle_BLP(self, params):
902
# check to see if this is in response to a SYN
903
if self._getState() == 'SYNC':
904
self._getStateData('list').privacy = listCodeToID[params[0].lower()]
907
self._fireCallback(id, int(params[1]), listCodeToID[params[2].lower()])
909
def handle_GTC(self, params):
910
# check to see if this is in response to a SYN
911
if self._getState() == 'SYNC':
912
if params[0].lower() == "a":
913
self._getStateData('list').autoAdd = 0
914
elif params[0].lower() == "n":
915
self._getStateData('list').autoAdd = 1
917
raise MSNProtocolError, "Invalid Paramater for GTC" # debug
920
if params[1].lower() == "a":
921
self._fireCallback(id, 0)
922
elif params[1].lower() == "n":
923
self._fireCallback(id, 1)
925
raise MSNProtocolError, "Invalid Paramater for GTC" # debug
927
def handle_SYN(self, params):
930
self._setState('SESSION')
931
self._fireCallback(id, None, None)
933
contacts = MSNContactList()
934
contacts.version = int(params[1])
935
self._setStateData('list', contacts)
936
self._setStateData('lst_reply', int(params[2]))
937
self._setStateData('lsg_reply', int(params[3]))
938
self._setStateData('lst_sofar', 0)
939
self._setStateData('phone', [])
941
def handle_LSG(self, params):
942
if self._getState() == 'SYNC':
943
self._getStateData('list').groups[int(params[0])] = unquote(params[1])
945
# Please see the comment above the requestListGroups / requestList methods
946
# regarding support for this
949
# self._getStateData('groups').append((int(params[4]), unquote(params[5])))
950
# if params[3] == params[4]: # this was the last group
951
# self._fireCallback(int(params[0]), self._getStateData('groups'), int(params[1]))
952
# self._remStateData('groups')
954
def handle_PRP(self, params):
955
if self._getState() == 'SYNC':
956
self._getStateData('phone').append((params[0], unquote(params[1])))
958
self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
960
def handle_BPR(self, params):
961
numParams = len(params)
962
if numParams == 2: # part of a syn
963
self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
965
self.gotPhoneNumber(int(params[0]), params[1], params[2], unquote(params[3]))
967
def handle_ADG(self, params):
968
checkParamLen(len(params), 5, 'ADG')
970
if not self._fireCallback(id, int(params[1]), unquote(params[2]), int(params[3])):
971
raise MSNProtocolError, "ADG response does not match up to a request" # debug
973
def handle_RMG(self, params):
974
checkParamLen(len(params), 3, 'RMG')
976
if not self._fireCallback(id, int(params[1]), int(params[2])):
977
raise MSNProtocolError, "RMG response does not match up to a request" # debug
979
def handle_REG(self, params):
980
checkParamLen(len(params), 5, 'REG')
982
if not self._fireCallback(id, int(params[1]), int(params[2]), unquote(params[3])):
983
raise MSNProtocolError, "REG response does not match up to a request" # debug
985
def handle_ADD(self, params):
986
numParams = len(params)
987
if numParams < 5 or params[1].upper() not in ('AL','BL','RL','FL'):
988
raise MSNProtocolError, "Invalid Paramaters for ADD" # debug
990
listType = params[1].lower()
991
listVer = int(params[2])
992
userHandle = params[3]
994
if numParams == 6: # they sent a group id
995
if params[1].upper() != "FL":
996
raise MSNProtocolError, "Only forward list can contain groups" # debug
997
groupID = int(params[5])
998
if not self._fireCallback(id, listCodeToID[listType], userHandle, listVer, groupID):
999
self.userAddedMe(userHandle, unquote(params[4]), listVer)
1001
def handle_REM(self, params):
1002
numParams = len(params)
1003
if numParams < 4 or params[1].upper() not in ('AL','BL','FL','RL'):
1004
raise MSNProtocolError, "Invalid Paramaters for REM" # debug
1006
listType = params[1].lower()
1007
listVer = int(params[2])
1008
userHandle = params[3]
1011
if params[1] != "FL":
1012
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":
1016
self.userRemovedMe(userHandle, listVer)
1018
def handle_REA(self, params):
1019
checkParamLen(len(params), 4, 'REA')
1021
self._fireCallback(id, int(params[1]), unquote(params[3]))
1023
def handle_XFR(self, params):
1024
checkParamLen(len(params), 5, 'XFR')
1026
# check to see if they sent a host/port pair
1028
host, port = params[2].split(':')
1033
if not self._fireCallback(id, host, int(port), params[4]):
1034
raise MSNProtocolError, "Got XFR (referral) that I didn't ask for .. should this happen?" # debug
1036
def handle_RNG(self, params):
1037
checkParamLen(len(params), 6, 'RNG')
1038
# check for host:port pair
1040
host, port = params[1].split(":")
1045
self.gotSwitchboardInvitation(int(params[0]), host, port, params[3], params[4],
1048
def handle_OUT(self, params):
1049
checkParamLen(len(params), 1, 'OUT')
1050
if params[0] == "OTH":
1051
self.multipleLogin()
1052
elif params[0] == "SSD":
1053
self.serverGoingDown()
1055
raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
1059
def loggedIn(self, userHandle, screenName, verified):
1061
Called when the client has logged in.
1062
The default behaviour of this method is to
1063
update the factory with our screenName and
1064
to sync the contact list (factory.contacts).
1065
When this is complete self.listSynchronized
1068
@param userHandle: our userHandle
1069
@param screenName: our screenName
1070
@param verified: 1 if our passport has been (verified), 0 if not.
1071
(i'm not sure of the significace of this)
1074
self.factory.screenName = screenName
1075
if not self.factory.contacts:
1078
listVersion = self.factory.contacts.version
1079
self.syncList(listVersion).addCallback(self.listSynchronized)
1081
def loginFailure(self, message):
1083
Called when the client fails to login.
1085
@param message: a message indicating the problem that was encountered
1089
def gotProfile(self, message):
1091
Called after logging in when the server sends an initial
1092
message with MSN/passport specific profile information
1093
such as country, number of kids, etc.
1094
Check the message headers for the specific values.
1096
@param message: The profile message
1100
def listSynchronized(self, *args):
1102
Lists are now synchronized by default upon logging in, this
1103
method is called after the synchronization has finished
1104
and the factory now has the up-to-date contacts.
1108
def statusChanged(self, statusCode):
1110
Called when our status changes and it isn't in response to
1111
a client command. By default we will update the status
1112
attribute of the factory.
1114
@param statusCode: 3-letter status code
1116
self.factory.status = statusCode
1118
def gotContactStatus(self, statusCode, userHandle, screenName):
1120
Called after loggin in when the server sends status of online contacts.
1121
By default we will update the status attribute of the contact stored
1124
@param statusCode: 3-letter status code
1125
@param userHandle: the contact's user handle (passport)
1126
@param screenName: the contact's screen name
1128
self.factory.contacts.getContact(userHandle).status = statusCode
1130
def contactStatusChanged(self, statusCode, userHandle, screenName):
1132
Called when we're notified that a contact's status has changed.
1133
By default we will update the status attribute of the contact
1134
stored on the factory.
1136
@param statusCode: 3-letter status code
1137
@param userHandle: the contact's user handle (passport)
1138
@param screenName: the contact's screen name
1140
self.factory.contacts.getContact(userHandle).status = statusCode
1142
def contactOffline(self, userHandle):
1144
Called when a contact goes offline. By default this method
1145
will update the status attribute of the contact stored
1148
@param userHandle: the contact's user handle
1150
self.factory.contacts.getContact(userHandle).status = STATUS_OFFLINE
1152
def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
1154
Called when the server sends us phone details about
1155
a specific user (for example after a user is added
1156
the server will send their status, phone details etc.
1157
By default we will update the list version for the
1158
factory's contact list and update the phone details
1159
for the specific user.
1161
@param listVersion: the new list version
1162
@param userHandle: the contact's user handle (passport)
1163
@param phoneType: the specific phoneType
1164
(*_PHONE constants or HAS_PAGER)
1165
@param number: the value/phone number.
1167
self.factory.contacts.version = listVersion
1168
self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
1170
def userAddedMe(self, userHandle, screenName, listVersion):
1172
Called when a user adds me to their list. (ie. they have been added to
1173
the reverse list. By default this method will update the version of
1174
the factory's contact list -- that is, if the contact already exists
1175
it will update the associated lists attribute, otherwise it will create
1176
a new MSNContact object and store it.
1178
@param userHandle: the userHandle of the user
1179
@param screenName: the screen name of the user
1180
@param listVersion: the new list version
1181
@type listVersion: int
1183
self.factory.contacts.version = listVersion
1184
c = self.factory.contacts.getContact(userHandle)
1186
c = MSNContact(userHandle=userHandle, screenName=screenName)
1187
self.factory.contacts.addContact(c)
1188
c.addToList(REVERSE_LIST)
1190
def userRemovedMe(self, userHandle, listVersion):
1192
Called when a user removes us from their contact list
1193
(they are no longer on our reverseContacts list.
1194
By default this method will update the version of
1195
the factory's contact list -- that is, the user will
1196
be removed from the reverse list and if they are no longer
1197
part of any lists they will be removed from the contact
1200
@param userHandle: the contact's user handle (passport)
1201
@param listVersion: the new list version
1203
self.factory.contacts.version = listVersion
1204
c = self.factory.contacts.getContact(userHandle)
1205
c.removeFromList(REVERSE_LIST)
1207
self.factory.contacts.remContact(c.userHandle)
1209
def gotSwitchboardInvitation(self, sessionID, host, port,
1210
key, userHandle, screenName):
1212
Called when we get an invitation to a switchboard server.
1213
This happens when a user requests a chat session with us.
1215
@param sessionID: session ID number, must be remembered for logging in
1216
@param host: the hostname of the switchboard server
1217
@param port: the port to connect to
1218
@param key: used for authorization when connecting
1219
@param userHandle: the user handle of the person who invited us
1220
@param screenName: the screen name of the person who invited us
1224
def multipleLogin(self):
1226
Called when the server says there has been another login
1227
under our account, the server should disconnect us right away.
1231
def serverGoingDown(self):
1233
Called when the server has notified us that it is going down for
1240
def changeStatus(self, status):
1242
Change my current status. This method will add
1243
a default callback to the returned Deferred
1244
which will update the status attribute of the
1247
@param status: 3-letter status code (as defined by
1248
the STATUS_* constants)
1249
@return: A Deferred, the callback of which will be
1250
fired when the server confirms the change
1251
of status. The callback argument will be
1252
a tuple with the new status code as the
1256
id, d = self._createIDMapping()
1257
self.sendLine("CHG %s %s" % (id, status))
1259
self.factory.status = r[0]
1261
return d.addCallback(_cb)
1263
# I am no longer supporting the process of manually requesting
1264
# lists or list groups -- as far as I can see this has no use
1265
# if lists are synchronized and updated correctly, which they
1266
# should be. If someone has a specific justified need for this
1267
# then please contact me and i'll re-enable/fix support for it.
1269
#def requestList(self, listType):
1271
# request the desired list type
1273
# @param listType: (as defined by the *_LIST constants)
1274
# @return: A Deferred, the callback of which will be
1275
# fired when the list has been retrieved.
1276
# The callback argument will be a tuple with
1277
# the only element being a list of MSNContact
1280
# # this doesn't need to ever be used if syncing of the lists takes place
1281
# # i.e. please don't use it!
1282
# warnings.warn("Please do not use this method - use the list syncing process instead")
1283
# id, d = self._createIDMapping()
1284
# self.sendLine("LST %s %s" % (id, listIDToCode[listType].upper()))
1285
# self._setStateData('list',[])
1288
def setPrivacyMode(self, privLevel):
1290
Set my privacy mode on the server.
1293
This only keeps the current privacy setting on
1294
the server for later retrieval, it does not
1295
effect the way the server works at all.
1297
@param privLevel: This parameter can be true, in which
1298
case the server will keep the state as
1299
'al' which the official client interprets
1300
as -> allow messages from only users on
1301
the allow list. Alternatively it can be
1302
false, in which case the server will keep
1303
the state as 'bl' which the official client
1304
interprets as -> allow messages from all
1305
users except those on the block list.
1307
@return: A Deferred, the callback of which will be fired when
1308
the server replies with the new privacy setting.
1309
The callback argument will be a tuple, the 2 elements
1310
of which being the list version and either 'al'
1311
or 'bl' (the new privacy setting).
1314
id, d = self._createIDMapping()
1316
self.sendLine("BLP %s AL" % id)
1318
self.sendLine("BLP %s BL" % id)
1321
def syncList(self, version):
1323
Used for keeping an up-to-date contact list.
1324
A callback is added to the returned Deferred
1325
that updates the contact list on the factory
1326
and also sets my state to STATUS_ONLINE.
1329
This is called automatically upon signing
1330
in using the version attribute of
1331
factory.contacts, so you may want to persist
1332
this object accordingly. Because of this there
1333
is no real need to ever call this method
1336
@param version: The current known list version
1338
@return: A Deferred, the callback of which will be
1339
fired when the server sends an adequate reply.
1340
The callback argument will be a tuple with two
1341
elements, the new list (MSNContactList) and
1342
your current state (a dictionary). If the version
1343
you sent _was_ the latest list version, both elements
1344
will be None. To just request the list send a version of 0.
1347
self._setState('SYNC')
1348
id, d = self._createIDMapping(data=str(version))
1349
self._setStateData('synid',id)
1350
self.sendLine("SYN %s %s" % (id, version))
1352
self.changeStatus(STATUS_ONLINE)
1353
if r[0] is not None:
1354
self.factory.contacts = r[0]
1356
return d.addCallback(_cb)
1359
# I am no longer supporting the process of manually requesting
1360
# lists or list groups -- as far as I can see this has no use
1361
# if lists are synchronized and updated correctly, which they
1362
# should be. If someone has a specific justified need for this
1363
# then please contact me and i'll re-enable/fix support for it.
1365
#def requestListGroups(self):
1367
# Request (forward) list groups.
1369
# @return: A Deferred, the callback for which will be called
1370
# when the server responds with the list groups.
1371
# The callback argument will be a tuple with two elements,
1372
# a dictionary mapping group IDs to group names and the
1373
# current list version.
1376
# # this doesn't need to be used if syncing of the lists takes place (which it SHOULD!)
1377
# # i.e. please don't use it!
1378
# warnings.warn("Please do not use this method - use the list syncing process instead")
1379
# id, d = self._createIDMapping()
1380
# self.sendLine("LSG %s" % id)
1381
# self._setStateData('groups',{})
1384
def setPhoneDetails(self, phoneType, value):
1386
Set/change my phone numbers stored on the server.
1388
@param phoneType: phoneType can be one of the following
1389
constants - HOME_PHONE, WORK_PHONE,
1390
MOBILE_PHONE, HAS_PAGER.
1391
These are pretty self-explanatory, except
1392
maybe HAS_PAGER which refers to whether or
1393
not you have a pager.
1394
@param value: for all of the *_PHONE constants the value is a
1395
phone number (str), for HAS_PAGER accepted values
1396
are 'Y' (for yes) and 'N' (for no).
1398
@return: A Deferred, the callback for which will be fired when
1399
the server confirms the change has been made. The
1400
callback argument will be a tuple with 2 elements, the
1401
first being the new list version (int) and the second
1402
being the new phone number value (str).
1404
# XXX: Add a default callback which updates
1405
# factory.contacts.version and the relevant phone
1407
id, d = self._createIDMapping()
1408
self.sendLine("PRP %s %s %s" % (id, phoneType, quote(value)))
1411
def addListGroup(self, name):
1413
Used to create a new list group.
1414
A default callback is added to the
1415
returned Deferred which updates the
1416
contacts attribute of the factory.
1418
@param name: The desired name of the new group.
1420
@return: A Deferred, the callbacck for which will be called
1421
when the server clarifies that the new group has been
1422
created. The callback argument will be a tuple with 3
1423
elements: the new list version (int), the new group name
1424
(str) and the new group ID (int).
1427
id, d = self._createIDMapping()
1428
self.sendLine("ADG %s %s 0" % (id, quote(name)))
1430
self.factory.contacts.version = r[0]
1431
self.factory.contacts.setGroup(r[1], r[2])
1433
return d.addCallback(_cb)
1435
def remListGroup(self, groupID):
1437
Used to remove a list group.
1438
A default callback is added to the
1439
returned Deferred which updates the
1440
contacts attribute of the factory.
1442
@param groupID: the ID of the desired group to be removed.
1444
@return: A Deferred, the callback for which will be called when
1445
the server clarifies the deletion of the group.
1446
The callback argument will be a tuple with 2 elements:
1447
the new list version (int) and the group ID (int) of
1451
id, d = self._createIDMapping()
1452
self.sendLine("RMG %s %s" % (id, groupID))
1454
self.factory.contacts.version = r[0]
1455
self.factory.contacts.remGroup(r[1])
1457
return d.addCallback(_cb)
1459
def renameListGroup(self, groupID, newName):
1461
Used to rename an existing list group.
1462
A default callback is added to the returned
1463
Deferred which updates the contacts attribute
1466
@param groupID: the ID of the desired group to rename.
1467
@param newName: the desired new name for the group.
1469
@return: A Deferred, the callback for which will be called
1470
when the server clarifies the renaming.
1471
The callback argument will be a tuple of 3 elements,
1472
the new list version (int), the group id (int) and
1473
the new group name (str).
1476
id, d = self._createIDMapping()
1477
self.sendLine("REG %s %s %s 0" % (id, groupID, quote(newName)))
1479
self.factory.contacts.version = r[0]
1480
self.factory.contacts.setGroup(r[1], r[2])
1482
return d.addCallback(_cb)
1484
def addContact(self, listType, userHandle, groupID=0):
1486
Used to add a contact to the desired list.
1487
A default callback is added to the returned
1488
Deferred which updates the contacts attribute of
1489
the factory with the new contact information.
1490
If you are adding a contact to the forward list
1491
and you want to associate this contact with multiple
1492
groups then you will need to call this method for each
1493
group you would like to add them to, changing the groupID
1494
parameter. The default callback will take care of updating
1495
the group information on the factory's contact list.
1497
@param listType: (as defined by the *_LIST constants)
1498
@param userHandle: the user handle (passport) of the contact
1500
@param groupID: the group ID for which to associate this contact
1501
with. (default 0 - default group). Groups are only
1502
valid for FORWARD_LIST.
1504
@return: A Deferred, the callback for which will be called when
1505
the server has clarified that the user has been added.
1506
The callback argument will be a tuple with 4 elements:
1507
the list type, the contact's user handle, the new list
1508
version, and the group id (if relevant, otherwise it
1512
id, d = self._createIDMapping()
1513
listType = listIDToCode[listType].upper()
1514
if listType == "FL":
1515
self.sendLine("ADD %s FL %s %s %s" % (id, userHandle, userHandle, groupID))
1517
self.sendLine("ADD %s %s %s %s" % (id, listType, userHandle, userHandle))
1520
self.factory.contacts.version = r[2]
1521
c = self.factory.contacts.getContact(r[1])
1523
c = MSNContact(userHandle=r[1])
1525
c.groups.append(r[3])
1528
return d.addCallback(_cb)
1530
def remContact(self, listType, userHandle, groupID=0):
1532
Used to remove a contact from the desired list.
1533
A default callback is added to the returned deferred
1534
which updates the contacts attribute of the factory
1535
to reflect the new contact information. If you are
1536
removing from the forward list then you will need to
1537
supply a groupID, if the contact is in more than one
1538
group then they will only be removed from this group
1539
and not the entire forward list, but if this is their
1540
only group they will be removed from the whole list.
1542
@param listType: (as defined by the *_LIST constants)
1543
@param userHandle: the user handle (passport) of the
1544
contact being removed
1545
@param groupID: the ID of the group to which this contact
1546
belongs (only relevant for FORWARD_LIST,
1549
@return: A Deferred, the callback for which will be called when
1550
the server has clarified that the user has been removed.
1551
The callback argument will be a tuple of 4 elements:
1552
the list type, the contact's user handle, the new list
1553
version, and the group id (if relevant, otherwise it will
1557
id, d = self._createIDMapping()
1558
listType = listIDToCode[listType].upper()
1559
if listType == "FL":
1560
self.sendLine("REM %s FL %s %s" % (id, userHandle, groupID))
1562
self.sendLine("REM %s %s %s" % (id, listType, userHandle))
1565
l = self.factory.contacts
1567
c = l.getContact(r[1])
1570
if group: # they may not have been removed from the list
1571
c.groups.remove(group)
1575
c.removeFromList(r[0])
1577
l.remContact(c.userHandle)
1579
return d.addCallback(_cb)
1581
def changeScreenName(self, newName):
1583
Used to change your current screen name.
1584
A default callback is added to the returned
1585
Deferred which updates the screenName attribute
1586
of the factory and also updates the contact list
1589
@param newName: the new screen name
1591
@return: A Deferred, the callback for which will be called
1592
when the server sends an adequate reply.
1593
The callback argument will be a tuple of 2 elements:
1594
the new list version and the new screen name.
1597
id, d = self._createIDMapping()
1598
self.sendLine("REA %s %s %s" % (id, self.factory.userHandle, quote(newName)))
1600
self.factory.contacts.version = r[0]
1601
self.factory.screenName = r[1]
1603
return d.addCallback(_cb)
1605
def requestSwitchboardServer(self):
1607
Used to request a switchboard server to use for conversations.
1609
@return: A Deferred, the callback for which will be called when
1610
the server responds with the switchboard information.
1611
The callback argument will be a tuple with 3 elements:
1612
the host of the switchboard server, the port and a key
1613
used for logging in.
1616
id, d = self._createIDMapping()
1617
self.sendLine("XFR %s SB" % id)
1622
Used to log out of the notification server.
1623
After running the method the server is expected
1624
to close the connection.
1627
self.sendLine("OUT")
1629
class NotificationFactory(ClientFactory):
1631
Factory for the NotificationClient protocol.
1632
This is basically responsible for keeping
1633
the state of the client and thus should be used
1634
in a 1:1 situation with clients.
1636
@ivar contacts: An MSNContactList instance reflecting
1637
the current contact list -- this is
1638
generally kept up to date by the default
1640
@ivar userHandle: The client's userHandle, this is expected
1641
to be set by the client and is used by the
1642
protocol (for logging in etc).
1643
@ivar screenName: The client's current screen-name -- this is
1644
generally kept up to date by the default
1646
@ivar password: The client's password -- this is (obviously)
1647
expected to be set by the client.
1648
@ivar passportServer: This must point to an msn passport server
1649
(the whole URL is required)
1650
@ivar status: The status of the client -- this is generally kept
1651
up to date by the default command handlers
1658
passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
1660
protocol = NotificationClient
1663
# XXX: A lot of the state currently kept in
1664
# instances of SwitchboardClient is likely to
1665
# be moved into a factory at some stage in the
1668
class SwitchboardClient(MSNEventBase):
1670
This class provides support for clients connecting to a switchboard server.
1672
Switchboard servers are used for conversations with other people
1673
on the MSN network. This means that the number of conversations at
1674
any given time will be directly proportional to the number of
1675
connections to varioius switchboard servers.
1677
MSN makes no distinction between single and group conversations,
1678
so any number of users may be invited to join a specific conversation
1679
taking place on a switchboard server.
1681
@ivar key: authorization key, obtained when receiving
1682
invitation / requesting switchboard server.
1683
@ivar userHandle: your user handle (passport)
1684
@ivar sessionID: unique session ID, used if you are replying
1685
to a switchboard invitation
1686
@ivar reply: set this to 1 in connectionMade or before to signifiy
1687
that you are replying to a switchboard invitation.
1698
MSNEventBase.__init__(self)
1699
self.pendingUsers = {}
1700
self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
1702
def connectionMade(self):
1703
MSNEventBase.connectionMade(self)
1704
print 'sending initial stuff'
1707
def connectionLost(self, reason):
1708
self.cookies['iCookies'] = {}
1709
self.cookies['external'] = {}
1710
MSNEventBase.connectionLost(self, reason)
1712
def _sendInit(self):
1714
send initial data based on whether we are replying to an invitation
1717
id = self._nextTransactionID()
1719
self.sendLine("USR %s %s %s" % (id, self.userHandle, self.key))
1721
self.sendLine("ANS %s %s %s %s" % (id, self.userHandle, self.key, self.sessionID))
1723
def _newInvitationCookie(self):
1725
if self._iCookie > 1000:
1727
return self._iCookie
1729
def _checkTyping(self, message, cTypes):
1730
""" helper method for checkMessage """
1731
if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
1732
self.userTyping(message)
1735
def _checkFileInvitation(self, message, info):
1736
""" helper method for checkMessage """
1737
guid = info.get('Application-GUID', '').lower()
1738
name = info.get('Application-Name', '').lower()
1740
# Both fields are required, but we'll let some lazy clients get away
1741
# with only sending a name, if it is easy for us to recognize the
1742
# name (the name is localized, so this check might fail for lazy,
1743
# non-english clients, but I'm not about to include "file transfer"
1744
# in 80 different languages here).
1746
if name != "file transfer" and guid != classNameToGUID["file transfer"]:
1749
cookie = int(info['Invitation-Cookie'])
1750
fileName = info['Application-File']
1751
fileSize = int(info['Application-FileSize'])
1753
log.msg('Received munged file transfer request ... ignoring.')
1755
self.gotSendRequest(fileName, fileSize, cookie, message)
1758
def _checkFileResponse(self, message, info):
1759
""" helper method for checkMessage """
1761
cmd = info['Invitation-Command'].upper()
1762
cookie = int(info['Invitation-Cookie'])
1765
accept = (cmd == 'ACCEPT') and 1 or 0
1766
requested = self.cookies['iCookies'].get(cookie)
1769
requested[0].callback((accept, cookie, info))
1770
del self.cookies['iCookies'][cookie]
1773
def _checkFileInfo(self, message, info):
1774
""" helper method for checkMessage """
1776
ip = info['IP-Address']
1777
iCookie = int(info['Invitation-Cookie'])
1778
aCookie = int(info['AuthCookie'])
1779
cmd = info['Invitation-Command'].upper()
1780
port = int(info['Port'])
1783
accept = (cmd == 'ACCEPT') and 1 or 0
1784
requested = self.cookies['external'].get(iCookie)
1786
return 1 # we didn't ask for this
1787
requested[0].callback((accept, ip, port, aCookie, info))
1788
del self.cookies['external'][iCookie]
1791
def checkMessage(self, message):
1793
hook for detecting any notification type messages
1794
(e.g. file transfer)
1796
cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
1797
if self._checkTyping(message, cTypes):
1799
if 'text/x-msmsgsinvite' in cTypes:
1800
# header like info is sent as part of the message body.
1802
for line in message.message.split('\r\n'):
1804
key, val = line.split(':')
1805
info[key] = val.lstrip()
1808
if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info):
1810
elif 'text/x-clientcaps' in cTypes:
1811
# do something with capabilities
1816
def handle_USR(self, params):
1817
checkParamLen(len(params), 4, 'USR')
1818
if params[1] == "OK":
1822
def handle_CAL(self, params):
1823
checkParamLen(len(params), 3, 'CAL')
1825
if params[1].upper() == "RINGING":
1826
self._fireCallback(id, int(params[2])) # session ID as parameter
1829
def handle_JOI(self, params):
1830
checkParamLen(len(params), 2, 'JOI')
1831
self.userJoined(params[0], unquote(params[1]))
1833
# users participating in the current chat
1834
def handle_IRO(self, params):
1835
checkParamLen(len(params), 5, 'IRO')
1836
self.pendingUsers[params[3]] = unquote(params[4])
1837
if params[1] == params[2]:
1838
self.gotChattingUsers(self.pendingUsers)
1839
self.pendingUsers = {}
1841
# finished listing users
1842
def handle_ANS(self, params):
1843
checkParamLen(len(params), 2, 'ANS')
1844
if params[1] == "OK":
1847
def handle_ACK(self, params):
1848
checkParamLen(len(params), 1, 'ACK')
1849
self._fireCallback(int(params[0]), None)
1851
def handle_NAK(self, params):
1852
checkParamLen(len(params), 1, 'NAK')
1853
self._fireCallback(int(params[0]), None)
1855
def handle_BYE(self, params):
1856
#checkParamLen(len(params), 1, 'BYE') # i've seen more than 1 param passed to this
1857
self.userLeft(params[0])
1863
called when all login details have been negotiated.
1864
Messages can now be sent, or new users invited.
1868
def gotChattingUsers(self, users):
1870
called after connecting to an existing chat session.
1872
@param users: A dict mapping user handles to screen names
1873
(current users taking part in the conversation)
1877
def userJoined(self, userHandle, screenName):
1879
called when a user has joined the conversation.
1881
@param userHandle: the user handle (passport) of the user
1882
@param screenName: the screen name of the user
1886
def userLeft(self, userHandle):
1888
called when a user has left the conversation.
1890
@param userHandle: the user handle (passport) of the user.
1894
def gotMessage(self, message):
1896
called when we receive a message.
1898
@param message: the associated MSNMessage object
1902
def userTyping(self, message):
1904
called when we receive the special type of message notifying
1905
us that a user is typing a message.
1907
@param message: the associated MSNMessage object
1911
def gotSendRequest(self, fileName, fileSize, iCookie, message):
1913
called when a contact is trying to send us a file.
1914
To accept or reject this transfer see the
1915
fileInvitationReply method.
1917
@param fileName: the name of the file
1918
@param fileSize: the size of the file
1919
@param iCookie: the invitation cookie, used so the client can
1920
match up your reply with this request.
1921
@param message: the MSNMessage object which brought about this
1922
invitation (it may contain more information)
1928
def inviteUser(self, userHandle):
1930
used to invite a user to the current switchboard server.
1932
@param userHandle: the user handle (passport) of the desired user.
1934
@return: A Deferred, the callback for which will be called
1935
when the server notifies us that the user has indeed
1936
been invited. The callback argument will be a tuple
1937
with 1 element, the sessionID given to the invited user.
1938
I'm not sure if this is useful or not.
1941
id, d = self._createIDMapping()
1942
self.sendLine("CAL %s %s" % (id, userHandle))
1945
def sendMessage(self, message):
1947
used to send a message.
1949
@param message: the corresponding MSNMessage object.
1951
@return: Depending on the value of message.ack.
1952
If set to MSNMessage.MESSAGE_ACK or
1953
MSNMessage.MESSAGE_NACK a Deferred will be returned,
1954
the callback for which will be fired when an ACK or
1955
NACK is received - the callback argument will be
1956
(None,). If set to MSNMessage.MESSAGE_ACK_NONE then
1957
the return value is None.
1960
if message.ack not in ('A','N'):
1961
id, d = self._nextTransactionID(), None
1963
id, d = self._createIDMapping()
1964
if message.length == 0:
1965
message.length = message._calcMessageLen()
1966
self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
1967
# apparently order matters with at least MIME-Version and Content-Type
1968
self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
1969
self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
1970
# send the rest of the headers
1971
for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
1972
self.sendLine("%s: %s" % (header[0], header[1]))
1973
self.transport.write(CR+LF)
1974
self.transport.write(message.message)
1977
def sendTypingNotification(self):
1979
used to send a typing notification. Upon receiving this
1980
message the official client will display a 'user is typing'
1981
message to all other users in the chat session for 10 seconds.
1982
The official client sends one of these every 5 seconds (I think)
1983
as long as you continue to type.
1986
m.ack = m.MESSAGE_ACK_NONE
1987
m.setHeader('Content-Type', 'text/x-msmsgscontrol')
1988
m.setHeader('TypingUser', self.userHandle)
1992
def sendFileInvitation(self, fileName, fileSize):
1994
send an notification that we want to send a file.
1996
@param fileName: the file name
1997
@param fileSize: the file size
1999
@return: A Deferred, the callback of which will be fired
2000
when the user responds to this invitation with an
2001
appropriate message. The callback argument will be
2002
a tuple with 3 elements, the first being 1 or 0
2003
depending on whether they accepted the transfer
2004
(1=yes, 0=no), the second being an invitation cookie
2005
to identify your follow-up responses and the third being
2006
the message 'info' which is a dict of information they
2007
sent in their reply (this doesn't really need to be used).
2008
If you wish to proceed with the transfer see the
2009
sendTransferInfo method.
2011
cookie = self._newInvitationCookie()
2014
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2015
m.message += 'Application-Name: File Transfer\r\n'
2016
m.message += 'Application-GUID: %s\r\n' % (classNameToGUID["file transfer"],)
2017
m.message += 'Invitation-Command: INVITE\r\n'
2018
m.message += 'Invitation-Cookie: %s\r\n' % str(cookie)
2019
m.message += 'Application-File: %s\r\n' % fileName
2020
m.message += 'Application-FileSize: %s\r\n\r\n' % str(fileSize)
2021
m.ack = m.MESSAGE_ACK_NONE
2023
self.cookies['iCookies'][cookie] = (d, m)
2026
def fileInvitationReply(self, iCookie, accept=1):
2028
used to reply to a file transfer invitation.
2030
@param iCookie: the invitation cookie of the initial invitation
2031
@param accept: whether or not you accept this transfer,
2032
1 = yes, 0 = no, default = 1.
2034
@return: A Deferred, the callback for which will be fired when
2035
the user responds with the transfer information.
2036
The callback argument will be a tuple with 5 elements,
2037
whether or not they wish to proceed with the transfer
2038
(1=yes, 0=no), their ip, the port, the authentication
2039
cookie (see FileReceive/FileSend) and the message
2040
info (dict) (in case they send extra header-like info
2041
like Internal-IP, this doesn't necessarily need to be
2042
used). If you wish to proceed with the transfer see
2047
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2048
m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2049
m.message += 'Invitation-Cookie: %s\r\n' % str(iCookie)
2051
m.message += 'Cancel-Code: REJECT\r\n'
2052
m.message += 'Launch-Application: FALSE\r\n'
2053
m.message += 'Request-Data: IP-Address:\r\n'
2055
m.ack = m.MESSAGE_ACK_NONE
2057
self.cookies['external'][iCookie] = (d, m)
2060
def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
2062
send information relating to a file transfer session.
2064
@param accept: whether or not to go ahead with the transfer
2066
@param iCookie: the invitation cookie of previous replies
2067
relating to this transfer
2068
@param authCookie: the authentication cookie obtained from
2069
an FileSend instance
2071
@param port: the port on which an FileSend protocol is listening.
2074
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
2075
m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
2076
m.message += 'Invitation-Cookie: %s\r\n' % iCookie
2077
m.message += 'IP-Address: %s\r\n' % ip
2078
m.message += 'Port: %s\r\n' % port
2079
m.message += 'AuthCookie: %s\r\n' % authCookie
2081
m.ack = m.MESSAGE_NACK
2084
class FileReceive(LineReceiver):
2086
This class provides support for receiving files from contacts.
2088
@ivar fileSize: the size of the receiving file. (you will have to set this)
2089
@ivar connected: true if a connection has been established.
2090
@ivar completed: true if the transfer is complete.
2091
@ivar bytesReceived: number of bytes (of the file) received.
2092
This does not include header data.
2095
def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
2097
@param auth: auth string received in the file invitation.
2098
@param myUserHandle: your userhandle.
2099
@param file: A string or file object represnting the file
2101
@param directory: optional parameter specifiying the directory.
2102
Defaults to the current directory.
2103
@param overwrite: if true and a file of the same name exists on
2104
your system, it will be overwritten. (0 by default)
2107
self.myUserHandle = myUserHandle
2111
self.directory = directory
2112
self.bytesReceived = 0
2113
self.overwrite = overwrite
2115
# used for handling current received state
2116
self.state = 'CONNECTING'
2117
self.segmentLength = 0
2120
if isinstance(file, types.StringType):
2121
path = os.path.join(directory, file)
2122
if os.path.exists(path) and not self.overwrite:
2123
log.msg('File already exists...')
2124
raise IOError, "File Exists" # is this all we should do here?
2125
self.file = open(os.path.join(directory, file), 'wb')
2129
def connectionMade(self):
2131
self.state = 'INHEADER'
2132
self.sendLine('VER MSNFTP')
2134
def connectionLost(self, reason):
2138
def parseHeader(self, header):
2139
""" parse the header of each 'message' to obtain the segment length """
2141
if ord(header[0]) != 0: # they requested that we close the connection
2142
self.transport.loseConnection()
2145
extra, factor = header[1:]
2147
# munged header, ending transfer
2148
self.transport.loseConnection()
2151
factor = ord(factor)
2152
return factor * 256 + extra
2154
def lineReceived(self, line):
2161
handler = getattr(self, "handle_%s" % cmd.upper(), None)
2163
handler(params) # try/except
2165
self.handle_UNKNOWN(cmd, params)
2167
def rawDataReceived(self, data):
2168
bufferLen = len(self.buffer)
2169
if self.state == 'INHEADER':
2171
self.buffer += data[:delim]
2172
if len(self.buffer) == 3:
2173
self.segmentLength = self.parseHeader(self.buffer)
2174
if not self.segmentLength:
2177
self.state = 'INSEGMENT'
2178
extra = data[delim:]
2180
self.rawDataReceived(extra)
2183
elif self.state == 'INSEGMENT':
2184
dataSeg = data[:(self.segmentLength-bufferLen)]
2185
self.buffer += dataSeg
2186
self.bytesReceived += len(dataSeg)
2187
if len(self.buffer) == self.segmentLength:
2188
self.gotSegment(self.buffer)
2190
if self.bytesReceived == self.fileSize:
2194
self.sendLine("BYE 16777989")
2196
self.state = 'INHEADER'
2197
extra = data[(self.segmentLength-bufferLen):]
2199
self.rawDataReceived(extra)
2202
def handle_VER(self, params):
2203
checkParamLen(len(params), 1, 'VER')
2204
if params[0].upper() == "MSNFTP":
2205
self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
2207
log.msg('they sent the wrong version, time to quit this transfer')
2208
self.transport.loseConnection()
2210
def handle_FIL(self, params):
2211
checkParamLen(len(params), 1, 'FIL')
2213
self.fileSize = int(params[0])
2214
except ValueError: # they sent the wrong file size - probably want to log this
2215
self.transport.loseConnection()
2218
self.sendLine("TFR")
2220
def handle_UNKNOWN(self, cmd, params):
2221
log.msg('received unknown command (%s), params: %s' % (cmd, params))
2223
def gotSegment(self, data):
2224
""" called when a segment (block) of data arrives. """
2225
self.file.write(data)
2227
class FileSend(LineReceiver):
2229
This class provides support for sending files to other contacts.
2231
@ivar bytesSent: the number of bytes that have currently been sent.
2232
@ivar completed: true if the send has completed.
2233
@ivar connected: true if a connection has been established.
2234
@ivar targetUser: the target user (contact).
2235
@ivar segmentSize: the segment (block) size.
2236
@ivar auth: the auth cookie (number) to use when sending the
2240
def __init__(self, file):
2242
@param file: A string or file object represnting the file to send.
2245
if isinstance(file, types.StringType):
2246
self.file = open(file, 'rb')
2254
self.targetUser = None
2255
self.segmentSize = 2045
2256
self.auth = randint(0, 2**30)
2257
self._pendingSend = None # :(
2259
def connectionMade(self):
2262
def connectionLost(self, reason):
2263
if self._pendingSend.active():
2264
self._pendingSend.cancel()
2265
self._pendingSend = None
2266
if self.bytesSent == self.fileSize:
2271
def lineReceived(self, line):
2278
handler = getattr(self, "handle_%s" % cmd.upper(), None)
2282
self.handle_UNKNOWN(cmd, params)
2284
def handle_VER(self, params):
2285
checkParamLen(len(params), 1, 'VER')
2286
if params[0].upper() == "MSNFTP":
2287
self.sendLine("VER MSNFTP")
2288
else: # they sent some weird version during negotiation, i'm quitting.
2289
self.transport.loseConnection()
2291
def handle_USR(self, params):
2292
checkParamLen(len(params), 2, 'USR')
2293
self.targetUser = params[0]
2294
if self.auth == int(params[1]):
2295
self.sendLine("FIL %s" % (self.fileSize))
2296
else: # they failed the auth test, disconnecting.
2297
self.transport.loseConnection()
2299
def handle_TFR(self, params):
2300
checkParamLen(len(params), 0, 'TFR')
2301
# they are ready for me to start sending
2304
def handle_BYE(self, params):
2305
self.completed = (self.bytesSent == self.fileSize)
2306
self.transport.loseConnection()
2308
def handle_CCL(self, params):
2309
self.completed = (self.bytesSent == self.fileSize)
2310
self.transport.loseConnection()
2312
def handle_UNKNOWN(self, cmd, params):
2313
log.msg('received unknown command (%s), params: %s' % (cmd, params))
2315
def makeHeader(self, size):
2316
""" make the appropriate header given a specific segment size. """
2317
quotient, remainder = divmod(size, 256)
2318
return chr(0) + chr(remainder) + chr(quotient)
2321
""" send a segment of data """
2322
if not self.connected:
2323
self._pendingSend = None
2324
return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
2325
data = self.file.read(self.segmentSize)
2327
dataSize = len(data)
2328
header = self.makeHeader(dataSize)
2329
self.bytesSent += dataSize
2330
self.transport.write(header + data)
2331
self._pendingSend = reactor.callLater(0, self.sendPart)
2333
self._pendingSend = None
2336
# mapping of error codes to error messages
2339
200 : "Syntax error",
2340
201 : "Invalid parameter",
2341
205 : "Invalid user",
2342
206 : "Domain name missing",
2343
207 : "Already logged in",
2344
208 : "Invalid username",
2345
209 : "Invalid screen name",
2346
210 : "User list full",
2347
215 : "User already there",
2348
216 : "User already on list",
2349
217 : "User not online",
2350
218 : "Already in mode",
2351
219 : "User is in the opposite list",
2352
223 : "Too many groups",
2353
224 : "Invalid group",
2354
225 : "User not in group",
2355
229 : "Group name too long",
2356
230 : "Cannot remove group 0",
2357
231 : "Invalid group",
2358
280 : "Switchboard failed",
2359
281 : "Transfer to switchboard failed",
2361
300 : "Required field missing",
2362
301 : "Too many FND responses",
2363
302 : "Not logged in",
2365
500 : "Internal server error",
2366
501 : "Database server error",
2367
502 : "Command disabled",
2368
510 : "File operation failed",
2369
520 : "Memory allocation failed",
2370
540 : "Wrong CHL value sent to server",
2372
600 : "Server is busy",
2373
601 : "Server is unavaliable",
2374
602 : "Peer nameserver is down",
2375
603 : "Database connection failed",
2376
604 : "Server is going down",
2377
605 : "Server unavailable",
2379
707 : "Could not create connection",
2380
710 : "Invalid CVR parameters",
2381
711 : "Write is blocking",
2382
712 : "Session is overloaded",
2383
713 : "Too many active users",
2384
714 : "Too many sessions",
2385
715 : "Not expected",
2386
717 : "Bad friend file",
2387
731 : "Not expected",
2389
800 : "Requests too rapid",
2391
910 : "Server too busy",
2392
911 : "Authentication failed",
2393
912 : "Server too busy",
2394
913 : "Not allowed when offline",
2395
914 : "Server too busy",
2396
915 : "Server too busy",
2397
916 : "Server too busy",
2398
917 : "Server too busy",
2399
918 : "Server too busy",
2400
919 : "Server too busy",
2401
920 : "Not accepting new users",
2402
921 : "Server too busy",
2403
922 : "Server too busy",
2404
923 : "No parent consent",
2405
924 : "Passport account not yet verified"
2409
# mapping of status codes to readable status format
2412
STATUS_ONLINE : "Online",
2413
STATUS_OFFLINE : "Offline",
2414
STATUS_HIDDEN : "Appear Offline",
2415
STATUS_IDLE : "Idle",
2416
STATUS_AWAY : "Away",
2417
STATUS_BUSY : "Busy",
2418
STATUS_BRB : "Be Right Back",
2419
STATUS_PHONE : "On the Phone",
2420
STATUS_LUNCH : "Out to Lunch"
2424
# mapping of list ids to list codes
2427
FORWARD_LIST : 'fl',
2434
# mapping of list codes to list ids
2436
for id,code in listIDToCode.items():
2437
listCodeToID[code] = id
2441
# Mapping of class GUIDs to simple english names
2443
"{5D3E02AB-6190-11d3-BBBB-00C04F795683}": "file transfer",
2446
# Reverse of the above
2447
classNameToGUID = {}
2448
for guid, name in guidToClassName.iteritems():
2449
classNameToGUID[name] = guid